Tuesday, November 12, 2013

How to programmatically configure WCF services and clients at runtime pt. 3.

In this post, we’ll go into some details about WCF and WIF (Windows Identity Foundation). I have to admit that I used some time to get the demo to work (with both azure  acs and ADFS). However, it seems that Azure ACS does have a big short coming for this type of scenario, it does not support ActAs (or identity delegation if you like). Which in a SoA context is vital to ensure end-to-end security for messages and features of the UI.

Please note that the code provided is _not_ production ready.

The two scenarios we are looking at:

1.       Identity federation: enable claims based authentication on wcf services by dynamically configuring WCF and WIF. Ideal for active clients like thick clients and background services.

2.       Identity delegation: allows a user identity to be reused by a trusted service account. Ideal for passive clients like web browsers.

Because ACS does not support ActAs we end up with a slightly more complex example that I initially imagined. Here is how this sample works:



The end user authenticates with Azure ACS (in the sample ACS is configured to authenticate with Facebook, google, Microsoft or Yahoo!) while the web server authenticates the service account with ADFS only. Both the user and the service account must have valid users that is authenticated.
Time for code (enough with the abstract stuff).

First we need to add a new binding builder for the secure binding.

internal class SecureBindingBuilder : IBindingBuilder
    {
        private bool IsClient;
 
        public Binding CreateBinding(string serviceName)
        {
            System.Net.ServicePointManager.ServerCertificateValidationCallback =
           ((sender, certificate, chain, sslPolicyErrors) => true);
            var secureBinding = new WS2007FederationHttpBinding(WSFederationHttpSecurityMode.TransportWithMessageCredential)
                                    {
                                        Name = "secure",
                                        TransactionFlow = false,
                                        HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
                                        MaxBufferPoolSize = int.Parse(GetWsValue(serviceName, "MaxBufferPoolSize")),
                                        MaxReceivedMessageSize = int.Parse(GetWsValue(serviceName, "MaxReceivedSize")),
                                        MessageEncoding = (WSMessageEncoding)Enum.Parse(typeof(WSMessageEncoding), GetWsValue(serviceName, "MessageFormat")),
                                        TextEncoding = Encoding.GetEncoding(GetWsValue(serviceName, "TextEncoding")),
                                        ReaderQuotas = XmlDictionaryReaderQuotas.Max,
                                        BypassProxyOnLocal = true,
                                        UseDefaultWebProxy = false,
                                    };
            SetSecuritySettings(serviceName, secureBinding);
            return secureBinding;
        }
 
        private void SetSecuritySettings(string serviceName, WSFederationHttpBinding secureBinding)
        {
            secureBinding.Security.Message.AlgorithmSuite = SecurityAlgorithmSuite.Basic256;
            secureBinding.Security.Message.NegotiateServiceCredential = false;
            secureBinding.Security.Message.EstablishSecurityContext = false;
            secureBinding.Security.Message.IssuedKeyType = SecurityKeyType.BearerKey;
            secureBinding.Security.Message.IssuedTokenType = null;
            EndpointIdentity identity = EndpointIdentity.CreateDnsIdentity((new Uri(AddressHelper.GetIssuerName(serviceName)).DnsSafeHost));
            secureBinding.Security.Message.IssuerAddress = CreateIssuerAddress(serviceName,identity);
            if (IsClient) secureBinding.Security.Message.IssuerBinding = CreateIssuerBinding(serviceName);
            else
                secureBinding.Security.Message.IssuerMetadataAddress = new EndpointAddress(new Uri(AddressHelper.GetStsMetadataAddress(serviceName)),identity);
        }
 
        private  EndpointAddress CreateIssuerAddress(string serviceName,EndpointIdentity identity)
        {
            if(IsClient)
                return new EndpointAddress(new Uri(AddressHelper.GetStsActAsAddress(serviceName)));
            return new EndpointAddress(new Uri(AddressHelper.GetIssuerName(serviceName)));
 
        }
 
        private static  Binding CreateIssuerBinding(string serviceName)
        {
            return BindingFactory.GetBindingBuilder("stsbinding").CreateBinding(serviceName);
        }
 
        private static string GetWsValue(string serviceName, string valueName)
        {
            var inheritFrom = GetSecureValue(serviceName, "InheritFrom");
            return ConfigurationReader.Reader.GetValue(ExpressionBuilder.GetEndpointConfigValue(serviceName, inheritFrom, valueName));
        }
 
        private static string GetSecureValue(string serviceName, string valueName)
        {
            return ConfigurationReader.Reader.GetValue(ExpressionBuilder.GetEndpointConfigValue(serviceName, "secure", valueName));
        }
 
        public void SetClientMode(bool isClient)
        {
            IsClient = isClient;
        }
    }

Note that we use the WS2007FederationHttpBinding which has most of the binding elements for WIF added. The first line in CreateBinding is added only to allow connections over SSL with self signed certificates, this should be removed in production code or made optional though the configuration framework.

For WIF to hook properly into the WCF framework we need to do some configuration on the service host. Create a helper method in HostFactory called Configure to make this independent of the service implementation.

public static void Configure(ServiceConfiguration config)
        {
            var serviceName = GetServiceName(config.Description.ServiceType);
            config.IdentityConfiguration = new IdentityConfiguration()
                                               {
                                                   TrustedStoreLocation = new StoreLocation(),
                                                   AudienceRestriction = { AudienceMode = AudienceUriMode.Always },
                                                   SaveBootstrapContext = true,
                                                   IssuerTokenResolver = new IssuerTokenResolver(),
                                                   CertificateValidationMode = X509CertificateValidationMode.None,
                                               };
            config.IdentityConfiguration.AudienceRestriction.AllowedAudienceUris.Add(new Uri(GetSecureValue(serviceName, "Audience")));
            config.IdentityConfiguration.IssuerNameRegistry = CreateIssuerNameRegistry(serviceName);
            config.UseIdentityConfiguration = true;
        }
 
        private static ConfigurationBasedIssuerNameRegistry CreateIssuerNameRegistry(string serviceName)
        {
            var registry = new ConfigurationBasedIssuerNameRegistry();
            registry.AddTrustedIssuer(GetSecureValue(serviceName, "Thumbprint"), GetSecureValue(serviceName, "IssuerName"));
            return registry;
        }

 We need to run this code from the service implementation (to me it seems that this is convention based) by adding the following code in DemoService.svc.cs

        {
            HostFactory.Configure(config);
        }

When this is done we need some way to get security tokens from ADFS. By isolating the needed code in a TokenManager class, we reduce the number of changes needed in the ServiceProxyContainer class.

public class TokenManager
    {
        private readonly string ServiceName;
        private readonly string UserName;
        private readonly string Password;
        private readonly SecurityToken BootstrapToken;
 
        public TokenManager(string serviceName, string username,string password)
        {
            UserName = username;
            Password = password;
            ServiceName = serviceName;
        }
 
        public TokenManager(string serviceName,SecurityToken bootstrapToken, string username, string password)
        {
            UserName = username;
            Password = password;
            ServiceName = serviceName;
            BootstrapToken = bootstrapToken;
        }
 
        public SecurityToken GetToken()
        {
            var channel = CreateChannel();
            var rst = CreateIssueNewRequest();
            return GetSecurityToken(channel, rst);
        }
 
        private RequestSecurityToken CreateIssueNewRequest()
        {
            var rst = new RequestSecurityToken
                          {
                              RequestType = RequestTypes.Issue,
                              KeyType = KeyTypes.Bearer,
                              AppliesTo =
                                  new EndpointReference(
                                  ConfigurationReader.Reader.GetValue(
                                      ExpressionBuilder.GetEndpointConfigValue(
                                          ServiceName, "secure", "Address")))
                          };
            return rst;
        }
 
        private static SecurityToken GetSecurityToken(WSTrustChannel channel, RequestSecurityToken rst)
        {
            RequestSecurityTokenResponse rstr = null;
            var token = channel.Issue(rst, out rstr);
            return token;
        }
 
        private WSTrustChannel CreateChannel()
        {
            var stsBinding = CreateStsBinding();
            var trustChannelFactory = new WSTrustChannelFactory(stsBinding, AddressHelper.GetStsAddress(ServiceName))
                                          {
                                              TrustVersion =
                                                  TrustVersion
                                                  .WSTrust13
                                          };
            trustChannelFactory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode =
                X509CertificateValidationMode.None;
            trustChannelFactory.Credentials.UserName.UserName = UserName;
            trustChannelFactory.Credentials.UserName.Password = Password;
            trustChannelFactory.Endpoint.Behaviors.Add(new MustUnderstandBehavior(false));
            var channel = (WSTrustChannel)trustChannelFactory.CreateChannel();
            return channel;
        }
 
        private Binding CreateStsBinding()
        {
            return BindingFactory.GetBindingBuilder("stsbinding").CreateBinding(ServiceName);
        }
 
        public SecurityToken GetTokenOnBehalfOf()
        {
            var channel = CreateChannel();
            var rst = CreateDelegateTokenRequest();
            return GetSecurityToken(channel, rst);
        }
 
        private RequestSecurityToken CreateDelegateTokenRequest()
        {
            var rst = new RequestSecurityToken
                          {
                              RequestType = RequestTypes.Issue,
                              KeyType = KeyTypes.Bearer,
                              AppliesTo =
                                  new EndpointReference(
                                  ConfigurationReader.Reader.GetValue(
                                      ExpressionBuilder.GetEndpointConfigValue(
                                          ServiceName, "secure", "Address"))),
                              ActAs = new SecurityTokenElement(BootstrapToken)
                          };
            return rst;
        }
    }         

As you can see, we reuse the binding builder for the STS binding both in the client and the service. This set up the communication with, in this case, ADFS.

Create a new channel factory based on this binding, and set the client credentials. Regardless whether we are using delegate tokens or direct logon the channel will be the same. In the sample code we do not perform certificate validation. After the channel is configured and created, we create a request and call the Issue method on the channel. The token we receive will be used when we create the service channel in the container.
As a last step we need to modify the ServiceProxyContainer:

Add to methods to get security tokens:

private SecurityToken GetToken()
        {
            var manager = new TokenManager(ServiceName, UserName, Password);
            return manager.GetToken();
        }
 
        private SecurityToken GetTokenOnBehalfOf()
        {
            var manager = new TokenManager(ServiceName,GetTokenFromBootstrap(), UserName, Password);
            return manager.GetTokenOnBehalfOf();
        }

 

And create the channel that uses the issued security token:

public T GetClient()
        {
            if (Client == null)
            {
                if (BootstrapContext != null) Client = ClientFactory.CreateChannelWithIssuedToken(GetTokenOnBehalfOf());
                else if (UseSecureChannel) Client = ClientFactory.CreateChannelWithIssuedToken(GetToken());
                else Client = ClientFactory.CreateChannel(new EndpointAddress(Url));
            }
            return Client;
        }

Handling the tokens in this way makes it a lot easier to do debugging and to inspect the tokens before they are sent to the service.

This is basically it.

I have added a simple MVC app to show delegation in action. While the console application is mainly the same.

Some help with setting up ADFS to forward name identifier in the issued token.
http://social.msdn.microsoft.com/Forums/vstudio/en-US/2fbd4acf-358e-4bf3-abf6-fc2c9daa20ca/incoming-name-id-format-for-incoming-claim-type-name-id-in-adfs-v20

I hope you enjoyed these posts, I might add some samples on MSMQ and Net.TCP if you are interested. However, it should be easy to add this using the sample code provided as a template.

No comments: