This post continues where the
last one left off. We now have a SAML token that is re-serializable, which is good, because that will make it possible to use the same SAML token when calling a second business service from the first one. We could also easily check if we have a SAML token available by casting the current principal object to an ITokenPrincipal (
see this post).
Now, what's left to do is to create a custom behavior that grabs the SAML token form the principal object, checks that the token is still valid, checks that the token are valid for the service that are going to be called, and in that case attaches that SAML token to the outgoing call. To be able to interact with WCF's client credentials the first step is to create a custom ClientCredentials class, so I created a TokenPrincipalClientCredentials class. The only purpose for this class is to be able to override the CreateSecurityTokenManager method so that we could return a custom ClientCredentialsSecurityTokenManager. The custom ClientCredentialsSecurityTokenManager only purpose is to override the CreateSecurityTokenProvider method, so that it is possible to provide a custom SecurityTokenProvider that checks the principal object for a valid SAML token. This means that we need to create three classes, where the first two only purpose is to let us inject the third one, which will do all the work. For the sake of completeness, here is the first two without some boilerplate code.
public class TokenPrincipalClientCredentials : ClientCredentials
{
…
public override SecurityTokenManager CreateSecurityTokenManager()
{
return new TokenPrincipalClientCredentialsSecurityTokenManager(this);
}
}
internal class TokenPrincipalClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager
{
…
public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement)
{
if (this.IsIssuedSecurityTokenRequirement(tokenRequirement))
{
IssuedSecurityTokenProvider baseProvider = (IssuedSecurityTokenProvider)base.CreateSecurityTokenProvider(tokenRequirement);
TokenPrincipalSecurityTokenProvider provider = new TokenPrincipalSecurityTokenProvider(baseProvider);
return provider;
}
else
{
return base.CreateSecurityTokenProvider(tokenRequirement);
}
}
…
}
The reason that I first get the standard SecurityTokenProvider is that it would make it easier to implement the custom TokenPrincipalSecurityTokenProvider, so I create that class with the standard SecurityTokenProvider. Finally we can take a look at the class where the real action is (again stripped from some boilerplate code).
internal class TokenPrincipalSecurityTokenProvider : IssuedSecurityTokenProvider, IDisposable
{
…
protected override SecurityToken GetTokenCore(TimeSpan timeout)
{
ITokenPrincipal principal = System.Threading.Thread.CurrentPrincipal as ITokenPrincipal;
SecurityToken securityToken = null;
if (principal != null && !principal.Expired)
{
securityToken = principal.Token;
//Check that the token is valid for the endpoint
if (!ValidateAudienceRestrictions(securityToken as SamlSecurityToken))
securityToken = innerProvider.GetToken(timeout);
}
else
securityToken = innerProvider.GetToken(timeout);
return securityToken;
}
private bool ValidateAudienceRestrictions(SamlSecurityToken token)
{
if (token == null) return true;
foreach (SamlCondition samlCondition in token.Assertion.Conditions.Conditions)
{
SamlAudienceRestrictionCondition audienceRestriction = samlCondition as SamlAudienceRestrictionCondition;
if (audienceRestriction != null)
{
bool uriFound = false;
string endpointUri = base.TargetAddress.Uri.ToString();
foreach (Uri uri in audienceRestriction.Audiences)
{
string uriString = uri.ToString();
if (endpointUri.StartsWith(uriString, StringComparison.OrdinalIgnoreCase))
{
uriFound = true;
break;
}
}
if (!uriFound)
{
System.Diagnostics.Trace.TraceWarning(Resources.Resource.SamlTokenNotValidForEndpoint, endpointUri);
}
return uriFound;
}
}
//No SamlAudienceRestrictionCondition so it's ok to use the SamlToken
return true;
}
…
}
In the GetTokenCore method I check the current principal object for the SAML token, and If it's found, I try to validate it for the endpoint. To be able to determine if the SAML token should be used for the service that is being called I check the SAML conditions if there is a SamlAudienceRestrectionCondition and in that case if the Uri of the condition matches the Url that is being called.
The fact that the SamlAudienceRestrectionCondition is there is one of the features that I have added to my STS. It uses the same Uri that is configured in the config-file for the service token.
SamlConditions conditions = new SamlConditions(validFrom, validTo);
Configuration.ServiceTokenConfiguration serviceToken = SecurityTokenService.GetServiceToken(rst.AppliesTo);
if (serviceToken != null)
conditions.Conditions.Add(new SamlAudienceRestrictionCondition(
new List<Uri> { serviceToken.Uri }
));
Note how I instantiate the list with the new short syntax provided in Visual Studio 2008, so that I don't need to declare a new List and then add elements. This could of course be misused, but in cases like this when I need a list, containing only one item, I find it very handy.
To make this custom security token provider come in and play with WCF, all we have to do is to config a custom behavior for our endpoint on the client side.
<endpointBehaviors>
<behavior name="xxx">
<clientCredentials type="IRM.ServiceModel.Description.TokenPrincipalClientCredentials, IRM.ServiceModel.v1.0, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
</clientCredentials>
</behavior>
</endpointBehaviors>