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.
Comments
Anonymous
May 20, 2007
If you look at the default authorization model for WCF, you will notice that it expects you to implementAnonymous
September 27, 2008
That's exactly the kind of information I was looking for! Thanks! I have a question though: In our project, we implement the custom credential mapping (we use our own username credential validated against our SQL 2005 database), in an external authorization policy. We then hook up the service host with the external authz policy. Do you think there is a problem with that? Could we also just set the authCtx.Properties["Principal"] in the authz policy or is it too late in the WCF pipeline? Also, is there a reason why we can't directly set the System.Threading.Thread.CurrentPrincipal instead of putting it into the authContext 'Principal' property?Anonymous
September 28, 2008
Hi Krishna Thank you for your question. When you write that you are using an external authorization policy, I assume that you've implemented IAuthorizationPolicy? IAuthorizationPolicy implementations are a bit misnamed, since they arent so much used for authorization decisions as to mapping incoming tokens to claims. Then, subsequently, claims can be examined in a ServiceAuthorizationManager to make authorization decisions. This would be the correct place to extract claims and create an IPrincipal instance, since this allows you to decouple the origin of claims from how you use them. It also allows you to aggregate claims from different sources before you make an authorization decision based on the aggregated mass of evidence. Why can't you directly set Thread.CurrentPrincipal instead of this more roundabout way of assigning the 'Principal' property of the AuthorizationContext? Because if you did that, Thread.CurrentPrincipal would only be populated for the request that triggered the ServiceAuthorizationManager evaluation. If you have subsequent calls within a WS-SecureConversation session (as is the case with WS-Federation), only the first call would get the correct IPrincipal. For all the next calls, the original token is substituted with a Security Context Token (SCT), and WCF is so smart that it knows how to map the SCT to the principal you assigned when the original token (in your case a username and password) was presented. HTHAnonymous
August 22, 2010
Can you elaborate on how the saml token is passed to the service? I mean, you must use federation to apply for the token to be sent to the service, right? Is there a way, without using federation, to take a predefined saml token (at the client side) and send it to the service in a way that the service would get all info in it's operation context?Anonymous
August 22, 2010
Yes, this example assumes that you use Federation to get the SAML token to the service. As far as I understand you want to implement a custom token that travels with the message itself? This is possible, but not for the faint of heart: msdn.microsoft.com/.../ms731872.aspx