Contents
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.
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.
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.
When the user enters the credentials in this dialog, the browser requests the resource from the server again. This time, the client adds additional information to the HTTP header regarding Digest Authentication.
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.
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 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.
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);
}
Every time the client sends 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 responds 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.
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 requests 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.
At runtime, the server instantiates both types of encoders and stores them in a dictionary with the qop algorithm as the key. This enables the server to easily switch between different types 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];
}
}
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)
|
An 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 and 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 differently. 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));
}
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.
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 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 obviously less secure than the first option.
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
The provided source code 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.