How To: Implement HMAC Authentication On a RESTful WCF Service
There are several approaches in implementing authentication in RESTful WCF services. Using SSL and Digest Authentication are two options, while in this post I would like to demonstrate another approach which uses HMAC (Hash Message Authentication Code) in order to achieve both message authentication and integrity.
In this approach the server provides the client an ID and a secret key through some technique (e.g. sending an email containing the ID and the secret key) which will be used by the client to sign all his requests.
A secret key can be generated in the following way:
byte[] secretkey = new Byte[64];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetBytes(secretkey);
This post will not cover what is a RESTful service and how should we implement all it’s properties in WCF, for this purpose you can refer to this great article which I used in my research prior to this post.
In our example the service will provide specific user’s bookmarks to its consumer. In order to consume this service, a requester must provide some hashed message (in our case – the request Uri encrypted with the secret key) in the request header authorization property (different implementations of the client would be provided in future posts).
So without further ado's, this is the service contract method definition:
[WebGet(UriTemplate = "users/{username}/bookmarks?tag={tag}")]
[OperationContract]
Bookmarks GetUserBookmarks(string username, string tag);
The contract enforces us to use the specified Uri template which implies that for a specific username (the requested user) and specific tag filter, the requester will be provided with a bookmarks list matching this criteria.
Here is the service method implementation:
public Bookmarks GetUserBookmarks(string username, string tag)
{
WebOperationContext context = WebOperationContext.Current;
OutgoingWebResponseContext outgoingResponseContext =
context.OutgoingResponse;
bool isUserAuthenticated = IsUserAuthenticated(username);
if (isUserAuthenticated == false)
{
outgoingResponseContext.StatusCode = HttpStatusCode.Unauthorized;
return null;
}
outgoingResponseContext.StatusCode = HttpStatusCode.OK;
Bookmarks bookmarks =
new Bookmarks(BookmarksProvider.GetUserBookmarks(username, tag));
return bookmarks;
}
The simple implementation I used here suggests that the request is being checked for its user’s authentication: if the authentication fails, the requester will be responded with Unauthorized HTTP status code (401) and no bookmarks. If the authentication succeeds – the bookmarks list is provided along with OK HTTP status code (200).
I’ve skipped the BookmarksProvider.GetUserBookmarks method implementation due to it’s irrelevancy for this post subject.
Here is my implementation for the IsUserAuthenticated method:
private bool IsUserAuthenticated(string username)
{
WebOperationContext context = WebOperationContext.Current;
IncomingWebRequestContext incomingRequestContext =
context.IncomingRequest;
string requestUri =
incomingRequestContext.UriTemplateMatch.RequestUri.ToString();
string authorizationHeader =
incomingRequestContext.Headers[HttpRequestHeader.Authorization];
string authenticationToekn =
string.Format("{1}{0}{2}",
Delimeters.FirstClassDelimeter,
authorizationHeader,
requestUri);
byte[] userKey = GetUserKey(username);
bool isValidHash = Authentication.ValidateHash(
userKey,
authenticationToekn,
Delimeters.FirstClassDelimeter,
UTF8Encoding.UTF8);
return isValidHash;
}
This method composes an authentication token (this is a pure implementation step – it is up to the server to decide it’s protocol – and up to the client to obey in order to consume..) from the request Uri and the hashed authorization header. Then the authentication token is being validated along with the current requester secret key (fetched by the GetUserKey method) through the Authentication.ValidateHash method:
public static bool ValidateHash(byte[] key,
string message, string delimeter, Encoding encoding)
{
HMACMD5 hmacMD5 = new HMACMD5(key);
string[] messageParts =
message.Split(new string[] { delimeter },
StringSplitOptions.RemoveEmptyEntries);
if (messageParts == null || messageParts.Length < 2)
{
return false;
}
string encodedText = messageParts[0];
string text = messageParts[1];
byte[] textBytes = encoding.GetBytes(text);
byte[] computedHash = hmacMD5.ComputeHash(textBytes);
string computedHashString = Convert.ToBase64String(computedHash);
return encodedText.Equals(computedHashString);
}
In the validation method, the authentication token is being split to the message and hashed message parts – in our case, it’s the request Uri and the request Uri hashed using the requester’s secret key. After the parts are split, the secret key is being used to generate the hashed message (using the HMACMD5 class) again in order to be compared to the received hashed message. If the hashed messages are equal then the authentication is ok, otherwise it is invalid.
There are several ways to consume this service. In this post I’ll go over one of them in which a console application consumes our service:
byte[] key = GetUserKey();
string uri = "http://localhost:4609/ServiceHost/BookmarksService.svc/users/itai/bookmarks?tag=News";
HttpWebRequest request = WebRequest.Create(uri) as HttpWebRequest;
string encodedUri = Authentication.EncodeText(key, uri, UTF8Encoding.UTF8);
request.Headers[HttpRequestHeader.Authorization] = encodedUri;
HttpWebResponse response = request.GetResponse() as HttpWebResponse;
Stream bookmarksStream = response.GetResponseStream();
StreamReader reader = new StreamReader(bookmarksStream);
string str = reader.ReadToEnd();
reader.Close();
bookmarksStream.Close();
As you can see for yourself, this client is rather simple: prepare the request object using service Uri and define the authorization header using the client secret key to hash the message (in our case it is the request Uri), then send the request and parse the response (XML formatted).
Here is Authentication.EncodeText method implementation:
public static string EncodeText(byte[] key, string text, Encoding encoding)
{
HMACMD5 hmacMD5 = new HMACMD5(key);
byte[] textBytes = encoding.GetBytes(text);
byte[] encodedTextBytes =
hmacMD5.ComputeHash(textBytes);
string encodedText =
Convert.ToBase64String(encodedTextBytes);
return encodedText;
}
And here are our client results:
Summary
In this post I’ve implemented a RESTful WCF service which is protected by HMAC authentication token which is being passed in the service request header by a simple service consumer.
In my upcoming posts, I’ll review another different client implementations – stay tuned..
Hope you’ll find it useful.
References:
A Guide to Designing and Building RESTful Web Services with WCF 3.5
HMACMD5 at MSDN