Disclaimer
For the motivation for this article please read this here first. I am not advocating the use of client generated SAML tokens in general, and I also know that there is not much point in the client generating claims for a service. This whole article is about replacing the UserName token with a SAML token in situations where you need extensibility points that a UserName tokens cannot give you.
As I said in my previous post, SAML is an extensible and flexible token type – but not very accessible in plain WCF. Geneva abstracts the creation and consumption of tokens in so called token handlers. All you need is a description of the token that you want to generate and then you feed this description into the appropriate token handler. I wrote an article about the token generation pipeline in Geneva here.
Even better, Geneva has some client side plumbing for WCF that lets you use the generated token for service calls in a quite straightforward fashion.
So the simple scenario is this: the client credential consists of three pieces: username, password and a customer ID (think e.g. of multi-tenant apps). The service receiving this credential could be a normal WCF service or a STS that issues tokens based on the client credential. For future extensibility a general version of this would be a credential with a user name and an unlimited number of properties (e.g. password, customer ID etc.). In a SAML token this would map to the name identifier and a number of attributes.
Client
Using Geneva you can generate a SAML token like this:
public static class ClientToken
{
private const string _claimsUri = http://www.leastprivilege.com/claims/;
public static SamlSecurityToken Create(string subjectName, Dictionary<string, string> properties)
{
ClaimsIdentity id = new ClaimsIdentity(
from item in properties
select new Claim(_claimsUri + item.Key, item.Value));
id.Claims.Add(new Claim(WSIdentityConstants.ClaimTypes.NameIdentifier, subjectName));
var description = new SecurityTokenDescriptor
{
Subject = id,
TokenIssuerName = “http://self”
};
var handler = new Saml11SecurityTokenHandler(new SamlSecurityTokenRequirement());
return (SamlSecurityToken)handler.CreateToken(description);
}
}
After creating the token you can use the Geneva extension methods for ChannelFactory<T> to set the token as a client credential:
static void Main(string[] args)
{
var props = new Dictionary<string, string>
{
{ “password”, “secret” },
{ “customerId”, “42” }
};
var token = ClientToken.Create(“dominick”, props);
var factory = new ChannelFactory<IServiceClientChannel>(“*”);
FederatedClientCredentials.ConfigureChannelFactory<IServiceClientChannel>(factory);
var proxy = factory.CreateChannelWithIssuedToken<IServiceClientChannel>(token);
proxy.Operation(“foo”);
proxy.Close();
}
Service
On the service side you need a corresponding token handler that “understands” the semantics of the SAML token. Geneva has a built-in handler for SAML tokens but it does not know how to authenticate the client based on the values of certain SAML attributes. Furthermore you need to make a decision which of the incoming attributes should become part of the claims identity in the service. Maybe you don’t want sensitive information like the password to flow to the service operations (maybe you want exactly that). Other customizations to the standard behavior would be to ignore audience URIs as well as signatures (since our client tokens won’t have them).
By deriving from the built-in token handler you can drive this logic while letting the base class do all the heavy lifting of token serialization and parsing:
public abstract class ClientSaml11SecurityTokenHandlerBase : Saml11SecurityTokenHandler
{
// disable audience URI checking
public ClientSaml11SecurityTokenHandlerBase()
: base(new SamlSecurityTokenRequirement { AudienceUriMode = AudienceUriMode.Never })
{ }
// extensibility point for authentication and claims filtering
protected abstract bool ValidateUser(
string subjectName, Dictionary<string, string> properties, ref List<string> claimsList);
// override signature handling
public override SecurityToken ReadToken(XmlReader reader)
{
Saml11Assertion assertion = this.ReadAssertion(reader);
return new SamlSecurityToken(assertion);
}
public override ClaimsIdentityCollection ValidateToken(SecurityToken token)
{
// call base class for token validation and serialization
var ids = base.ValidateToken(token);
var id = ids[0];
// retrieve client name
string subjectName = id.Claims.Where(
claim => claim.ClaimType == WSIdentityConstants.ClaimTypes.NameIdentifier).First().Value;
// copy attributes to dictionary
var properties = new Dictionary<string, string>();
id.Claims.ToList().ForEach(
claim => properties.Add(claim.ClaimType, claim.Value));
// call authentication and filtering logic
var claimsToKeep = new List<string>();
if (ValidateUser(subjectName, properties, ref claimsToKeep))
{
ClaimsIdentity identity = new ClaimsIdentity(“ClientSAML”);
// add client name
identity.Claims.Add(new Claim(
WSIdentityConstants.ClaimTypes.Name, subjectName, ClaimValueTypes.String, “LOCAL”));
// copy “allowed” attributes
claimsToKeep.ForEach(claimType =>
{
string value = id.Claims.Where(claim => claim.ClaimType == claimType).FirstOrDefault().Value;
if (!string.IsNullOrEmpty(value))
{
identity.Claims.Add(new Claim(claimType, value, ClaimValueTypes.String, “LOCAL”));
}
});
return new ClaimsIdentityCollection(identity);
}
else
{
throw new SecurityTokenValidationException();
}
}
}
The authentication logic and filtering could be implemented like this:
class ClientSaml11SecurityTokenHandler : ClientSaml11SecurityTokenHandlerBase
{
protected override bool ValidateUser(
string subjectName, Dictionary<string, string> properties, ref List<string> claimsList)
{
string password = properties[_passwordClaimType];
// sample password check – don’t just copy&paste this code ;)
if (subjectName != password)
{
return false;
}
claimsList.Add(_customerIdClaimType);
return true;
}
}
After wiring up the new token handler in the WCF service you get access to the claims in the operation via IClaimsPrincipal as usual.
Configuration
The last step deals with setting up the security parameters on the binding. Since the client SAML token is much like a UserName token on steroids, we choose similar security configurations (and make the same security guarantees). You can either use message security where the client token gets encrypted with the service certificate – or mixed mode security where the transport is secured using SSL.
For mixed mode security you can use the IssuedTokenOverTransport authentication mode. The custom binding looks like this:
<customBinding>
<binding name=“BearerTokenOverTransport“>
<security authenticationMode=“IssuedTokenOverTransport“>
<issuedTokenParameters
tokenType=”http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1″
keyType=“BearerKey“ />
</security>
<textMessageEncoding />
<httpsTransport />
</binding>
</customBinding>
For message security, there is no built-in binding element helper – you have to construct it via code:
public static SecurityBindingElement CreateClientTokenForCertificateBindingElement()
{
// protection token
var element = new SymmetricSecurityBindingElement(
new X509SecurityTokenParameters(
X509KeyIdentifierClauseType.Thumbprint,
SecurityTokenInclusionMode.Never));
// client token
var parameters = new IssuedSecurityTokenParameters(
Saml11SecurityTokenHandler.OasisWssSamlTokenProfile11,
new EndpointAddress(http://self),
new BasicHttpBinding());
parameters.KeyType = SecurityKeyType.BearerKey;
parameters.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;
element.EndpointSupportingTokenParameters.SignedEncrypted.Add(parameters);
element.MessageSecurityVersion =
MessageSecurityVersion.WSSecurity11WSTrust13
WSSecureConversation13
WSSecurityPolicy12BasicSecurityProfile10;
return element;
}
You can then use this helper to construct a custom binding (see the download for the complete code).
That’s it. HTH
ClientSamlCredential.zip (450.94 KB)
Hi Dominick,
I am new to ThinkTecture and i am trying to validate incoming SAML2.0 Token using ThinkkTecture. How to validate Thumbprint ?