Custom WCF service authentication using Microsoft Dynamics CRM credentials

Enterprise Microsoft Dynamics CRM implementations frequently require developing custom Windows Communication Foundation (WCF) web services to be used for integration with external systems. Typical use cases for custom web services would include situations in which a consuming system can't easily access the Dynamics CRM Organization Service, or a custom service is required to do some data processing above and beyond the capabilities of the CRM Organization Service. Because these custom web services are separate from the actual Dynamics CRM system, user authentication and authorization has to be handled via a separate mechanism.

In this post I will show how you can use a custom WCF username and password validator to authenticate consumers using their Dynamics CRM credentials. In addition to providing an easy-to-use authentication mechanism, this approach will give you the ability to authorize consumers’ access to web services based on their assigned Dynamics CRM roles.
WCF Custom Dynamics CRM Username and Password Validator

There are three steps required to set up custom username and password validation for a WCF service:

  1. Create the custom validator class
  2. Update the service binding's security configuration
  3. Update the service behavior to use the custom validator

Let's look at all three in more detail.

The custom validator

The custom validator is a class that inherits from the System.IdentityModel.Selectors.UserNamePasswordValidator class. That class contains a Validate method that we will need to override. The Validate method accepts two parameters, username and password, and if the validation fails, it throws a SecurityTokenException. If validation is successful, the method exits without returning anything.

In our custom validator class, we need to do the following:

  1. Create a connection to the Dynamics CRM Organization Service via the CRM SDK
  2. Execute a WhoAmIRequest using the provided user credentials
  3. If successful, retrieve the CRM user roles and store the CRM systemuserid and roles in a GenericPrincipal object
  4. If unsuccessful, throw an error

Here is the overridden Validate method:

///<summary>
/// Validate method to attempt to connect to CRM with supplied username/password and then execute a whoami request
/// </summary>
/// <param name="username">crm username</param>
/// <param name="password">crm password</param>
public override void Validate(string username, string password)
{
    //get the httpcontext so we can store the user guid for impersonation later
    HttpContext context = HttpContext.Current;
    //if username or password are null, obvs we can't continue
    if (null == username || null == password)
    {
        throw new ArgumentNullException();
    }
    //get the crm connection using the simplified connection string method
    // the following assumes the server url is stored in the web.config appsettings collection like so:
    // <add key="crmconnectionstring" value="Url=https://crm.example.com"/>
    string connectionString = ConfigurationManager.AppSettings["crmconnectionstring"];
    connectionString += " Username=" + username + "; Password=" + password;
    Microsoft.Xrm.Client.CrmConnection connection = CrmConnection.Parse(connectionString);
    //try the whoami request
    //if it fails (user can't be authenticated, is disabled, etc.), the client will get a soap fault message
    using (OrganizationService service = new OrganizationService(connection))
    {
        try
        {
            WhoAmIRequest req = new WhoAmIRequest();
            WhoAmIResponse resp = (WhoAmIResponse)service.Execute(req);
            List<string> roles = GetUserRoles(resp.UserId, service);
            context.User = new GenericPrincipal(new GenericIdentity(resp.UserId.ToString()), roles.ToArray());
        }
        catch (System.ServiceModel.Security.MessageSecurityException ex)
        {
            throw new FaultException(ex.Message); 
        }
        catch (Exception ex)
        {
            throw new FaultException(ex.Message);
        }
    }
}

And here is the GetUserRoles method called in the Validate method:

/// <summary>
/// retrieves a list of CRM roles assigned to a specific user
/// </summary>
/// <param name="userid"></param>
/// <param name="service"></param>
/// <returns></returns>
private List<string> GetUserRoles(Guid userid, OrganizationService service)
{
    List<string> roles = new List<string>();
    string fetchXml = @"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='true'>
     <entity name='role'>
        <attribute name='name' />
        <attribute name='businessunitid' />
        <attribute name='roleid' />
        <order attribute='name' descending='false' />
        <link-entity name='systemuserroles' from='roleid' to='roleid' visible='false' intersect='true'>
         <link-entity name='systemuser' from='systemuserid' to='systemuserid' alias='af'>
            <filter type='and'>
             <condition attribute='systemuserid' operator='eq' uitype='systemuser' value='{$USERID}' />
            </filter>
         </link-entity>
        </link-entity>
     </entity>
    </fetch>";
    fetchXml = fetchXml.Replace("$USERID", userid.ToString());
    EntityCollection results = service.RetrieveMultiple(new FetchExpression(fetchXml));
    foreach (Entity entity in results.Entities)
    {
        roles.Add((string)entity["name"]);
    }
    return roles;
}

You can download the complete CrmUsernamePasswordValidator.cs class here.

The service binding configuration

A custom validator can work with WCF Transport, Message or TransportWithMessageCredential security. Inside the security element of your service binding in your web.config file, you must specify a clientCredentialType value of "UserName."

Here's an example that uses a basicHttpBinding:

<basicHttpBinding>
    <binding name="BasicHttpEndpointBinding">
        <security mode="TransportWithMessageCredential">
            <message clientCredentialType="UserName"/>
        </security>
    </binding>
</basicHttpBinding>

For more information on WCF service bindings and security modes, please see this overview from the Microsoft Patterns and Practices guide "Improving Web Services Security: Scenarios and Implementation Guidance for WCF."

The service behavior configuration

The final step is to add a behaviors element to your web.config file that tells WCF to use the custom validator. Here is an example:

<behaviors>
    <serviceBehaviors>
        <behavior name="Behavior1" >
            <serviceCredentials>
                <userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="CustomValidators.CrmUsernamePasswordValidator, CustomValidators" />
            </serviceCredentials>
        </behavior>
    </serviceBehaviors>
</behaviors>

Note that "Behavior1" in line 3 should match the behaviorConfiguration attribute where you define the service. The customUserNamePasswordValidatorType is the name of your custom validator class and its assembly.

Wrapping it up

Once you're finished with these updates, your WCF service will now require the username and password to be supplied with inbound requests, and if the user can't be authenticated, a SOAP fault message will be returned. If you want to use the user's assigned CRM roles in your web service's business logic, you can check role membership using the HttpContext.Current.User.IsInRole method. You can also impersonate the user in subsequent calls to the Dynamics CRM Organization Service using the systemuserid value stored in the HttpContext.Current.User.Identity.Name field.

A version of this post was originally published on the HP Enterprise Services Application Services blog.

comments powered by Disqus