Hanskun
Hanskun

Reputation: 191

Custom client certificate and username validation in WCF service

My particular problem is something like this:

To solve this problem I am trying to implement a custom validator for both the username/password authentication (using UserNamePasswordValidator) and for the client certificates (using X509CertificateValidator) in WCF. The username/password validator will verify these credentials towards our database, while the client certificate validator will inspect whether the request is from a client from which we require a certificate, and if so verify that a valid client certificate is provided. I have not been able to configure WCF so that it uses both of these validators.

My WCF configuration on the server is currently set up like this:

<behaviors>
  <serviceBehaviors>
    <behavior name="MyServiceBehavior">
      <serviceMetadata httpsGetEnabled="true" policyVersion="Policy15" />
      <serviceDebug includeExceptionDetailInFaults="true" />
      <serviceCredentials>
        <clientCertificate>
          <authentication customCertificateValidatorType="MyWS.Security.MyServicesCertificateValidator, MyWS"
            certificateValidationMode="Custom" revocationMode="NoCheck" />
        </clientCertificate>
        <userNameAuthentication userNamePasswordValidationMode="Custom"
          customUserNamePasswordValidatorType="MyWS.Security.MyServicesUsernameValidator, MyWS" />
      </serviceCredentials>
    </behavior>
  </serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
<bindings>
  <basicHttpBinding>
    <binding name="MySoapBinding">
      <security mode="TransportWithMessageCredential">
        <transport clientCredentialType="Certificate" />
        <message clientCredentialType="UserName" />
      </security>
    </binding>
  </basicHttpBinding>
</bindings>
<services>
  <service behaviorConfiguration="MyServiceBehavior" name="MyWS.Services.TheService">
    <endpoint address="" binding="basicHttpBinding" bindingConfiguration="MySoapBinding" name="TheService" bindingNamespace="https://services.my/TheService" contract="MyWS.Interfaces.Service.ITheService" />
    <host>
      <baseAddresses>
        <add baseAddress="https://localhost:4434/MyWS/TheService"/>
      </baseAddresses>
    </host>
  </service>
</services>

As far as I understand this configuration is invalid because I can't use the customCertificateValidatorType at the transport layer (because IIS inspects the certificate before WCF is involved here), but I can not see how I am able to combine both the customCertificateValidatorType and customUserNamePasswordValidatorType as credential types at the message layer either.

I have implemented a message inspector and might be able to solve the problem using the OperationContext in some way (as suggested in the link below), but I have not been able to see a way for me to do it this way yet.

http://social.msdn.microsoft.com/Forums/en/wcf/thread/b6ab8b58-516b-41d4-bb0e-75b4baf92716

I suppose I might be trying to implement something that is incompatible with the way WCF works, but if someone have an idea about how this could be fixed I would be delighted to have your feedback on this.

Upvotes: 7

Views: 6957

Answers (2)

Hanskun
Hanskun

Reputation: 191

I think I have found a solution to my problem now thanks to valuable input from @ladislav-mrnka in his answer. I realized it is necessary to provide two endpoints to configure the different requirements, and I also learned about the supporting token possibilities when configuring the services.

I found a link about supporting tokens at MSDN, and by following this recipe I have implemented the endpoint on the server with the following custom binding (I switched to configuration through code. Not sure if this can be set up in web.config as well.)

private static Binding CreateMultiFactorAuthenticationBinding()
{
    var httpsTransport = new HttpsTransportBindingElement();

    // The message security binding element will be configured to require 2 tokens:
    // 1) A username-password encrypted with the service token
    // 2) A client certificate used to sign the message

    // Create symmetric security binding element with encrypted username-password token.
    // Symmetric key is encrypted with server certificate.
    var messageSecurity = SecurityBindingElement.CreateUserNameForCertificateBindingElement();
    messageSecurity.AllowInsecureTransport = false;

    // Require client certificate as endorsing supporting token for all requests from client to server
    var clientX509SupportingTokenParameters = new X509SecurityTokenParameters
                                                    {
                                                        InclusionMode =
                                                            SecurityTokenInclusionMode.AlwaysToRecipient
                                                    };
    messageSecurity.EndpointSupportingTokenParameters.Endorsing.Add(clientX509SupportingTokenParameters);

    return new CustomBinding(messageSecurity, httpsTransport);
}

This binding creates a SymmetricSecurityBindingElement where a symmetric key (encrypted with the server certificate) is used to encrypt a username/password security token in the message header, and the message body itself.

In addition a X509 security token is added as an endorsing, supporting token to the binding. This token is configured to always be included in the client requests to the server.

This custom binding was subsequently used to configure a new WCF-service with an endpoint requiring this binding. I am using the WcfFacility in Castle Windsor to configure the service.

This code does the following:

  • Sets the service certificate
  • Sets the validation mode for the client certificates to chain trust, so that incoming client certificates must be issued by a trusted root certificate authority in the server store
  • Adds custom validators for username/password credentials and client certificate
//// Registering WCF-services
var returnFaults = new ServiceDebugBehavior {IncludeExceptionDetailInFaults = true};
var metaData = new ServiceMetadataBehavior {HttpsGetEnabled = true};

var serviceCredentials = new ServiceCredentials();

// Configure service sertificate
serviceCredentials.ServiceCertificate.SetCertificate(
    StoreLocation.LocalMachine, 
    StoreName.My, 
    X509FindType.FindBySubjectName,
    "ServerCertificate");

// Configure client certificate authentication mode
serviceCredentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.ChainTrust;

// Add custom username-password validator
serviceCredentials.UserNameAuthentication.UserNamePasswordValidationMode =
    UserNamePasswordValidationMode.Custom;
serviceCredentials.UserNameAuthentication.CustomUserNamePasswordValidator =
    _container.Resolve<MyServicesUsernameValidator>();

// Add custom certificate validator
serviceCredentials.ClientCertificate.Authentication.CertificateValidationMode =
    X509CertificateValidationMode.Custom;
serviceCredentials.ClientCertificate.Authentication.CustomCertificateValidator =
    _container.Resolve<MyServicesCertificateValidator>();

var serviceModel = new DefaultServiceModel();

serviceModel.AddEndpoints(
    WcfEndpoint.ForContract<IMyContract>().BoundTo(CreateMultiFactorAuthenticationBinding()));
serviceModel.BaseAddresses.Add(new Uri("https://server.com/MyServiceImplementation.svc"));

serviceModel.AddExtensions(serviceCredentials);
serviceModel.AddExtensions(metaData);

_container.AddFacility<WcfFacility>(f => f.CloseTimeout = TimeSpan.Zero)
    .Register(Component.For<IMyContract>()
                    .ImplementedBy<MyServiceImplementation>()
                    .AsWcfService(serviceModel),
                Component.For<IServiceBehavior>().Instance(returnFaults));

MyServicesUsernameValidator inherits UserNamePasswordValidator and MyServicesCertificateValidator inherits X509CertificateValidator. Both overrides their corresponding Validate methods.

This seems to solve my particular problem... Hope it solves yours! :)

Upvotes: 6

Ladislav Mrnka
Ladislav Mrnka

Reputation: 364279

That is not possible to define in configuration with out of the box bindings. Even custom binding doesn't support enough infrastructure to define such binding in configuration.

First you will definitely need two endpoints for this. One will be used for clients with user name / password only. This endpoint can be configured with some common binding expecting either Message security with UserName client credentials or transport security with message credentials. The second endpoint will be for your more complex validation. This endpoint needs new binding defined in code. This binding must use:

  • Asymetric security binding element (mutual certificate authentication)
  • X.509 security token as primary security token
  • User name security token as supporting security token

This is example of the binding I had to use when communicating with similar service:

  Custom binding = new CustomBinding();
  var userNameToken = new UserNameSecurityTokenParameters();
  userNameToken.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;

  var securityElement = new AsymmetricSecurityBindingElement();
  securityElement.IncludeTimestamp = true;
  securityElement.RecipientTokenParameters = new X509SecurityTokenParameters(X509KeyIdentifierClauseType.SubjectKeyIdentifier, SecurityTokenInclusionMode.Never);
  securityElement.InitiatorTokenParameters = new X509SecurityTokenParameters(X509KeyIdentifierClauseType.SubjectKeyIdentifier, SecurityTokenInclusionMode.AlwaysToRecipient);
  securityElement.DefaultAlgorithmSuite = SecurityAlgorithmSuite.Basic256;
  securityElement.SecurityHeaderLayout = SecurityHeaderLayout.Strict;
  securityElement.SetKeyDerivation(false);
  securityElement.EndpointSupportingTokenParameters.SignedEncrypted.Add(userNameToken);
  securityElement.MessageProtectionOrder = MessageProtectionOrder.EncryptBeforeSign;
  securityElement.MessageSecurityVersion = MessageSecurityVersion.WSSecurity11WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11;
  binding.Elements.Add(securityElement);

  var encodingElement = new TextMessageEncodingBindingElement();
  encodingElement.MessageVersion = MessageVersion.Soap12WSAddressingAugust2004;
  binding.Elements.Add(encodingElement);

  var httpElement = new HttpTransportBindingElement();
  httpElement.UseDefaultWebProxy = true;
  binding.Elements.Add(httpElement); 

This example uses CustomBinding defined in code. If you want to use this in configuration you must create whole new binding and binding extension and register that extension in configuration file.

Even then I'm not sure that both validators will be used - I used this as the client of the service. The main point is that request can have only single main token and it is possible that default WCF infrastructure will choose only one to validate but such logic can be also replaced.

Upvotes: 4

Related Questions