Contents

Introduction

This is a follow-up on my earlier article that described how to use BASIC Authentication with a WCF REST Service. The disadvantage of that solution was that you need a https tunnel to really secure the username password verification process. Although possible this is not always a feasible situation, for example if you don´t want to invest in certificates or loose performance when using a https tunnel.

This article explains a different authentication mechanism called Digest Authentication which provides an alternative. This security mechanism is more secure than Basic Authentication and does not have the drawbacks from using a https tunnel.

Digest Authentication is available on multiple web servers and supported by multiple internet browsers. The drawback when using Digest Authentication with Internet Information server is that it automatically authenticates credentials against active directory. This article describes an implementation which enables you to secure a WCF REST service with Digest Authentication and authenticate against any back-end.

Digest Authentication was first described in RFC 2069 as an extension to HTTP Basic Authentication. Later the verification algorithm and security was improved by RFC 2617. This is the current stable specification. The implementation in this article is based on that RFC 2617 specification. Digest Authentication is more secure because it uses MD5 cryptographic hashing and the use of a nonce to discourage cryptanalysis.

Overview of Digest Communication

Digest communication starts with a client that requests a resource from a web server. If the resource is secured with Digest Authentication the server will respond with the http status code 401, which means Unauthorized.

Digest Authentication Communication

In the same response the server indicates in the HTTP header with which mechanism the resource is secured. The HTTP header contains the following "WWW-Authenticate: Digest realm="realm", nonce="IVjZjc3Yg==", qop="auth". The first thing you should notice is the string Digest in the response, here the server indicates that the resource that was requested by the client is secured using Digest Authentication. Secondly, the server indicates the type of Digest Authentication algorithm to use by the client with Quality Of Protection (QOP) and the string called nonce which I will explain later in this article.

An internet browser responds to this by presenting the user a dialog, in this dialog the user is able to enter his username and password. Note, that this dialog does not show the warning about transmitting the credentials in clear text as with a Basic Authentication secured site.

Digest Authentication Credentials Screen

When the user enters the credentials in this dialog, the browser request the resource from the server again. This time the client adds additional information to the HTTP header regarding Digest Authentication.

Digest Authentication Second

The server validates the information and returns the requested resource to the client. The details of the response from the server and the additional request of the client will be described in the following part of this article.

Digest Authentication

When the server responds to an unauthenticated client request, the server adds a nonce and a qop key to the header of the HTTP response. Both are typical for Digest Authentication. First the nonce will be described and second the QOP quality of protection.

The Nonce

The Nonce stands for "Number used Once", this is a pseudo random number that ensures that old communications between a client and a server cannot be reused in replay attacks. A replay attack is a network attack in which previous valid data transmission is repeated. This is done by an adversary who intercepts the data and retransmit it. According to the RFC 2716 specification the Nonce is a server specified data string which should be uniquely generated each time a 401 response is returned by the server. The 401 response that is sent back to the client includes the Nonce generated by the server. According to RFC 2716 the client should add this nonce to the header of next requests.

Generating the Nonce

The format of the nonce depends on the implementation. Each RFC 2617 digest authentication implementation may define their own nonce format. However, one should carefully design the format of the nonce as it is a part of the quality of the security. For my implementation I choose to include a date time stamp and the IP address of the client into the nonce. The implementation generates the nonce as follows.

Nonce = Base64( TimeStamp : PrivateHash)


The nonce is generated by base64 encoding the string that is constructed by concatenating the time stamp, a colon and a generated private hash. In the source code this is handled by the NonceGenerator class which has a Generate method that generates the Nonce string.
public string Generate(string ipAddress)
{
   double dateTimeInMilliSeconds =
      (DateTime.UtcNow - DateTime.MinValue).TotalMilliseconds;
   string dateTimeInMilliSecondsString =
      dateTimeInMilliSeconds.ToString();
   string privateHash = privateHashEncoder.Encode(
      dateTimeInMilliSecondsString,
      ipAddress);
   string stringToBase64Encode =
      string.Format("{0}:{1}", dateTimeInMilliSecondsString, privateHash);
   return base64Converter.Encode(stringToBase64Encode);
}
MD5 is used to generate the private hash of the string that is constructed by concatenating the time stamp, a colon, the IP address of the client, a colon and a private key that is only known to the server. As MD5 is used the generation is one-way. It is not possible to reconstruct this information from the private hash.

PrivateHash = MD5Hash( TimeStamp : IP Address : Private key)


In the source code generating the private hash is handled by the method Encode in the PrivateHashEncoder class. It uses the MD5Encoder class to actually generate the MD5 hash.
public string Encode(string dateTimeInMilliSecondsString,
    string ipAddress)
{
  string stringToEncode = string.Format(
     "{0}:{1}:{2}",
     dateTimeInMilliSecondsString,
     ipAddress,
     privateKey);
  return md5Encoder.Encode(stringToEncode);
}

Validating the Nonce

Every time the client send the nonce to the server, the server validates if this is the nonce that the server sends to the client. The server validates the nonce in two steps:

The first thing that this implementation on the server does is validate if this PrivateHash was generated by this server and returned to this client. The server does this by generating the PrivateHash with the time stamp that is available in the Nonce and the IP address of the client. If this does not deliver the same PrivateHash as in the nonce from the client, the nonce is incorrect and the server response with a 401. The NonceValidator is responsible in the source code for validating this nonce.

public virtual bool Validate(string nonce,
   string ipAddress)
{
   string[] decodedParts = GetDecodedParts(nonce);
   string md5EncodedString = privateHashEncoder.Encode(
      decodedParts[0],
      ipAddress);
   return string.CompareOrdinal(
      decodedParts[1],
      md5EncodedString) == 0;
}

Secondly, the server checks if the Time Stamp is too old. The server holds a certain time-out for a nonce. For example, the time-out is 300 seconds. The server validates if the time stamp in the nonce is not older than 300 seconds. If the nonce is older than 300 seconds the server returns an indication in the HTTP header that the received nonce is too old together with a new nonce. RFC 2617 uses a special key called Stale in the header that is sets to true when the Nonce is too old. The NonceValidator is also responsible for checking if the time stamp is too old.

public virtual bool IsStale(string nonce)
{
   string[] decodedParts = GetDecodedParts(nonce);
   DateTime dateTimeFromNonce =
      nonceTimeStampParser.Parse(decodedParts[0]);
   return dateTimeFromNonce.AddSeconds(
      staleTimeOutInSeconds) < DateTime.UtcNow;
}

By using a time stamp and the IP address in the nonce, we make sure that the request is recent and comes from the client that requested the resource.

Quality Of Protection

Digest Authentication allows the server to ask which algorithm the client should use to encrypt the credentials of the user. Digest Authentication allows the following Quality Of Protection.

  • none = Default protection compatible with RFC 2069
  • auth = Increased protection that includes a client nonce and a client nonce counter
  • auth-int = Increased protection and integrity that included all of auth and a hash of the contents of the body

Note that it is a request from the server, the client itself is allowed to choose a lesser secure qop algorithm. If the server request for auth, it is ok for the client to start communicating with the default or none qop.

The implementation with this article supports both default/none and auth. The class DefaultDigestEncoder and the class AuthDigestEncoder implement the default and the auth type of quality of protection. Both classes derive from DigestEncodeBase which holds common functionality.

DigestEncoder

At runtime the server instantiate both type of encoders and stores them in a dictionary with the qop algorithm as the key. This enables the server to easily switch between different type of encoders at runtime.

internal class DigestEncoders :
   Dictionary
{
 public DigestEncoders(MD5Encoder md5Encoder)
 {
  Add(DigestQop.None, new DefaultDigestEncoder(md5Encoder));
  Add(DigestQop.Auth, new AuthDigestEncoder(md5Encoder));
 }

 public virtual DigestEncoderBase GetEncoder(DigestQop digestQop)
 {
  return this[digestQop];
 }
}

None or default QOP

When an internet browser receives 401 HTTP status code with Digest in the authentication header it will show a dialog for entering the username and password. When the client uses the default qop which is compatible with RFC 2069, the client encrypts the user name and password as follows.

HA1 = MD5( username : realm : password)


HA2 = MD5( method : digestURI)


response = MD5( HA1 : nonce : HA2)

A MD5 hash is created from the user name, realm and password, a separate MD5 hash is created from the HTTP method and the URI of the resource that the client requests. The response is created through a MD5 hash that combines the previous two MD5 hashes and the server generated nonce. The DigestEncoderBase class holds the functionality to generate both the HA1 en HA2 hashes.

private string CreateHa1(DigestHeader digestHeader,
   string password)
{
  return md5Encoder.Encode(
    string.Format(
    "{0}:{1}:{2}",
    digestHeader.UserName,
    digestHeader.Realm,
    password));
}

private string CreateHa2(DigestHeader digestHeader)
{
  return md5Encoder.Encode(
    string.Format(
    "{0}:{1}",
    digestHeader.Method,
    digestHeader.Uri));
}

The base classes AuthDigestEncoder and DefaultDigestEncoder are responsible for generating the response. This last step, generating the response, is what differs in the two derived classes. The response of the Auth algorithm should be generated different. The Auth algorithm includes a nonceCount and a client generated Nonce in the response. Also the actual qop string is concatenated before the hash is calculated.

response = MD5( HA1 : nonce : nonceCount : clientNonce : qop : HA2)

This is why the Auth algorithm is more secure than Default, the server performs an additional check to see if the nonceCount is incremented by the client with every request. The CreateResponse method of the AuthDigestEncoder generates the Auth response.

public override string CreateResponse(
   DigestHeader digestHeader,
   string ha1,
   string ha2)
{
  return
   md5Encoder.Encode(
     string.Format(
     "{0}:{1}:{2}:{3}:{4}:{5}",
     ha1,
     digestHeader.Nonce,
     digestHeader.NounceCounter,
     digestHeader.Cnonce,
     digestHeader.Qop.ToString(),
     ha2));
}

Extending WCF REST

To be able to integrate Digest Authentication with WCF REST, the WCF REST framework has to be extended. This is done by creating a custom RequestInterceptor. For more information take a look at my previous article on CodeProject which explains this extension in more detail.

Retrieving and Storing user credentials

The password of the user is transmitted as part of the response generated by the client to the server. It is not possible for the server to extract the password from the response. The server generates a response and checks if the response is equal to the response that was given by the client. This means that there are two options for storing and retrieval of user credentials using Digest Authentication.

  • The first and most secure option is for every user to store the HA1 key in the credentials data storage and validate using the stored HA1 key. This has a disadvantage because you have to change the HA1 key in the data storage if the username, password or realm is changes.
  • The second option is to store the password of the use in the credentials data storage in such a way that it is possible to retrieve the original password. This is obvious less secure than the first option.

Using the source code

If you want to secure your own WCF REST service with Basic Authentication using the provided source code, you need to execute the following steps:

  • Add a reference to the DigestAuthenticationUsingWCF assembly
  • Create a custom membership provider derived from MembershipProvider
  • Implement the ValidateUser method against your back-end security storage
  • Create a custom membership user derived from Membership user
  • Implement the GetUser method against your back-end security storage
  • Create a custom DigestAuthenticationHostFactory, see the example in the provided source code
  • Add the new DigestAuthenticationHostFactory to the markup of the .svc file

Points of Interest

The provided sourcecode is developed using TDD and uses the NUnit framework for creating and executing tests. Rhino mocks is used as a mocking framework inside the unit tests.

History

  • 28th Feb, 2011
    • Initial post
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"