Google+ Followers

23 Oct 2013

Authentication against CRM contact using custom STS

Scenario
In claim based scenarios, sometimes we might need to authenticate against CRM contacts. CRM system might be acting more as backend or internal system. We can have a web application or might be a Sharepoint application which wants to authenticate end users against CRM entities. I used this in some application, and I find it interesting and useful to share.
Let’s try to understand the scenario:

Figure 1: Sharepoint system is authenticated against CRM using ADFS 2 and custom STS.
Look into the roles of various components involved here:
Client: Any PC where we want to access a SharePoint application. Client accesses the SharePoint URL. User enters the credentials in the 3rd step.
SharePoint Application: This application is configured as relying party in ADFS. It will request claims from ADFS.
ADFS: Active Directory Federation Service is Optional here. If we need to authenticate against AD and/or we need to federate among various claim/authentication providers then we would need this, otherwise we don’t. We can just use a custom STS with web application configured as relying party.
Custom STS (Security Token Service): A custom STS based on ASP.Net web application or WCF. It is configured as another authentication provider in ADFS. It creates required claims after successful authentication in federation scenario using ADFS.
CRM System: Custom STS accesses CRM using organization service to fetch contact details and authenticate against the details entered by user.

Now we can now go thru the code to achieve this in step by step manner. I am going to create a WCF based STS which can be accessed as service from any exiting STS (Active federation). This can be done using ASP.Net STS with a login page (Passive federation).
Steps to follow (I might give a miss to detailed explanation of claim fundamentals, rather I would like to focus more on custom STS and how to use it to authenticate against CRM contacts:-
Environment: Visual Studio 2010, CRM Dlls, IIS.
1.   Create WCF STS project using Visual Studio 2010:
Go to new website and then select following-


Select Web location as HTTP and URL like: https://localhost/AuthUsingCRM/.
It will create a startup project with basic code for WCF based STS.
Default Project structure:  
It basically generates following:

Some important classes generated:
CustomSecurityTokenService: WCF service for STS (The custom STS class)
public class CustomSecurityTokenService : SecurityTokenService
{
    // TODO: Set enableAppliesToValidation to true to enable only the RP Url's specified in the ActiveClaimsAwareApps array to get a token from this STS
    static bool enableAppliesToValidation = false;

    // TODO: Add relying party Url's that will be allowed to get token from this STS
    static readonly string[] ActiveClaimsAwareApps = { /*"https://localhost/ActiveClaimsAwareWebApp"*/ };

    /// 
    /// Creates an instance of CustomSecurityTokenService.
    /// 
    /// The SecurityTokenServiceConfiguration.
    public CustomSecurityTokenService( SecurityTokenServiceConfiguration configuration )
        : base( configuration )
    {
    }

    /// 
    /// Validates appliesTo and throws an exception if the appliesTo is null or contains an unexpected address.
    /// 
    /// The AppliesTo value that came in the RST.
    /// If 'appliesTo' parameter is null.
    /// If 'appliesTo' is not valid.
    void ValidateAppliesTo( EndpointAddress appliesTo )
    {
        if ( appliesTo == null )
        {
            throw new ArgumentNullException( "appliesTo" );
        }

        // TODO: Enable AppliesTo validation for allowed relying party Urls by setting enableAppliesToValidation to true. By default it is false.
        if ( enableAppliesToValidation )
        {
            bool validAppliesTo = false;
            foreach ( string rpUrl in ActiveClaimsAwareApps )
            {
                if ( appliesTo.Uri.Equals( new Uri( rpUrl ) ) )
                {
                    validAppliesTo = true;
                    break;
                }
            }

            if ( !validAppliesTo )
            {
                throw new InvalidRequestException( String.Format( "The 'appliesTo' address '{0}' is not valid.", appliesTo.Uri.OriginalString ) );
            }
        }
    }

    /// 
    /// This method returns the configuration for the token issuance request. The configuration
    /// is represented by the Scope class. In our case, we are only capable of issuing a token for a
    /// single RP identity represented by the EncryptingCertificateName.
    /// 
    /// The caller's principal.
    /// The incoming RST.
    /// The scope information to be used for the token issuance.
    protected override Scope GetScope( IClaimsPrincipal principal, RequestSecurityToken request )
    {
        ValidateAppliesTo( request.AppliesTo );

        //
        // Note: The signing certificate used by default has a Distinguished name of "CN=STSTestCert",
        // and is located in the Personal certificate store of the Local Computer. Before going into production,
        // ensure that you change this certificate to a valid CA-issued certificate as appropriate.
        //
        Scope scope = new Scope( request.AppliesTo.Uri.OriginalString, SecurityTokenServiceConfiguration.SigningCredentials );

        string encryptingCertificateName = WebConfigurationManager.AppSettings[ "EncryptingCertificateName" ];
        if ( !string.IsNullOrEmpty( encryptingCertificateName ) )
        {
            // Important note on setting the encrypting credentials.
            // In a production deployment, you would need to select a certificate that is specific to the RP that is requesting the token.
            // You can examine the 'request' to obtain information to determine the certificate to use.
            scope.EncryptingCredentials = new X509EncryptingCredentials( CertificateUtil.GetCertificate( StoreName.My, StoreLocation.LocalMachine, encryptingCertificateName ) );
        }
        else
        {
            // If there is no encryption certificate specified, the STS will not perform encryption.
            // This will succeed for tokens that are created without keys (BearerTokens) or asymmetric keys.  Symmetric keys are
            // required to be 'wrapped' and the STS will throw.
            scope.TokenEncryptionRequired = false;

            // Symmetric keys are required to be 'wrapped' or the STS will throw, uncomment the code below to turn off proof key encryption.
            // Turning off proof key encryption is not secure and should not be used in a deployment scenario.

            // scope.SymmetricKeyEncryptionRequired = false;
        }

        return scope;
    }


    /// 
    /// This method returns the claims to be issued in the token.
    /// 
    /// The caller's principal.
    /// The incoming RST, can be used to obtain addtional information.
    /// The scope information corresponding to this request./// 
    /// If 'principal' parameter is null.
    /// The outgoing claimsIdentity to be included in the issued token.
    protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope )
    {
        if ( null == principal )
        {
            throw new ArgumentNullException( "principal" );
        }

        ClaimsIdentity outputIdentity = new ClaimsIdentity();

        // Issue custom claims.
        // TODO: Change the claims below to issue custom claims required by your application.
        // Update the application's configuration file too to reflect new claims requirement.

        outputIdentity.Claims.Add( new Claim( System.IdentityModel.Claims.ClaimTypes.Name, principal.Identity.Name ) );
        outputIdentity.Claims.Add( new Claim( ClaimTypes.Role, "Manager" ) );

        return outputIdentity;
    }
}
CustomSecurityTokenServiceConfiguration: We can use this class to override some of the default configurations. Later in the article, I will override the default user name token handler behavior.
/// 
/// A custom SecurityTokenServiceConfiguration implementation.
/// 
public class CustomSecurityTokenServiceConfiguration : SecurityTokenServiceConfiguration
{
    /// 
    /// CustomSecurityTokenServiceConfiguration constructor.
    /// 
    public CustomSecurityTokenServiceConfiguration()
        : base( WebConfigurationManager.AppSettings[Common.IssuerName],
                new X509SigningCredentials( CertificateUtil.GetCertificate(
                    StoreName.My, StoreLocation.LocalMachine,
                    WebConfigurationManager.AppSettings[Common.SigningCertificateName] ) ) )
    {
        this.SecurityTokenService = typeof( CustomSecurityTokenService );
    }
}

CertificateUtil: A kind of helper class to access certificates from local store.
FederationMetadata.xml: Default federation metadata definitions which are exposed by this custom STS. It has all the necessary information such as issuer name, certificate info, endpoint address or exposed claims.

Web.config: It plays a major role in setting up this STS correctly. We will discuss this in detail in 2nd step.

2.   Modify default web.config: It should be modified as following –
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <configSections>
    <section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
  </configSections>
  <appSettings>
    <add key="IssuerName" value="ActiveSTSForCRM"/>
    <add key="SigningCertificateName" value="CN=DEV01.corp.contoso.com"/>
    <add key="EncryptingCertificateName" value="CN=DEV01.corp.contoso.com"/>
  </appSettings>
  <location path="FederationMetadata">
    <system.web>
      <authorization>
        <allow users="*"/>
      </authorization>
    </system.web>
  </location>
  
  <system.web>
    <compilation debug="true" targetFramework="4.0">
      <assemblies>
        <add assembly="Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      </assemblies>
    </compilation>
    <authentication mode="None"/>
    <pages>
      <controls>
        <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
      </controls>
    </pages>
  </system.web>
  
  <system.web.extensions>
    <scripting>
      <webServices>
      </webServices>
    </scripting>
  </system.web.extensions>

  <system.serviceModel>
    <services>
      <service name="Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceContract" behaviorConfiguration="ServiceBehavior">
        <endpoint address="IWSTrust13" binding="ws2007HttpBinding" contract="Microsoft.IdentityModel.Protocols.WSTrust.IWSTrust13SyncContract" bindingConfiguration="ws2007HttpBindingConfiguration"/>
        <host>
          <baseAddresses>
            <add baseAddress="https://DEV01.corp.contoso.com/AAMCustomSTS/Service.svc"/>
            <add baseAddress="http:// devcrm01.contoso.com/dev
 /XRMServices/2011/Organization.svc"/>
          </baseAddresses>
        </host>
        <endpoint address="mex" binding="ws2007HttpBinding" contract="IMetadataExchange" bindingConfiguration="ws2007HttpBindingConfiguration"/>
      </service>
    </services>
    <bindings>
      <ws2007HttpBinding>
        <binding name="ws2007HttpBindingConfiguration">
          <security mode="TransportWithMessageCredential">
            <transport clientCredentialType="None"/>
            <message clientCredentialType="UserName" establishSecurityContext="false"/>
          </security>
        </binding>
      </ws2007HttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehavior">
          <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
          <serviceMetadata httpGetEnabled="true"/>
          <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="false"/>
          <serviceCredentials>
            <serviceCertificate findValue="DEV01.corp.contoso.com" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName"/>
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <diagnostics>
      <messageLogging logEntireMessage="true" logMessagesAtServiceLevel="true" logMessagesAtTransportLevel="true" logMalformedMessages="true" maxMessagesToLog="50000" maxSizeOfMessageToLog="20000"/>
    </diagnostics>
  </system.serviceModel>
  <microsoft.identityModel>
    <service name="Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceContract">
      <audienceUris>
        <add value="https://DEV01.corp.contoso.com/AuthUsingCRMClientSite/"/>
      </audienceUris>
      <issuerNameRegistry type="Microsoft.IdentityModel.Tokens.ConfigurationBasedIssuerNameRegistry, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
        <trustedIssuers>
          <add thumbprint="eaa8f8bd2063d55ac083a0907e52b74d8cdb9d07" name="https://DEV01.corp.contoso.com/AuthUsingCRM/Service.svc/IWSTrust13"/>
        </trustedIssuers>
      </issuerNameRegistry>
      <serviceCertificate>
        <certificateReference findValue="DEV01.corp.contoso.com" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectName"/>
      </serviceCertificate>      
    </service>
  </microsoft.identityModel>
</configuration>

  We need to make following changes:
     2.1.   AppSettings:
- Mention token issuer name for this STS.
- Token signing certificate: Create a self-signed certificate in IIS and mention the name here.
- Encrypting Certificate name: You can use separate certificate. Or use the same as signing certificate.
<appSettings>
    <add key="IssuerName" value="ActiveSTSForCRM"/>
    <add key="SigningCertificateName" value="CN=DEV01.corp.contoso.com"/>
    <add key="EncryptingCertificateName" value="CN=DEV01.corp.contoso.com"/>
</appSettings>
2.2.  Add a new config section for microsoft.identityModel:
Define section as added in above sample config.
Mention fully qualified name of the system (in place of Localhost). Replace DEV01.corp.contoso.com accordingly and the certificate name or thumbprint.
2.3.  Audience URI: This is the relying party for this STS. In some later step, I will add a test web client to consume this STS. The URI of this application should be mentioned as audient URI on STS web config.
2.4.  Specify service, binding, and service behavior appropriately. Use your domain name.

3.  Add a claim aware web site to test the custom STS created above:
Select new website:   
With claim aware web site, we will have most of the required assemblies automatically added into solution.

4.  Add logic to actively consume WCF based STS in login.aspx.cs:
WSTrustChannelFactory: We will actively consume the WCF STS end point using WSTurst channel factory class as shown in the below method.
protected void btnSubmit_Click( object sender, EventArgs e )
    {
        // Note: Add code to validate user name, password. This code is for illustrative purpose only.
        // Do not use it in production environment.        
        //FormsAuthentication.RedirectFromLoginPage( txtUserName.Text, false );
        GetToken();
    }

    private static void GetToken()
    {
        WSTrustChannelFactory factory = null;
        try
        {
            factory = new WSTrustChannelFactory(
                            new MIB.UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),                            new EndpointAddress("https://dev01.corp.contoso.com/AuthUsingCRM/Service.svc/IWSTrust13")); //IWSTrust13

            factory.TrustVersion = SSS.TrustVersion.WSTrust13;
            factory.Credentials.SupportInteractive = false;
            factory.Credentials.UserName.UserName = "domain\\username";
            factory.Credentials.UserName.Password = "password";

            factory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode = SSS.X509CertificateValidationMode.None;
            factory.Credentials.ServiceCertificate.Authentication.RevocationMode = X509RevocationMode.NoCheck;

            factory.Credentials.ClientCertificate.Certificate = CertificateUtil.GetCertificate(StoreName.My, StoreLocation.LocalMachine, WebConfigurationManager.AppSettings["SigningCertificateName"]);

            RequestSecurityToken rst = new RequestSecurityToken();
            rst.RequestType = RequestTypes.Issue;

            rst.AppliesTo = new EndpointAddress("https:// dev01.corp.contoso.com /AuthUsingCRMClientSite/");
            rst.KeyType = KeyTypes.Bearer;

            IWSTrustChannelContract channel = factory.CreateChannel();

            SecurityToken secToken = channel.Issue(rst);

        }
        finally
        {
            if (factory.State != CommunicationState.Closing)
            {
                factory.Close();
                factory.Abort();
            }
        }
    }
User Name / Password: Set the window’s user account name and password. By default, for username authentication, WindowsUserNameSecurityTokenHandler is used by STS configuration.

5.  Now Set the client web site as startup project and debug it.
Set a breakpoint at following line in GetToken() method in login.aspx.cs: SecurityToken secToken = channel.Issue(rst);

Once it executes, it will return a valid encrypted secToken. So, great job, our STS is working. Now we can modify it to authenticate against CRM.

6.  Add custom security handler to authenticate against CRM:
    We will add 2 classes to App_Code of STS project:
  CRMService: A wrapper around OrganizationService of CRM / XRM.
public class CRMService
{
    private OrganizationService _Service = null;

    public CRMService()
    {
        var connection = new CrmConnection("CRMConn");
        _Service = new OrganizationService(connection);
    }

    public OrganizationService Service
    {
        get
        {
            return _Service;
        }
        set
        {

            _Service = value;
        }
    }

    public Guid CreateEntity(Entity entity)
    {
        return Service.Create(entity);
    }

    public Entity RetreiveEntity(Guid id, string entityName)
    {
        return Service.Retrieve(entityName, id, new ColumnSet(true));
    }

    public EntityCollection RetreiveMultipleRecords(string fetchXml)
    {
        return Service.RetrieveMultiple(new FetchExpression(fetchXml));
    }

    public void UpdateEntity(Entity entity)
    {
        Service.Update(entity);
    }
}
    For this to work, we need to add following Xrm assemblies to Bin folder:
Microsoft.Xrm.Sdk.dll, microsoft.crm.sdk.proxy.dll, and microsoft.xrm.client.dll  
   
7.  CRMUserNameSecurityTokenHandler: A custom user name security token handler which will override the default WindowsUserNameSecurityTokenHandler from STS configuration. Here we need to override ValidateToken() method and access CRM services using CRMService wrapper to authenticate user.
{
public class CRMUserNameSecurityTokenHandler : Microsoft.IdentityModel.Tokens.UserNameSecurityTokenHandler    
public CRMUserNameSecurityTokenHandler()
    {

    }

    public override bool CanValidateToken
    {
        get
        {
            return true;

        }
    }

    public override Microsoft.IdentityModel.Claims.ClaimsIdentityCollection ValidateToken(SecurityToken token)
    {
        System.Diagnostics.Debugger.Launch();

        if (token == null)
        {
            throw new ArgumentNullException("token");
        }
        UserNameSecurityToken userNameToken = token as UserNameSecurityToken;

        if (userNameToken == null)
        {
            throw new SecurityTokenException("Invalid token");
        }
        IClaimsIdentity identity = new ClaimsIdentity();
        EntityCollection contacts = AuthenticateUser(userNameToken.UserName, userNameToken.Password);
        if (contacts != null && contacts.Entities.First() != null)
        {
            Entity contact = contacts.Entities.First();

            identity.Claims.Add(new Claim(Microsoft.IdentityModel.Claims.ClaimTypes.Authentication, "true"));
            identity.Claims.Add(new Claim(Microsoft.IdentityModel.Claims.ClaimTypes.Upn, userNameToken.UserName));
            string name = contact.Attributes["lastname"] != null ? contact.Attributes["lastname"].ToString() + ", " : "" +
                contact.Attributes["middlename"] != null ? contact.Attributes["middlename"].ToString() + " " : "" +
                contact.Attributes["firstname"] != null ? contact.Attributes["firstname"].ToString() : "";
            identity.Claims.Add(new Claim(Microsoft.IdentityModel.Claims.ClaimTypes.Name, name));
            identity.Claims.Add(new Claim(Microsoft.IdentityModel.Claims.ClaimTypes.Email, contact.Attributes["emailaddress1"] != null ? contact.Attributes["emailaddress1"].ToString() : userNameToken.UserName));
            identity.Claims.Add(new Claim(Microsoft.IdentityModel.Claims.ClaimTypes.PrimarySid, contact.Attributes["contactid"] != null ? contact.Attributes["contactid"].ToString() : "InvalidContactID"));
        }

        return new ClaimsIdentityCollection(new IClaimsIdentity[] { identity });
    }

    private EntityCollection AuthenticateUser(string userName, string password)
    {
        //Authentication against CRM
        CRMService svc = new CRMService();
        var fetchXML = @"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
                                          <entity name='contact'>
                                            <all-attributes/>
                                            <filter type='and'>
                                              <condition attribute='neu_username' operator='eq' value='" + userName + @"' />
                                            </filter>
                                          </entity>
                                        </fetch>";
        EntityCollection contacts = svc.RetreiveMultipleRecords(fetchXML);

        if (contacts.Entities.Count > 0)
        {
            if (password.ToLower() == contacts[0]["new_password_field"].ToString().ToLower())
                return contacts;
        }

        return null;
    }
}
8.  Override default user name security token handler with this custom one:
public class CustomSecurityTokenServiceConfiguration : SecurityTokenServiceConfiguration
{
    /// 
    /// CustomSecurityTokenServiceConfiguration constructor.
    /// 
    public CustomSecurityTokenServiceConfiguration()
        : base( WebConfigurationManager.AppSettings[Common.IssuerName],
                new X509SigningCredentials( CertificateUtil.GetCertificate(
                    StoreName.My, StoreLocation.LocalMachine,
                    WebConfigurationManager.AppSettings[Common.SigningCertificateName] ) ) )
    {
        var removeWinUNHdl = this.SecurityTokenHandlerCollectionManager.SecurityTokenHandlerCollections.ToList()[0].First(x => x is Microsoft.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler);
        this.SecurityTokenHandlerCollectionManager.SecurityTokenHandlerCollections.ToList()[0].Remove(removeWinUNHdl);
        this.SecurityTokenHandlerCollectionManager.SecurityTokenHandlerCollections.ToList()[0].Add(new CRMUserNameSecurityTokenHandler());
        
        this.SecurityTokenService = typeof( CustomSecurityTokenService );
    }
}
This can be done in web.config under Microsoft.IdentityModel section, but it was not working in my case.
9.  Set CRM connection string in web.config with correct User name and password.
<add name="CRMConn" connectionString="Url=http://devcrm01.contoso.com/Dev/; Username=domain\user_name; Password=password;"/>
   10.  Set CRM user name and password in GetToken() method of test client web site and Debug the application. If all these settings are set correctly, custom token handler will be called and it will authenticate against CRM. Finally it will return the claims which we have configured in custom token handler.

Source Code
I have developed this sample application and attached here with this article. The source code contains two websites (STS and test client) which has to be deployed correctly in IIS with all the configuration and certificate needed (as explained in this article). The source code is not perfect in all sense as its primary focus is to demo authentication using CRM.

Source code (Shared on GitHub): https://github.com/manoj-kumar1/auth-against-crm-using-STS

To run this application, you would need Visual Studio 2010.

Conclusion

In this article, I have demonstrated a technique to consume CRM services using custom STS for authenticating end user. This can be setup as Active or Passive federation with ADFS in advance scenarios as also explained in the beginning of this discussion.

Please get in touch with me if you have any questions or you think otherwise @ manoj.kumar[at]neudesic.com.