Secure Self-Hosted WCF REST Services with a Custom UserNamePasswordValidator

Download the code for this blog post here.

When securing WCF services you’re faced with a choice: Message versus Transport security. Unless you need to conceal messages from an intermediary, your best bet is to stick with transport security and use SSL to secure messages traveling over HTTP.  Using SSL is generally the best choice for ensuring point-to-point privacy and integrity, which lets you pass user credentials over the wire when directly invoking service operations.  This means you can eschew the complexity and overhead of message-based security in favor of the simpler and leaner model of transport-based security.

Once you’ve settled on the option of transport security, there’s the issue of which authentication mode to use.  In most B2B scenarios, it makes sense to go with X509 certificates for client authentication, but that also places demands on clients to sign messages using the certificate.  Another possibility is plain old shared-secret authentication where you might look up usernames and passwords in a database in order to authenticate requests.  WCF has terrific support for this scenario and allows you to supply a custom UserNamePasswordValidator, which you can use to validate client credentials.  Here is an example of a class that extends UserNamePasswordValidator by validating passwords against a hard-coded value.  (Of course, you’ll want to employ a more sophisticated technique, such as hashing the password to compare against entries in a database table.)

public class PasswordValidator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        if (string.Equals(userName, "Alice", StringComparison.OrdinalIgnoreCase)
            && password == "Password123!@#") return;
        throw new SecurityTokenValidationException();
    }
}

You’ll need to set the security mode of the basic HTTP binding to “TransportWithMessageCredential.” This will cause the service to look in a soap header for client credentials. Then you’ll want to add a serviceCredentials behavior that sets the validation mode to “Custom” and specifies PasswordValidator as the validator type.

<serviceBehaviors>
  <behavior>
    <serviceCredentials>
      <userNameAuthentication userNamePasswordValidationMode="Custom"
          customUserNamePasswordValidatorType="Security.PasswordValidator, Security"/>
    </serviceCredentials>
  </behavior>
</serviceBehaviors>

This is all fine and dandy, but it assumes that clients will only be talking Soap – what about REST-ful clients who don’t know a thing about Soap? It turns out the serviceCredentials behavior doesn’t really have much to do with whether it’s a Soap or Rest-based service.  To authenticate REST clients, you can set the security mode of the web http binding to “Transport” and specify a client credential type of “Basic.”

<webHttpBinding>
  <binding>
    <security mode="Transport">
      <transport clientCredentialType="Basic"/>
    </security>
  </binding>
</webHttpBinding>

Note that this technique will only work when your service is self-hosted (for example, using a Windows Service).  In this case, WCF will hand off authentication to your custom UserNamePasswordValidator type.  However, when hosting in IIS, WCF will allow IIS to handle basic authentication using Windows accounts, and you’ll need a different approach, such as setting the client credential type to None and handling the authentication yourself.

Once you’ve enabled Basic authentication in your self-hosted WCF service, it’s up to the client to set the Authorization header to “Basic” with a username:password string that is base64 encoded.

private static string GetAuthHeader(string userName, string password)
{
    string userNamePassword = Convert.ToBase64String
        (new UTF8Encoding().GetBytes(string.Format("{0}:{1}", userName, password)));
    return string.Format("Basic {0}", userNamePassword);
}

To enable SSL for self-hosted services you’ll need to perform a few extra steps.  First, open an admin command prompt and bind the certificate to the port you’ll be using.  You’ll need to inspect the certificate details (use the Certificates MMC snap-in) to copy the thumbprint, remove the spaces and set the certHash parameter to it. Any arbitrary guid will work for the appId parameter.

netsh http add sslcert ipport=0.0.0.0:2345 certhash=a66c5ed2b8ab91de21d637c6f9a13fd45a8ba92a appid={646937c0-1042-4e81-a3b6-47d678d68ba9}

You may also need to grant permission for the process hosting your service to register a url with Http.Sys.

netsh http add urlacl url=https://+:2345/ user=NetworkService

This assumes you’re running Windows Vista or later.  For earlier versions of Windows you will need to use httpcfg – see here for more info.

The nice thing about WCF is its unified programming model, which allows you to use the same username / password validator for both Soap and Rest clients.  Download and go through the code for this blog post to see it all in action.  Enjoy.

About Tony Sneed

Sr. Software Solutions Architect, Hilti Global Application Software
This entry was posted in Technical and tagged , , . Bookmark the permalink.

19 Responses to Secure Self-Hosted WCF REST Services with a Custom UserNamePasswordValidator

  1. THis does not work in IIS. Basic in WCF means also Basic in IIS. And then IIS will authenticate against Windows accounts only. The validator won’t even get called.

  2. Ah – and the encoding for the header is also not correct. Need to look at my code, but it’s not ASCII…

  3. Ian Hannah says:

    An excellent article but I have one question. I want to play with basic authentication without using a certificate i.e. over HTTP and not HTTPs. However, in order to use the user name and password validation I must have the security mode set to transport otherwise it will not get called and so this means that I must use HTTPs! Is there a way to use the validation without the need for a certificate?

    Thanks
    Ian

    • Tony Sneed says:

      This should be covered in the blog article, but it only works for self-hosted WCF services, for ex, a Windows Service. If hosted in IIS you’re out of luck – your validator will not be called.

      • Ian Hannah says:

        Hi Tony,
        I have it all working now. What I now want to do is use the validate method to call a stored procedure in our database which returns a user id. I need to user the user id in all my web service methods.

        What is the best way to “store” the user id in the validate method and use it in my methods?

        Thanks
        Ian

  4. Andre says:

    Hi Tony,
    This is a great example and its really helped me out. I do have one problem. After I put the certs in the store(s) Local Computer in Trusted Root and personal. When I reboot i have to rebind it with netsh http add sslcert . Have you found a workaround?

  5. Joseph D'silva says:

    Tony, thanks for your Articles. They are useful 🙂

  6. Simon says:

    Hi Tony,
    Your service only use WebGet. Can you make a WebInvoke with POST method? Also, can you try to use a JQuery client and call the POST method? It seems that I can make the GET method works,
    but not POST.

    Thanks
    Simon

  7. Simon says:

    Hi Tony,
    I think I understand what was going on but I don’t know how fix it. It is about the parameter passing. For example I have a API method like
    [OperationContract]
    [WebInvoke(Method = “POST”, BodyStyle = WebMessageBodyStyle.Wrapped,
    RequestFormat = WebMessageFormat.Json,
    ResponseFormat = WebMessageFormat.Json)]
    UserInfo GetUserInfo(string username, string domain);
    The Ajax call have to make this a json format parameter.
    Now I don’t know how to.Something like ‘{“username”: “Simon”, “domain”: “people.loc.com”}’ does not work, any idea?

    • Tony Sneed says:

      To pass multiple parameters to POST or PUT, you need to specify a UriTemplate with WebInvoke. See this answer on SO: http://stackoverflow.com/a/5281382.

      • Simon says:

        The problem with GET method is that I have to put it in the querystring and that is the major a reason I try to avoid this method if the parameter is sort of sensitive data. If I use the first option from that blog, It is like making it a querystring again. Now I think the only option I will adopt is make all primitive parameter as properties of a class. The drawback is not a readable change. Anyway, thank you very much for your prompt response and help!

  8. Simon says:

    Hi Tony,

    I can make the self-signed certificate works when I setup the WCF client and server in the same machine, but I have trouble to make it works between two machines. Here are what I have done,

    I followed the link https://msdn.microsoft.com/en-us/library/ff648498.aspx, use makecert to create self-signed certificate and register it both in server and client machines.

    In my web.config, I follow most of your configuration.

    Now I can make this setup works perfectly when the client and server in the same machine, but I cannot make it works between two machines. It cannot load the page: page not found or something like that. I can ping from the client to the server. I used the same CN name with the host name.

    Very thankful on any hint!

    Simon

    • Tony Sneed says:

      The problem might be with how you are using netsh to reserve the url with https. This is a lot easier using IIS of course. 🙂

      • Simon says:

        Thanks, I will try to use netsh to reserve the URL with https. Last time I did that with AppId(Guid), but it does not work. Let me try it again

      • Tony Sneed says:

        You also have to make sure that the CN on the cert matches the host name as resolved by DNS, and that the cert is issued by a CA that is in the trusted root CA folder of the cert store on the client. By the way, there is a GUI cert tool called HttpConfig, which is easier to use than netsh and can be downloaded from http://www.stevestechspot.com.

  9. Simon says:

    BTW, I can make it works between two machines if I do not use SSL

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.