Partager via


Integrating Custom Identity provider with Asp.net and Provider hosted apps to achieve single sign on with Forms Based Authentication

This post is a contribution from Sohail Sayed, an engineer with the SharePoint Developer Support team

We demonstrated creating a custom identity provider and integrating it with SharePoint to achieve single sign on in the blog post https://blogs.technet.microsoft.com/sharepointdevelopersupport/2017/07/07/creating-a-custom-identity-provider-and-integrating-with-sharepoint-to-achieve-single-sign-on-with-fba-across-multiple-web-applications/. In this blog we will demonstrate integrating the custom identity provider with Asp.Net web sites and Provider hosted apps.

Create a new asp.net web site.
We will start by creating a new asp.net web site. In my case I chose the Empty Web Site Template. This will create an empty web site project that will only contain the web.config file. Add a new web form to the site. If you have an existing asp.net site update the Web.Config with the configuration mentioned in the subsequent sections.

 

Web.Config configuration
The below sections describe the various web.config changes required. Note I am using the configuration for Microsoft.IdentityModel namespace. The same configuration can be done using System.IdentityModel namespace. More information on this can be found at /en-us/dotnet/framework/security/namespace-mapping-between-wif-3-5-and-wif-4-5

 

Define the configSections
We need to add the configSections that defines the rest of the configuration. Add the below tags. If your web.config already has a <configSections> element just add the <section> tag.

   <configSections>
    <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
  </configSections>

 

Modify the system.web section
Modify the <system.web> section as below. Note the requestValidationType defined in the httpRuntimeElement. This defines a class that will perform validation of the sign in request. This class is invoked by the IsSignInResponse method of the Microsoft.IdentityModel.Web.WSFederationAuthenticationModule. We will define this class in a later section in this blog

   <system.web>
    <compilation targetFramework="4.5" debug="true">
      <assemblies>
        <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      </assemblies>
    </compilation>
    <authentication mode="None"/>
    <authorization>
      <deny users="?"/>
    </authorization>
    <pages controlRenderingCompatibilityVersion="4.0"/>
    <httpRuntime requestValidationType="SampleRequestValidator"/>
  </system.web>

 

Define system.webServer section
Add the <system.webServer> section which adds the WSFederationAuthenticationModule. This module is responsible for securing the asp.net site and handling the redirection to the custom identity provider

   <system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules>
      <add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
      <add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler"/>
    </modules>
  </system.webServer>

 

Define the microsoft.identityModel section
Finally add the <microsoft.identityModel> section. This section defines the following

    1. Audience Uris indicating the valid audience for the identity provider
    2. Issuer and realm information
    3. Certificate details used for trust and communication
   <microsoft.identityModel>
    <service>
      <audienceUris>
        <add value="https://custom.contoso.com/WebSite1/"/>
      </audienceUris>
      <federatedAuthentication>
        <wsFederation passiveRedirectEnabled="true" issuer="https://custom.contoso.com/customidp/" realm="https://custom.contoso.com/WebSite1/" requireHttps="true"/>
        <cookieHandler requireSsl="false"/>
      </federatedAuthentication>
      <serviceCertificate>
        <certificateReference x509FindType="FindByThumbprint" findValue="88c1d6ea0c3d9ee05459403cc8121e6ace5a7edd" storeLocation="LocalMachine" storeName="My"/>
      </serviceCertificate>
      <applicationService>
        <claimTypeRequired>
          <claimType type="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" optional="true"/>
          <claimType type="https://schemas.microsoft.com/ws/2008/06/identity/claims/role" optional="true"/>
          <claimType type="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" optional="true"/>
        </claimTypeRequired>
      </applicationService>
      <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
        <trustedIssuers>
          <add thumbprint="88c1d6ea0c3d9ee05459403cc8121e6ace5a7edd" name="https://custom.contoso.com/customidp/"/>
        </trustedIssuers>
      </issuerNameRegistry>
    </service>
  </microsoft.identityModel>

 

We need to make the following changes to the above section

  • Modify the value in the <audienceUris> and the value of the realm attribute in <wsFederation> tag to the url of the current web site.
  • Change the issuer attribute in the <wsFederation> and the name attribute in the <trustedIssuers>/<add> tag to the url of the trusted identity provider
  • Update the findValue attribute the <certificateReference> tag and the thumbprint attribute <trustedIssuers>/<add> tag with the thumbprint of the certificate used by the trusted identity provider.

Note when copying the thumbprint from the certificate a special character gets added to the start of the thumbprint. This character is not visible in notepad. To remove this copy the thumbprint to the notepad. Press home key on the keyboard so that the cursor is at the beginning of the thumbprint value. Press delete key twice till the first character of the thumbprint is deleted. Manually type back the character.

 

Define the SampleRequestValidator class
In the “Modify system.web section” section we had specified the requestValidationType as SampleRequestValidator. We need to define this class now. Add a new class to the project and name it as SampleRequestValidator. Overwrite the code of this class with the below code

 using System;
using System.Web;
using System.Web.Util;

using Microsoft.IdentityModel.Protocols.WSFederation;

/// <summary>
/// This SampleRequestValidator validates the wresult parameter of the
/// WS-Federation passive protocol by checking for a SignInResponse message
/// in the form post. The SignInResponse message contents are verified later by
/// the WSFederationPassiveAuthenticationModule or the WIF signin controls.
/// </summary>

public class SampleRequestValidator : RequestValidator
{
    protected override bool IsValidRequestString(HttpContext context, string value, RequestValidationSource requestValidationSource, string collectionKey, out int validationFailureIndex)
    {
        validationFailureIndex = 0;

        if (requestValidationSource == RequestValidationSource.Form && collectionKey.Equals(WSFederationConstants.Parameters.Result, StringComparison.Ordinal))
        {
            SignInResponseMessage message = WSFederationMessage.CreateFromFormPost(context.Request) as SignInResponseMessage;

            if (message != null)
            {
                return true;
            }
        }

        return base.IsValidRequestString(context, value, requestValidationSource, collectionKey, out validationFailureIndex);
    }

}

 

You can find more information on this class at https://social.technet.microsoft.com/wiki/contents/articles/1725.wif-troubleshooting-a-potentially-dangerous-request-form-value-was-detected-from-the-client.aspx

 

Read the user value

Finally we need to be able to read the user value and the claims.

Update the code in the default.aspx Page Load method as below

     protected void Page_Load(object sender, EventArgs e)
    {
        if (HttpContext.Current.User.Identity.IsAuthenticated)
        {            
            Microsoft.IdentityModel.Claims.ClaimsPrincipal cp = HttpContext.Current.User as Microsoft.IdentityModel.Claims.ClaimsPrincipal;

            if (cp != null)
            {
                foreach (Microsoft.IdentityModel.Claims.Claim claim in cp.Identities[0].Claims)
                {
                    Response.Write(claim.ClaimType + " - " + claim.Value +"<br>");
                }
            }
        }
    }

 

Browse the page. It should redirect you to the custom identity provider login page if you are not already logged in. After successfully login you should be able to see the claims on the page.

Additional Configuration for Provider hosted add-ins.
Provider hosted apps are asp.net web sites and will use the same configuration as defined above. We need to make some additional changes to be able to make calls into SharePoint.

Firstly you need to configure the Provider hosted add-in to use SAML as mentioned in this blog from Steve Peschka

https://samlman.wordpress.com/2015/03/01/using-sharepoint-apps-with-saml-and-fba-sites-in-sharepoint-2013/

The helper classes mentioned in the above blog are available at the below link

https://samlman.wordpress.com/2015/02/28/an-updated-claimstokenhelper-for-sharepoint-2013-high-trust-apps-and-saml/

By default in this code the RetrieveIdentityForSamlClaimsUser method of the TokenHelper class casts the HttpContext.Current.User object as System.IdentityModel.Claims.ClaimsPrincipal. Since in the web.config we are using the configuration for Microsoft.IdentityModel the above operation will return null. To fix this go to the TokenHelper.RetrieveIdentityForSamlClaimsUser.

Find the line System.IdentityModel.Claims.ClaimsPrincipal cp = UserPrincipal as System.IdentityModel.Claims.ClaimsPrincipal;

Change this to

Microsoft.IdentityModel.Claims.ClaimsPrincipal cp = UserPrincipal as Microsoft.IdentityModel.Claims.ClaimsPrincipal;

This should now retrieve a valid claims identity.

In addition in the default.aspx use SMTP to retrieve the ClientContext since we are using email address as the identity claim

             var spContext = SharePointContextProvider.Current.GetSharePointContext(Context);

            // using (var clientContext = spContext.CreateUserClientContextForSPHost())
            using (var clientContext = spContext.CreateUserClientContextForSPHost(TokenHelper.IdentityClaimType.SMTP))
            {
                clientContext.Load(clientContext.Web, web => web.Title);
                clientContext.ExecuteQuery();
                Response.Write(clientContext.Web.Title);
            }

 

If you now deploy the provider hosted add-in you may still get a 401 error. To fix this we need to update the user profile of that user. This is because the user rehydration process in SharePoint will map the identity claim to one of the fields in the user profile. In our case since we are using SMTP in the Provider hosted app when getting the ClientContext we need to ensure the WorkEmail field in the user profile for the current user in SharePoint Central Administration matches the value of the identity claim. The Provider hosted add-in will start working after this change. For more information on user profile configuration with Provider hosted add-ins refer to the below blog

https://samlman.wordpress.com/2015/03/01/oauth-and-the-rehydrated-user-in-sharepoint-2013-howd-they-do-that-and-what-do-i-need-to-know/