Mapping SAML Tokens to IPrincipals

In my post about integration testing of WCF services, I briefly touched on the topic of authorization managers and the desirability of separation of concerns. When working with user identity, it's important to place authentication and authorization logic in the right places, but it's not always the case that user identity arrives at the service in the form of a Windows identity. If, for instance, identity is defined in a SAML token, you can use a custom authorization manager to extract user information from the SAML token. However, despite its name, the authorization manager may not be the correct place to perform authorization logic, as individual service operations may have different authorization requirements. Whether or not this is the case, the user identity extracted by the authorization manager may be needed in the implementation code, but as I wrote in an earlier post, if you can avoid making calls to e.g. OperationContext.Current in your operation implementations, the service becomes much less dependent on WCF or specific implementation details.

For this reason, the service implementation should expect user identity to be represented by Thread.CurrentPrincipal. This allows you to unit test the service itself, and will also make your service more configurable, since you can set it up with different authorization managers depending on your needs. The responsibility of any authorization manager then becomes to map from the incoming identity to an IPrincipal instance and place it on Thread.CurrentPrincipal.

In the rest of this post, I will demonstrate how to map from an incoming SAML token to an IPrincipal instance, but you can use the principles to map from other identity formats as well.

The first step is to create a custom authorization manager that can translate the SAML token to an IPrincipal. This is done by deriving from System.ServiceModel.ServiceAuthorizationManager and overriding CheckAccessCore:

 protected override bool CheckAccessCore(OperationContext operationContext)
 {
     if (!base.CheckAccessCore(operationContext))
     {
         return false;
     }
  
     AuthorizationContext authCtx =
         operationContext.ServiceSecurityContext.AuthorizationContext;
     ClaimSet issuerClaimSet =
         MyServiceAuthorizationManager.GetIssuerClaimSet(authCtx);
     if (issuerClaimSet == null)
     {
         return false;
     }
  
     authCtx.Properties["Principal"] =
         MyServiceAuthorizationManager.CreatePrincipal(issuerClaimSet);
     return true;
 }

In this implementation, there are two calls to private member methods. GetIssuerClaimSet just extracts the ClaimSet issued by the expected SecurityTokenService (or rather, issued with the expected X509 certificate):

 private static ClaimSet GetIssuerClaimSet(AuthorizationContext authCtx)
 {
     List<ClaimSet> claimSets = new List<ClaimSet>(authCtx.ClaimSets);
     ClaimSet issuerClaimSet = claimSets.Find(delegate(ClaimSet cs)
     {
         X509CertificateClaimSet certificateClaimSet =
             cs.Issuer as X509CertificateClaimSet;
         return (certificateClaimSet != null) &&
             (certificateClaimSet.X509Certificate.Subject == "CN=MySTS");
     });
     return issuerClaimSet;
 }

The CreatePrincipal method extracts the claims from the ClaimSet and creates a corresponding IPrincipal instance:

 private static object CreatePrincipal(ClaimSet issuerClaimSet)
 {
     List<Claim> claims = new List<Claim>(issuerClaimSet);
  
     Claim nameClaim = claims.Find(delegate(Claim c)
     {
         return (c.ClaimType == ClaimTypes.Name) &&
             (c.Right == Rights.PossessProperty);
     });
     Claim emailClaim = claims.Find(delegate(Claim c)
     {
         return (c.ClaimType == ClaimTypes.Email) &&
             (c.Right == Rights.PossessProperty);
     });
  
     MyIdentity callerIdentity =
         new MyIdentity(nameClaim.Resource.ToString());
     callerIdentity.Email = emailClaim.Resource.ToString();
     return new GenericPrincipal(callerIdentity, new string[] { });
 }

Notice that in this case, I have elected to use a custom IIdentity implementation (called MyIdentity) that also stores the user's email address, since that information is also part of the SAML token. This means that subsequent code can attempt to cast Thread.CurrentPrincipal.Identity to MyIdentity, and if the cast succeeds, extract the email address of the user.

Finally, setting the authorization context's Properties["Principal"] to the newly created IPrincipal instance enables WCF to populate Thread.CurrentPrincipal with this instance, but to do this, the authorization manager must be configured in the service's config file. This is done in the service's behavior configuration:

 <serviceAuthorization serviceAuthorizationManagerType="Service.MyServiceAuthorizationManager, Service"
                       principalPermissionMode="Custom" />

The serviceAuthorizationManagerType attribute specifies the custom authorization manager class. Setting principalPermissionMode to Custom instructs WCF to access the authorization context's Properties["Principal"] instance and assign it to Thread.CurrentPrincipal. This means that all code implementing the service will be able to access the IPrincipal instance assigned by the custom authorization manager. This code can then proceed to use declarative or imperative security decisions using PrincipalPermissionAttribute, PrincipalPermission, etc.

This effectively decouples the service implementation from the user identity implementation, and it's even possible to unit test the service, as long as you place an appropriate IPrincipal on Thread.CurrentPrincipal before invoking the service's methods.