Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom client certificate and username validation in WCF service

My particular problem is something like this:

  • We are currently running a set of services which requires the clients to provide a username and password as authentication when calling the services.

  • We would like to implement a PKI-infrastructure on these services, but some of our partners will use longer time to accommodate to this new infrastructure than the others.

  • As a first step we want to require client certificates from some of our partners. A client certificate will be required (in addition to username and password) to access their data on our servers, while for the other users only username and password will be required.

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.

like image 549
Hanskun Avatar asked Jul 05 '11 13:07

Hanskun


1 Answers

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! :)

like image 115
Hanskun Avatar answered Nov 03 '22 02:11

Hanskun