Search code examples
c#.netwcfwcf-security

WCF "Basic" transport security issue when hosted in IIS


I am attempting to secure a new .Net 4.5 WCF service using HTTPS / SSL, Basic client credentials and the WebHttpBinding. From reading up online I found a good series of Blog Posts from Allen Conway which I have used as a template.

WCF configuration

 <system.serviceModel>
    <bindings>
      <webHttpBinding>
        <binding name="webInteropSecureBinding" allowCookies="false" maxBufferPoolSize="2097152" maxBufferSize="2097152" maxReceivedMessageSize="2097152">
          <security mode="Transport">
            <transport clientCredentialType="Basic"></transport>
          </security>
        </binding>
      </webHttpBinding>
    </bindings>
    <services>
      <service name="PsmDataProvider.PsmProvider" behaviorConfiguration="SecureRest">
        <clear />
        <endpoint address="" binding="webHttpBinding" bindingConfiguration="webInteropSecureBinding" name="PsmProvider" contract="PsmDataProvider.IPsmProvider" behaviorConfiguration="webHttpBehavior" />
        <endpoint address="mex" binding="mexHttpsBinding" name="mex" contract="IMetadataExchange" listenUriMode="Explicit" />
        <host>
          <baseAddresses>
            <add baseAddress="https://localhost:44300/PsmProvider/" />
          </baseAddresses>
        </host>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="SecureRest">
          <serviceMetadata httpGetEnabled="false" httpsGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
          <serviceCredentials>
            <userNameAuthentication userNamePasswordValidationMode="Custom" 
                                    customUserNamePasswordValidatorType="PsmDataProvider.Security.CustomerUserNamePasswordValidator, PsmDataProvider"/>
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="webHttpBehavior">
          <webHttp />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
  </system.serviceModel>

CustomerUserNamePasswordValidator

I have stubbed out the CustomerUserNamePasswordValidator implementation and have confirmed that the constructor is called before the exception is raised.

using System;
using System.IdentityModel.Selectors;

namespace PsmDataProvider.Security
{
    internal class CustomerUserNamePasswordValidator : UserNamePasswordValidator, ICustomerUserNamePasswordValidator 
    {

        public CustomerUserNamePasswordValidator()
        {
        }

        public override void Validate(string userName, string password)
        {          
            if (userName == null) throw new ArgumentNullException("userName","The username must be provided in the request to access this service");
            if (password == null) throw new ArgumentNullException("password", "The password must be provided in the request to access this service");

        }
    }
}

When I try to run the code in VS2012 through IIS Express the service fails to start with the below error.

enter image description here

If I remove the clientCredentialType from the configuration then it works but I require the additional security of using the username / password validation on the service and possibly at a method level in the future.

Is this something I have configured incorrectly in the WCF config or a problem with the configuration in IISExpress?

Please help...


Solution

  • The issue appears to be when using Basic Authentication when hosting the service in IIS as IIS wants to handle the authentication.

    This is discussed in this MSDN blog post

    In the version of WCF that shipped with .Net Framework 3.0 we didn't support custom validators with transport level HTTP security. We received much feedback from the community that this was a highly desired feature, so I'm happy to say we added support for this scenario in the 3.5 release of the .Net Framework. Note that this is only supported under self hosted services.

    There is a resolution as discussed in Allen Conway's Blog Post by implementing a custom authorisation manager derived from ServiceAuthorizationManager

    CustomAuthorizationManager

    public class CustomAuthorizationManager : ServiceAuthorizationManager 
    {
        private const string UserName = "username";
        private const string Password = "password";
    
        protected override bool CheckAccessCore(OperationContext operationContext)
        {
            string authHeader = WebOperationContext.Current.IncomingRequest.Headers["Authorization"];
    
            if ((authHeader != null) && (authHeader != string.Empty))
            {
                string[] svcCredentials = System.Text.ASCIIEncoding.ASCII
                                            .GetString(Convert.FromBase64String(authHeader.Substring(6)))
                                            .Split(':');
    
                var user = new { Name = svcCredentials[0], Password = svcCredentials[1] };
    
                if ((user.Name.Equals(UserName) && user.Password.Equals(Password)))
                    return true;
                else
                    return false;
            }
            else
            {
                WebOperationContext.Current.OutgoingResponse.Headers.Add("WWW-Authenticate: Basic realm=\"PsmProvider\"");
                throw new WebFaultException(HttpStatusCode.Unauthorized);
            }
        }
    
    }
    

    Config

      <system.serviceModel>
        <bindings>
          <webHttpBinding>
            <binding name="webInteropSecureBinding" allowCookies="false" maxBufferPoolSize="51200" maxBufferSize="51200" maxReceivedMessageSize="51200">
              <security mode="Transport"/>
            </binding>
          </webHttpBinding>
        </bindings>
        <services>
          <service name="PsmDataProvider.PsmProvider" behaviorConfiguration="SecureRest">
            <clear />
            <endpoint binding="webHttpBinding" bindingConfiguration="webInteropSecureBinding" 
                        name="PsmProvider" contract="PsmDataProvider.IPsmProvider" behaviorConfiguration="webHttpBehavior" />
            <endpoint address="mex" binding="mexHttpsBinding" name="mex" contract="IMetadataExchange" />
            <host>
              <baseAddresses>
                <add baseAddress="https://localhost:44300/PsmProvider/" />
              </baseAddresses>
            </host>
          </service>
        </services>
        <behaviors>
          <serviceBehaviors>
            <behavior name="SecureRest">
              <serviceMetadata httpGetEnabled="false" httpsGetEnabled="true" />
              <serviceDebug includeExceptionDetailInFaults="true" />
              <serviceAuthorization serviceAuthorizationManagerType="PsmDataProvider.Security.CustomAuthorizationManager, PsmDataProvider"/>
            </behavior>
          </serviceBehaviors>
          <endpointBehaviors>
            <behavior name="webHttpBehavior">
              <webHttp/>
            </behavior>
          </endpointBehaviors>
        </behaviors>
        <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
      </system.serviceModel>
    

    Note

    Also note a comment from Travich regarding the IIS / IIS Express configuration

    Travich said... One thing to help other users. It was briefly stated, but something I overlooked... Turn off Basic Auth in IIS and remove tag from your webHttpBinding!

    Works for me.