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 applications, 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 through the code to achieve this in a 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.
- Create WCF STS project using Visual Studio 2010:
Go to new website and then select the 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 the following:
Some important classes generated:
CustomSecurityTokenService: WCF service for STS (The custom STS class)
public class CustomSecurityTokenService : SecurityTokenService
{
static bool enableAppliesToValidation = false;
static readonly string[] ActiveClaimsAwareApps =
{ };
public CustomSecurityTokenService( SecurityTokenServiceConfiguration configuration )
: base( configuration )
{
}
void ValidateAppliesTo( EndpointAddress appliesTo )
{
if ( appliesTo == null )
{
throw new ArgumentNullException( "appliesTo" );
}
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 ) );
}
}
}
protected override Scope GetScope( IClaimsPrincipal principal, RequestSecurityToken request )
{
ValidateAppliesTo( request.AppliesTo );
Scope scope = new Scope( request.AppliesTo.Uri.OriginalString,
SecurityTokenServiceConfiguration.SigningCredentials );
string encryptingCertificateName = WebConfigurationManager.AppSettings
[ "EncryptingCertificateName" ];
if ( !string.IsNullOrEmpty( encryptingCertificateName ) )
{
scope.EncryptingCredentials = new X509EncryptingCredentials(
CertificateUtil.GetCertificate
( StoreName.My, StoreLocation.LocalMachine, encryptingCertificateName ) );
}
else
{
scope.TokenEncryptionRequired = false;
}
return scope;
}
protected override IClaimsIdentity GetOutputClaimsIdentity
( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope )
{
if ( null == principal )
{
throw new ArgumentNullException( "principal" );
}
ClaimsIdentity outputIdentity = new ClaimsIdentity();
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.
public class CustomSecurityTokenServiceConfiguration : SecurityTokenServiceConfiguration
{
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 the 2nd step.
- Modify default web.config: It should be modified as follows:
="1.0"="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">
<serviceMetadata httpGetEnabled="true"/>
<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 the 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 the 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.
- Add a claim aware web site to test the custom STS created above:
Select new website:
Use URI like: https://localhost/AuthUsingCRMClientSite
With claim aware web site, we will have most of the required assemblies automatically added into solution.
- 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 )
{
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"));
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.
- Now set the client web site as startup project and debug it.
Set a breakpoint at the 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.
- Add custom security handler to authenticate against CRM:
We will add two 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.
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)
{
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;
}
}
- Override default user name security token handler with this custom one:
public class CustomSecurityTokenServiceConfiguration : SecurityTokenServiceConfiguration
{
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.
- 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;"/>
- 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 it here with this article. The source code contains two websites (STS and test client) which have 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) can be found at 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.