Monday, 21 November 2011

Custom WCF security token

This has been hanging around in my drafts for a while. I have just been looking at Secure Token Services and decided to dust this off before posting any of my findings with STS’s.

A while back I was involved in a smart client application for a customer that used a mixture of WCF and Microsoft Synchronization Framework for downloading offline data from our system. One of the interesting aspects of the projects was how authentication was done. The authentication was done via our clients SSO website which had to be done via screen scraping as there was no service available, all transports on our end had to be HTTP. After successful authentication a signed XML SAML token was returned to the users web browser as a base64 string. Because the SAML token given to us from the customers services did not do everything the Microsoft way, we could not turn it into a SamlSecurityToken. Initially, we were sending the encoded string as a parameter on each of the WCF method calls and then unpacked the token at the other end and validating it before allowing the user to do their action.

Whilst this polluted our interfaces it worked fine on most of our services, but it meant that our interface for the Microsoft Synchronization services was not protected. In order to add protection we needed to leverage some of WCF’s goodness with securing the message. Ideally, I would have liked to put a Secure Token Service in place which would have validated the SAML token from the customers SSO and returned a token that could be embedded in the messages, but unfortunately that option was not available so we went down the road of creating a custom security token with the SAML token embedded in it. This example shows how the base64 string was embedded but it does not have to be a base64 string, it can be any serializable object.

In order to achieve this you need three sets of components, a client side SecurityTokenProvider and related components, a common SecurityToken and related serialization components and a ServiceCredentialsSecurityTokenManager with a custom IAuthorizationPolicy and SecurityTokenAuthenticator. I have broken down the components into common, client and service which I put into separate assemblies but there is no reason that they cannot all be in the same assembly.

Common Components

These are all the components used on both the client and service used for reading/writing, serializing/de-serializing and transporting the  security details.

Security Token

The SecurityToken forms the core of the system, this is the object that will get passed between the client and the service. Our token revolves around our base64 SAML string which is passed into the constructor and stored as a public property. One of the things that we did was to pull out the valid from and to date time stamps from the XML and set these to the security tokens valid from and to properties. We could have just have created our own arbitrary time range but this felt more complete.

   1: public class Base64SamlToken : SecurityToken
   2: {
   3:     private readonly string id;
   4:     private readonly ReadOnlyCollection<SecurityKey> securityKeys = new ReadOnlyCollection<SecurityKey>(new List<SecurityKey>());
   5:     private DateTime validFrom;
   6:     private DateTime validTo;
   7:     
   8:     public Base64SamlToken(string base64SamlTokenString, string id)
   9:     {
  10:         Base64SamlTokenString = base64SamlTokenString;
  11:      
  12:         this.id = id;
  13:      
  14:         ExtractValidationTimesFromSamlToken(base64SamlTokenString);
  15:     }
  16:      
  17:     private void ExtractValidationTimesFromSamlToken(string base64SamlTokenString)
  18:     {
  19:         using (var base64SamlTokenStream = new MemoryStream(Convert.FromBase64String(base64SamlTokenString)))
  20:         using (XmlReader xmlReader = XmlReader.Create(base64SamlTokenStream))
  21:         {
  22:             while (xmlReader.Read())
  23:             {
  24:                 if (xmlReader.IsStartElement("Conditions", "urn:oasis:names:tc:SAML:1.0:assertion"))
  25:                 {
  26:                     validFrom = DateTime.Parse(xmlReader.GetAttribute("NotBefore"));
  27:                     validTo = DateTime.Parse(xmlReader.GetAttribute("NotOnOrAfter"));
  28:      
  29:                     break;
  30:                 }
  31:             }
  32:         }
  33:     }
  34:      
  35:     public string Base64SamlTokenString { get; private set; }
  36:      
  37:     public override string Id { get { return id; } }
  38:      
  39:     public override ReadOnlyCollection<SecurityKey> SecurityKeys { get { return securityKeys; } }
  40:      
  41:     public override DateTime ValidFrom { get { return validFrom; } }
  42:      
  43:     public override DateTime ValidTo { get { return validTo; } }
  44: }

Security Token Parameters

The SecurityTokenParameters  provides the information about the Base64SamlToken that the WCF BindingElementExtensionElement will use, such as, the required type being a Base64SamlToken. It also sets up what the Token supports.

   1: public class Base64SamlTokenParameters : SecurityTokenParameters
   2: {
   3:     protected override bool HasAsymmetricKey { get { return false; } }
   4:     protected override bool SupportsClientAuthentication { get { return true; } }
   5:     protected override bool SupportsClientWindowsIdentity { get { return false; } }
   6:     protected override bool SupportsServerAuthentication { get { return false; } }
   7:     
   8:     protected override SecurityTokenParameters CloneCore()
   9:     {
  10:         return new Base64SamlTokenParameters();
  11:     }
  12:  
  13:     protected override void InitializeSecurityTokenRequirement(SecurityTokenRequirement requirement)
  14:     {
  15:         requirement.TokenType = Base64SamlTokenConstants.Base64SamlTokenType;
  16:         return;
  17:     }
  18:  
  19:     protected override SecurityKeyIdentifierClause CreateKeyIdentifierClause(SecurityToken token, SecurityTokenReferenceStyle referenceStyle)
  20:     {
  21:         if (referenceStyle == SecurityTokenReferenceStyle.Internal)
  22:         {
  23:             return token.CreateKeyIdentifierClause<LocalIdKeyIdentifierClause>();
  24:         }
  25:  
  26:         throw new NotSupportedException("External references are not supported for Base 64 Saml Tokens");
  27:     }
  28: }

Binding Element Extension Element

The BindingElementExtensionElement is not required if you do not want to wire anything up in the applications config file. It provides the wire up for the service or the client in order for it to send or receive the security token.

   1: public class Base64SamlTokenBindingElement : BindingElementExtensionElement
   2: {
   3:     private const string CONST_ELEMENT_LOCALCLIENTSETTINGS = "localClientSettings";
   4:     private const string CONST_ELEMENT_LOCALSERVICESETTINGS = "localServiceSettings";
   5:      
   6:     protected override BindingElement CreateBindingElement()
   7:     {
   8:         var x509ProtectionParameters = new X509SecurityTokenParameters
   9:         {
  10:             InclusionMode = SecurityTokenInclusionMode.Never
  11:         };
  12:      
  13:         var innerBindingElement = new SymmetricSecurityBindingElement();
  14:      
  15:         innerBindingElement.EndpointSupportingTokenParameters.SignedEncrypted.Add(new Base64SamlTokenParameters());
  16:      
  17:         innerBindingElement.ProtectionTokenParameters = x509ProtectionParameters;
  18:      
  19:         ApplyConfiguration(innerBindingElement);
  20:      
  21:         return innerBindingElement;
  22:     }
  23:      
  24:     public override void ApplyConfiguration(BindingElement bindingElement)
  25:     {
  26:         base.ApplyConfiguration(bindingElement);
  27:         var element = (SecurityBindingElement)bindingElement;
  28:          
  29:         if (ElementInformation.Properties[CONST_ELEMENT_LOCALCLIENTSETTINGS].ValueOrigin != PropertyValueOrigin.Default)
  30:         {
  31:             ApplyConfiguration(element.LocalClientSettings);
  32:         }
  33:         if (ElementInformation.Properties[CONST_ELEMENT_LOCALSERVICESETTINGS].ValueOrigin != PropertyValueOrigin.Default)
  34:         {
  35:             ApplyConfiguration(element.LocalServiceSettings);
  36:         }
  37:     }
  38:      
  39:     private void ApplyConfiguration(LocalServiceSecuritySettings settings)
  40:     {
  41:         settings.DetectReplays = LocalServiceSettings.DetectReplays;
  42:         settings.IssuedCookieLifetime = LocalServiceSettings.IssuedCookieLifetime;
  43:         settings.MaxClockSkew = LocalServiceSettings.MaxClockSkew;
  44:         settings.MaxPendingSessions = LocalServiceSettings.MaxPendingSessions;
  45:         settings.MaxStatefulNegotiations = LocalServiceSettings.MaxStatefulNegotiations;
  46:         settings.NegotiationTimeout = LocalServiceSettings.NegotiationTimeout;
  47:         settings.ReconnectTransportOnFailure = LocalServiceSettings.ReconnectTransportOnFailure;
  48:         settings.ReplayCacheSize = LocalServiceSettings.ReplayCacheSize;
  49:         settings.ReplayWindow = LocalServiceSettings.ReplayWindow;
  50:         settings.SessionKeyRenewalInterval = LocalServiceSettings.SessionKeyRenewalInterval;
  51:         settings.SessionKeyRolloverInterval = LocalServiceSettings.SessionKeyRolloverInterval;
  52:         settings.InactivityTimeout = LocalServiceSettings.InactivityTimeout;
  53:         settings.TimestampValidityDuration = LocalServiceSettings.TimestampValidityDuration;
  54:         settings.MaxCachedCookies = LocalServiceSettings.MaxCachedCookies;
  55:     }
  56:      
  57:     private void ApplyConfiguration(LocalClientSecuritySettings settings)
  58:     {
  59:         settings.CacheCookies = LocalClientSettings.CacheCookies;
  60:         settings.DetectReplays = LocalClientSettings.DetectReplays;
  61:         settings.MaxClockSkew = LocalClientSettings.MaxClockSkew;
  62:         settings.MaxCookieCachingTime = LocalClientSettings.MaxCookieCachingTime;
  63:         settings.ReconnectTransportOnFailure = LocalClientSettings.ReconnectTransportOnFailure;
  64:         settings.ReplayCacheSize = LocalClientSettings.ReplayCacheSize;
  65:         settings.ReplayWindow = LocalClientSettings.ReplayWindow;
  66:         settings.SessionKeyRenewalInterval = LocalClientSettings.SessionKeyRenewalInterval;
  67:         settings.SessionKeyRolloverInterval = LocalClientSettings.SessionKeyRolloverInterval;
  68:         settings.TimestampValidityDuration = LocalClientSettings.TimestampValidityDuration;
  69:         settings.CookieRenewalThresholdPercentage = LocalClientSettings.CookieRenewalThresholdPercentage;
  70:     }
  71:      
  72:     [ConfigurationProperty(CONST_ELEMENT_LOCALCLIENTSETTINGS)]
  73:     public LocalClientSecuritySettingsElement LocalClientSettings
  74:     {
  75:         get
  76:         {
  77:             return (LocalClientSecuritySettingsElement)this[CONST_ELEMENT_LOCALCLIENTSETTINGS];
  78:         }
  79:     }
  80:      
  81:     [ConfigurationProperty(CONST_ELEMENT_LOCALSERVICESETTINGS)]
  82:     public LocalServiceSecuritySettingsElement LocalServiceSettings
  83:     {
  84:         get
  85:         {
  86:             return (LocalServiceSecuritySettingsElement)this["localServiceSettings"];
  87:         }
  88:     }
  89:      
  90:     public override Type BindingElementType
  91:     {
  92:         get { return typeof(SymmetricSecurityBindingElement); }
  93:     }
  94: }

Security Token Serializer

In order to serialize the token between the client and the service we need a custom SecurityTokenSerializer, in our scenario we used a WSSecurityTokenSerializer because we were going over Http and were going to use a certificate to secure the message.

   1: public class Base64SamlTokenSecurityTokenSerializer : WSSecurityTokenSerializer
   2: {
   3:     protected override bool CanReadTokenCore(XmlReader reader)
   4:     {
   5:         XmlDictionaryReader localReader = XmlDictionaryReader.CreateDictionaryReader(reader);
   6:      
   7:         if (reader.IsStartElement(Base64SamlTokenConstants.Base64SamlTokenName, Base64SamlTokenConstants.Base64SamlTokenNamespace))
   8:         {
   9:             return true;
  10:         }
  11:      
  12:         return base.CanReadTokenCore(reader);
  13:     }
  14:      
  15:     protected override SecurityToken ReadTokenCore(XmlReader reader, SecurityTokenResolver tokenResolver)
  16:     {
  17:         if (reader.IsStartElement(Base64SamlTokenConstants.Base64SamlTokenName, Base64SamlTokenConstants.Base64SamlTokenNamespace))
  18:         {
  19:             string id = reader.GetAttribute(CommonConstants.Attributes.Id, CommonConstants.Namespaces.WsUtility);
  20:      
  21:             reader.ReadStartElement();
  22:              
  23:             string base64SamlToken = reader.ReadElementString(Base64SamlTokenConstants.Base64SamlTokenElementName, Base64SamlTokenConstants.Base64SamlTokenNamespace);
  24:              
  25:             reader.ReadEndElement();
  26:              
  27:             return new Base64SamlToken(base64SamlToken, id);
  28:         }
  29:              
  30:         return DefaultInstance.ReadToken(reader, tokenResolver);
  31:     }
  32:      
  33:     protected override bool CanWriteTokenCore(SecurityToken token)
  34:     {
  35:         if (token is Base64SamlToken)
  36:         {
  37:             return true;
  38:         }
  39:      
  40:         return base.CanWriteTokenCore(token);
  41:     }
  42:      
  43:     protected override void WriteTokenCore(XmlWriter writer, SecurityToken token)
  44:     {
  45:         if (token is Base64SamlToken)
  46:         {
  47:             var base64SamlToken = (Base64SamlToken) token;
  48:      
  49:             writer.WriteStartElement("cc", Base64SamlTokenConstants.Base64SamlTokenName, Base64SamlTokenConstants.Base64SamlTokenNamespace);
  50:             writer.WriteAttributeString("wsu", CommonConstants.Attributes.Id, CommonConstants.Namespaces.WsUtility, token.Id);
  51:             writer.WriteElementString(Base64SamlTokenConstants.Base64SamlTokenElementName, Base64SamlTokenConstants.Base64SamlTokenNamespace, base64SamlToken.Base64SamlTokenString);
  52:             writer.WriteEndElement();
  53:             writer.Flush();
  54:         }
  55:         else
  56:         {
  57:             base.WriteTokenCore(writer, token);
  58:         }
  59:     }
  60: }

Constants

I found that strings are used all over the place in WCF, especially for namespaces, so I pushed all the strings into constant values held in central classes so that they could be reused in order to prevent mistakes as the strings had to match exactly on both the client and service side.

   1: public class Base64SamlTokenConstants
   2: {
   3:     public const string Base64SamlTokenType = "http://foo/ServiceModel/Tokens/Base64SamlToken";
   4:     public const string Base64SamlTokenNamespace = "http://foo/ServiceModel/Tokens/Base64SamlToken";
   5:     public const string Base64SamlTokenName = "Base64SamlToken";
   6:     public const string Base64SamlTokenElementName = "Base64SamlToken";
   7: }
   8:  
   9: public static class CommonConstants
  10: {
  11:     public static class Namespaces
  12:     {
  13:         public static string WsUtility = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
  14:     }
  15:  
  16:     public static class Attributes
  17:     {
  18:         public static string Id = "Id";
  19:     }
  20: }

Client side components

These are the components which sole purpose is to get the security token and provide it to WCF for bundling into the message.

Security Token Provider

The SecurityTokenProvider is used by the client to generate the required security token. In our scenario it takes in our bas64 SAML token and creates a new Base64SamlToken from the string.

   1: public class Base64SamlTokenProvider : SecurityTokenProvider
   2: {
   3:     private readonly string base64SamlTokenString;
   4:      
   5:     public Base64SamlTokenProvider(string base64SamlTokenString)
   6:     {
   7:         this.base64SamlTokenString = base64SamlTokenString;
   8:     }
   9:      
  10:     protected override SecurityToken GetTokenCore(TimeSpan timeout)
  11:     {
  12:         return new Base64SamlToken(base64SamlTokenString, Guid.NewGuid().ToString());
  13:     }
  14: }

Client Credentials

The ClientCredentials are added to the channel’s behaviours to tell WCF how it should authenticate on the channel, it does this by returning the SecurityTokenManager it wants to use which in our case will be an implementation of a ClientCredentialsSecurityTokenManager.

   1: public class Base64SamlTokenClientCredentials : ClientCredentials
   2: {
   3:     public Base64SamlTokenClientCredentials(string base64SamlToken)
   4:     {
   5:         Base64SamlToken = base64SamlToken;
   6:     }
   7:      
   8:     public string Base64SamlToken { get; set; }
   9:      
  10:     protected override ClientCredentials CloneCore()
  11:     {
  12:         return new Base64SamlTokenClientCredentials(Base64SamlToken);
  13:     }
  14:      
  15:     public override SecurityTokenManager CreateSecurityTokenManager()
  16:     {
  17:         return new Base64SamlTokenClientCredentialsSecurityTokenManager(this);
  18:     }
  19: }

Client Credentials Security Token Manager

The ClientCredentialsSecurityTokenManager handles the token serialization and authentication for the client, it will return our Bas64SamlTokenSerializer and ensure the Base64SamlTokenProvider is used with the base64 SAML token string passed in as the client credentials.

   1: public class Base64SamlTokenClientCredentialsSecurityTokenManager : ClientCredentialsSecurityTokenManager
   2: {
   3:     private readonly Base64SamlTokenClientCredentials clientCredentials;
   4:      
   5:     public Base64SamlTokenClientCredentialsSecurityTokenManager(Base64SamlTokenClientCredentials clientCredentials) : base(clientCredentials)
   6:     {
   7:         this.clientCredentials = clientCredentials;
   8:     }
   9:      
  10:     public override SecurityTokenProvider CreateSecurityTokenProvider(SecurityTokenRequirement tokenRequirement)
  11:     {
  12:         if (tokenRequirement.TokenType == Base64SamlTokenConstants.Base64SamlTokenType)
  13:         {
  14:             return new Base64SamlTokenProvider(clientCredentials.Base64SamlToken);
  15:         }
  16:          
  17:         if (tokenRequirement is InitiatorServiceModelSecurityTokenRequirement)
  18:         {
  19:             if (tokenRequirement.TokenType == SecurityTokenTypes.X509Certificate)
  20:             {
  21:                 return new X509SecurityTokenProvider(clientCredentials.ServiceCertificate.DefaultCertificate);
  22:             }
  23:         }
  24:  
  25:         return base.CreateSecurityTokenProvider(tokenRequirement);
  26:     }
  27:  
  28:     public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version)
  29:     {
  30:         return new Base64SamlTokenSecurityTokenSerializer();
  31:     }
  32: }
  33:  

Client Credentials Element

As before, we don’t have to use XML configuration but in order to allow wire up in the configuration you need a ClientCredentialsElement . It is responsible for setting up the client credentials the channel is going to use, so here we use our Base64SamlTokenClientCredentials.

   1: public class Base64SamlTokenClientCredentialsElement : ClientCredentialsElement
   2: {
   3:     protected override object CreateBehavior()
   4:     {
   5:         var credentials = new Base64SamlTokenClientCredentials(string.Empty);
   6:         ApplyConfiguration(credentials);
   7:         return credentials;
   8:     }
   9: }

Service Side Components

These are the components which sole purpose is to get the security token from the message on the service side and expose it for validation.

Service Credentials Security Manager

As with the client side we need a service side implementation of a SecurityTokenManager which on the service side is done by inheriting from ServiceCredentialsSecurityTokenManager. It is responsible for imposing what security token is used by setting the type of serializer to use and how the token should be authenticated. In our case we use the Bas64SamlTokenSerializer and a Base64SamlTokenAuthenticator.

   1: public class Base64SamlTokenServiceCredentialsSecurityTokenManager : ServiceCredentialsSecurityTokenManager
   2: {
   3:     protected Base64SamlTokenServiceCredentialsSecurityTokenManager(ServiceCredentials parent) : base(parent) { }
   4:  
   5:     public override SecurityTokenAuthenticator CreateSecurityTokenAuthenticator(SecurityTokenRequirement tokenRequirement, out SecurityTokenResolver outOfBandTokenResolver)
   6:     {
   7:         if (tokenRequirement.TokenType == Base64SamlTokenConstants.Base64SamlTokenType)
   8:         {
   9:             outOfBandTokenResolver = null;
  10:             return new Base64SamlTokenAuthenticator(new SamlTokenValidator());
  11:         }
  12:         return base.CreateSecurityTokenAuthenticator(tokenRequirement, out outOfBandTokenResolver);
  13:     }
  14:  
  15:     public override SecurityTokenSerializer CreateSecurityTokenSerializer(SecurityTokenVersion version)
  16:     {
  17:         return new Base64SamlTokenSecurityTokenSerializer();
  18:     }
  19: }

Security Token Authenticator

The SecurityTokenAuthenticator determines if it can validate the supplied token and how it should validate the token. Our authenticator takes in a SamlTokenValidator which I have not included in the example but all it does is extract out the username from the base64 SAML token string using some third party SAML library from ComponentSpace as this is what was originally used to generate the token and was easy to use to read the token. Once the name has been extracted from the token it it is passed into an instance of our custom authorization policy which in turn is added to the authenticators list of authorization policies.

   1: public class Base64SamlTokenAuthenticator : SecurityTokenAuthenticator
   2: {
   3:     private readonly SamlTokenValidator validator;
   4:  
   5:     public Base64SamlTokenAuthenticator(SamlTokenValidator validator)
   6:     {
   7:         this.validator = validator;
   8:     }
   9:  
  10:     protected override bool CanValidateTokenCore(SecurityToken token)
  11:     {
  12:         return token is Base64SamlToken;
  13:     }
  14:  
  15:     protected override ReadOnlyCollection<IAuthorizationPolicy> ValidateTokenCore(SecurityToken token)
  16:     {
  17:         var base64SamlToken = (Base64SamlToken) token;
  18:  
  19:         string nameIdentifier = validator.GetNameIdentifierFromSamlResponse(base64SamlToken.Base64SamlTokenString);
  20:  
  21:         var policies = new List<IAuthorizationPolicy>(1) { new Base64SamlTokenAuthorizationPolicy(nameIdentifier) };
  22:  
  23:         return policies.AsReadOnly();
  24:     }
  25: }

Authorization Policy

Authorization policies are defined by implementing the IAuthorizationPolicy interface. They allow a user to be authorized by applying a set of rules against the users claims. Here we don’t actually authorise as this was done in our SamlTokenValidator which perhaps should have been injected into our authorization policy but it is what it is. All we are doing in our policy is adding the user name extracted from the SAML token to the users claim sets as a name identifier.

   1: public class Base64SamlTokenAuthorizationPolicy : IAuthorizationPolicy
   2: {
   3:     private readonly string nameIdentifier;
   4:      
   5:     public Base64SamlTokenAuthorizationPolicy(string nameIdentifier)
   6:     {
   7:         this.nameIdentifier = nameIdentifier;
   8:          
   9:         Id = Guid.NewGuid().ToString();
  10:     }
  11:      
  12:     public string Id { get; private set; }
  13:      
  14:     public ClaimSet Issuer { get; private set; }
  15:      
  16:     public bool Evaluate(EvaluationContext evaluationContext, ref object state)
  17:     {
  18:         var claimSet = new DefaultClaimSet(new Claim(ClaimTypes.NameIdentifier, nameIdentifier, Rights.Identity));
  19:         evaluationContext.AddClaimSet(this, claimSet);
  20:          
  21:         return true;
  22:     }
  23: }

Service Credentials

The ServiceCredentials work the same way on the service as they do on the client, except that they inherit from a different base class, they define the token manager to use.

   1: public class Base64SamlTokenServiceCredentials : ServiceCredentials
   2: {
   3:     protected override ServiceCredentials CloneCore()
   4:     {
   5:         return new Base64SamlTokenServiceCredentials();
   6:     }
   7:  
   8:     public override SecurityTokenManager CreateSecurityTokenManager()
   9:     {
  10:         return new Base64SamlTokenServiceCredentialsSecurityTokenManager(this);
  11:     }
  12: }

Service Credentials Element

Again as with the client we don’t have to do wire up in the config but it can be cleaner. Here we inherit from the ServiceCredentialsElement base class and set up the behaviour we expect our service to use.

   1: public class Base64SamlTokenServiceCredentialsElement : ServiceCredentialsElement
   2: {
   3:     protected override object CreateBehavior()
   4:     {
   5:         var credentials = new Base64SamlTokenServiceCredentials();
   6:         ApplyConfiguration(credentials);
   7:         return credentials;
   8:     }
   9: }

Configuration and Setup

Finally the above is irrelevant if we don’t wire it up, in order to use it all we need to wire up both the client and the service and use the information now stored in the claim set.

Client configuration

The following initializes the channel, extracts out the client credentials so that  a certificate can be applied for securing the message and our base64 SAML token can be added.

   1: var channelFactory = new ChannelFactory<IMyService>("BasicHttpBinding_IMyService");
   2:  
   3: var credentials = ((Base64SamlTokenClientCredentials)channelFactory.Endpoint.Behaviors[typeof(Base64SamlTokenClientCredentials)]);
   4: credentials.ServiceCertificate.DefaultCertificate = new X509Certificate2(<Some certificate for securing the channel>);
   5: credentials.Base64SamlToken = <our base64 SAML token>
   6:  
   7: return channelFactory.CreateChannel();

So that we don’t have to wire up the behaviours in code we do the wire up in the config file, this is done with a custom binding using our Base64SamlTokenBinding and our Base64SamlTokenClientCredentials behaviour.

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <configuration>
   3:     ...
   4:     <system.serviceModel>
   5:  
   6:         <bindings>
   7:             <customBinding>
   8:                 <binding name="MyServiceBinding" sendTimeout="00:05:00">
   9:                     <Base64SamlTokenBinding>
  10:                         <localClientSettings maxClockSkew="01:00:00" />
  11:                     </Base64SamlTokenBinding>
  12:                 </binding>
  13:             </customBinding>
  14:         </bindings>
  15:  
  16:         <behaviors>
  17:             <endpointBehaviors>
  18:                 <behavior name="MyServiceBehavior">
  19:                     <Base64SamlTokenClientCredentials />
  20:                 </behavior>
  21:             </endpointBehaviors>
  22:         </behaviors>
  23:  
  24:         <extensions>
  25:             <bindingElementExtensions>
  26:                 <add name="Base64SamlTokenBinding" type="MyNamespace.Common.Base64SamlTokenBindingElement, MyCommonAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  27:             </bindingElementExtensions>
  28:             <behaviorExtensions>
  29:                 <add name="Base64SamlTokenClientCredentials" type="MyNamespace.Client.Base64SamlTokenClientCredentialsElement, MyClientAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  30:             </behaviorExtensions>
  31:         </extensions>
  32:     </system.serviceModel>
  33:     ...
  34: </configuration>

Server Configuration

The following wires up the service host. It sets the certificate needed for de-serializing the message from the client.

   1: public class MyServiceHost : ServiceHost
   2: {
   3:     public MyServiceHost(params Uri[] addresses) : base(typeof(MyService), addresses) { } 
   4:  
   5:     protected override void InitializeRuntime()
   6:     {
   7:         var credentials = (Base64SamlTokenServiceCredentials)Description.Behaviors[typeof(Base64SamlTokenServiceCredentials)];
   8:         credentials.ServiceCertificate.Certificate = new X509Certificate2(<some certificate for securing the channel>);
   9:  
  10:         base.InitializeRuntime();
  11:     }
  12: }

Like the client we need to wire up the binding and the behaviour in the config if we are not doing it in code.

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <configuration>
   3:     ...
   4:     <system.serviceModel>
   5:         <extensions>
   6:             <bindingElementExtensions>
   7:                 <add name="Base64SamlTokenBinding" type="MyNamespace.Common.Base64SamlTokenBindingElement, MyCommonAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
   8:             </bindingElementExtensions>
   9:             <behaviorExtensions>
  10:                 <add name="Base64SamlTokenServiceCredentials" type="MyNamespace.Server.Base64SamlTokenServiceCredentialsElement, MyServerAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  11:             </behaviorExtensions>
  12:     </extensions>
  13:     <bindings>
  14:      
  15:         <customBinding>
  16:             <binding name="MyServiceBinding" sendTimeout="00:05:00">
  17:                 <Base64SamlTokenBinding>
  18:                     <localServiceSettings maxClockSkew="01:00:00" />
  19:                 </Base64SamlTokenBinding>
  20:             </binding>
  21:         </customBinding>
  22:      
  23:     </bindings>
  24:         <behaviors>
  25:             <serviceBehaviors>
  26:                 <behavior name="MyServiceBehavior">
  27:                     <Base64SamlTokenServiceCredentials />
  28:                 </behavior>
  29:             </serviceBehaviors>
  30:         </behaviors>
  31:     </system.serviceModel>
  32:     ...
  33: </configuration>

Finally we need to actually make use of the information sent. Here we just extract the username from the claim set and return it in a simple hello string. In reality, we used the name to lookup the user in a local database. The query to find the claim is ugly and should probably be done as a nice linq query but it does show how the claims and claim sets are structured.

   1: public class MyService : IMyService
   2: {
   3:     public string HelloWorld()
   4:     {
   5:         AuthorizationContext authContext = ServiceSecurityContext.Current.AuthorizationContext;
   6:  
   7:         foreach (ClaimSet claimset in ServiceSecurityContext.Current.AuthorizationContext.ClaimSets)
   8:         {
   9:             foreach (Claim claim in claimset)
  10:             {
  11:                 if (claim.ClaimType == ClaimTypes.NameIdentifier)
  12:                 {
  13:                     return "Hello " + claim.Resource.ToString();
  14:                 }
  15:             }
  16:         }
  17:         throw new Exception("Didn't expect to get here!");
  18:     }
  19: }

1 comment: