In this article I will explain the concepts behind HMAC authentication and will show how to write an example implementation for ASP.NET Web API using message handlers. The project will include both server and client side (using Web API's HttpClient) bits.
HMAC (hash-based message authentication code) provides a relatively simple way to authenticate HTTP messages using a secret that is known to both client and server. Unlike basic authentication it does not require transport level encryption (HTTPS), which makes its an appealing choice in certain scenarios. Moreover, it guarantees message integrity (prevents malicious third parties from modifying contents of the message).
On the other hand proper HMAC authentication implementation requires slightly more work than basic HTTP authentication and not all client platforms support it out of the box (most of them support cryptographic algorithms required to implement it though). My suggestion would be to use it only if HTTPS + basic authentication does not suit your requirements.
One prominent example of HMAC usage is Amazon S3 service.
The basic idea behind HMAC authentication in HTTP can be described as follows:
As you can see the secret key (eg. password hash) is only shared between client and server once (eg. during user registration). Noone will be able to produce a valid signature without the access to the secret also any modification of the message (eg. appending content) will result in server calculating a different signature and refusing authorization.
Broadly speaking to create a HMAC authenticated client/server pair using ASP.NET Web API we need:
Ok, so let's start by writing the first piece.
public interface IBuildMessageRepresentation { string BuildRequestRepresentation(HttpRequestMessage requestMessage); }
public class CanonicalRepresentationBuilder : IBuildMessageRepresentation { /// <summary> /// Builds message representation as follows: /// HTTP METHOD\n + /// Content-MD5\n + /// Timestamp\n + /// Username\n + /// Request URI /// </summary> /// <returns></returns> public string BuildRequestRepresentation(HttpRequestMessage requestMessage) { bool valid = IsRequestValid(requestMessage); if (!valid) { return null; } if (!requestMessage.Headers.Date.HasValue) { return null; } DateTime date = requestMessage.Headers.Date.Value.UtcDateTime; string md5 = requestMessage.Content == null || requestMessage.Content.Headers.ContentMD5 == null ? "" : Convert.ToBase64String(requestMessage.Content.Headers.ContentMD5); string httpMethod = requestMessage.Method.Method; //string contentType = requestMessage.Content.Headers.ContentType.MediaType; if (!requestMessage.Headers.Contains(Configuration.UsernameHeader)) { return null; } string username = requestMessage.Headers .GetValues(Configuration.UsernameHeader).First(); string uri = requestMessage.RequestUri.AbsolutePath.ToLower(); // you may need to add more headers if thats required for security reasons string representation = String.Join("\n", httpMethod, md5, date.ToString(CultureInfo.InvariantCulture), username, uri); return representation; } private bool IsRequestValid(HttpRequestMessage requestMessage) { //for simplicity I am omitting headers check (all required headers should be present) return true; } }
A couple of points worth mentioning:
Now lets look at that component that will calculate authentication code (signature).
public interface ICalculteSignature { string Signature(string secret, string value); }
public class HmacSignatureCalculator : ICalculteSignature { public string Signature(string secret, string value) { var secretBytes = Encoding.UTF8.GetBytes(secret); var valueBytes = Encoding.UTF8.GetBytes(value); string signature; using (var hmac = new HMACSHA256(secretBytes)) { var hash = hmac.ComputeHash(valueBytes); signature = Convert.ToBase64String(hash); } return signature; } }
The signature will be encoded using base64 so that we can pass it easily in a header. What header you may ask? Well, unfortunately there is no standard way of including message authentication codes into the message (as there is no standard way of constructing message representation). We will use Authorization HTTP header for that purpose providing a custom schema (ApiAuth).
Authorization: ApiAuth HMAC_SIGNATURE
The HMAC will be calculated and attached to the request in a custom message handler.
public class HmacSigningHandler : HttpClientHandler { private readonly ISecretRepository _secretRepository; private readonly IBuildMessageRepresentation _representationBuilder; private readonly ICalculteSignature _signatureCalculator; public string Username { get; set; } public HmacSigningHandler(ISecretRepository secretRepository, IBuildMessageRepresentation representationBuilder, ICalculteSignature signatureCalculator) { _secretRepository = secretRepository; _representationBuilder = representationBuilder; _signatureCalculator = signatureCalculator; } protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { if (!request.Headers.Contains(Configuration.UsernameHeader)) { request.Headers.Add(Configuration.UsernameHeader, Username); } request.Headers.Date = new DateTimeOffset(DateTime.Now,DateTime.Now-DateTime.UtcNow); var representation = _representationBuilder.BuildRequestRepresentation(request); var secret = _secretRepository.GetSecretForUser(Username); string signature = _signatureCalculator.Signature(secret, representation); var header = new AuthenticationHeaderValue(Configuration.AuthenticationScheme, signature); request.Headers.Authorization = header; return base.SendAsync(request, cancellationToken); } }
public class Configuration { public const string UsernameHeader = "X-ApiAuth-Username"; public const string AuthenticationScheme = "ApiAuth"; }
public class DummySecretRepository : ISecretRepository { private readonly IDictionary<string, string> _userPasswords = new Dictionary<string, string>() { {"username","password"} }; public string GetSecretForUser(string username) { if (!_userPasswords.ContainsKey(username)) { return null; } var userPassword = _userPasswords[username]; var hashed = ComputeHash(userPassword, new SHA1CryptoServiceProvider()); return hashed; } private string ComputeHash(string inputData, HashAlgorithm algorithm) { byte[] inputBytes = Encoding.UTF8.GetBytes(inputData); byte[] hashed = algorithm.ComputeHash(inputBytes); return Convert.ToBase64String(hashed); } } public interface ISecretRepository { string GetSecretForUser(string username); }
In a real life scenario you could retrieve the hashed password from the a persistent store (a database). If you remember how we constructed our message representation you will notice that we also need to set content MD5 header. We could do it in HmacSigningHandler, but to have separation of concerns and because Web API allows us to combine handlers in a neat way I moved it to a separate (dedicated) handler.
public class RequestContentMd5Handler : DelegatingHandler { protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { if (request.Content == null) { return await base.SendAsync(request, cancellationToken); } byte[] content = await request.Content.ReadAsByteArrayAsync(); MD5 md5 = MD5.Create(); byte[] hash = md5.ComputeHash(content); request.Content.Headers.ContentMD5 = hash; var response = await base.SendAsync(request, cancellationToken); return response; } }
For simplicity the HMAC handler derives directly from HttpClientHandler. Here is how we would make a request:
static void Main(string[] args) { var signingHandler = new HmacSigningHandler(new DummySecretRepository(), new CanonicalRepresentationBuilder(), new HmacSignatureCalculator()); signingHandler.Username = "username"; var client = new HttpClient(new RequestContentMd5Handler() { InnerHandler = signingHandler }); client.PostAsJsonAsync("http://localhost:48564/api/values","some content").Wait(); }
And that's basically it as far as http client is concerned. Let's have a look at server part.
The general logic will be that we will want to authenticate every incoming request (we can us per route handlers to secure only one route for example). Each request's authentication code will be calculated using the very same IBuildMessageRepresentation and ICalculateSignature implementations. If the signature does not match (or the content md5 hash is different from the value in the header) we will immediately return a 401 response.
public class HmacAuthenticationHandler : DelegatingHandler { private const string UnauthorizedMessage = "Unauthorized request"; private readonly ISecretRepository _secretRepository; private readonly IBuildMessageRepresentation _representationBuilder; private readonly ICalculteSignature _signatureCalculator; public HmacAuthenticationHandler(ISecretRepository secretRepository, IBuildMessageRepresentation representationBuilder, ICalculteSignature signatureCalculator) { _secretRepository = secretRepository; _representationBuilder = representationBuilder; _signatureCalculator = signatureCalculator; } protected async Task<bool> IsAuthenticated(HttpRequestMessage requestMessage) { if (!requestMessage.Headers.Contains(Configuration.UsernameHeader)) { return false; } if (requestMessage.Headers.Authorization == null || requestMessage.Headers.Authorization.Scheme != Configuration.AuthenticationScheme) { return false; } string username = requestMessage.Headers.GetValues(Configuration.UsernameHeader) .First(); var secret = _secretRepository.GetSecretForUser(username); if (secret == null) { return false; } var representation = _representationBuilder.BuildRequestRepresentation(requestMessage); if (representation == null) { return false; } if (requestMessage.Content.Headers.ContentMD5 != null && !await IsMd5Valid(requestMessage)) { return false; } var signature = _signatureCalculator.Signature(secret, representation); var result = requestMessage.Headers.Authorization.Parameter == signature; return result; } protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { var isAuthenticated = await IsAuthenticated(request); if (!isAuthenticated) { var response = request .CreateErrorResponse(HttpStatusCode.Unauthorized, UnauthorizedMessage); response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue( Configuration.AuthenticationScheme)); return response; } return await base.SendAsync(request, cancellationToken); } }
The bulk of work is done by IsAuthenticated() method. Also please note that we do not sign the response, meaning the client will not be able verify the authenticity of the response (although response signing would be easy to do given components that we already have). I have omitted IsMd5Valid()method for brevity, it basically compares content hash with MD5 header value (just remember not to compare byte[] arrays using == operator).
Configuration part is simple and can look like that (per route handler):
config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", constraints: null, handler: new HmacAuthenticationHandler(new DummySecretRepository(), new CanonicalRepresentationBuilder(), new HmacSignatureCalculator()) { InnerHandler = new HttpControllerDispatcher(config) }, defaults: new { id = RouteParameter.Optional } );
There is one very important flaw in the current approach. Imagine a malicious third party intercepts a valid (properly authenticated) HTTP request coming from a legitimate client (eg. using a sniffer). Such a message can be stored and resent to our server at any time enabling attacker to repeat operations performed previously by authenticated users. Please note that new messages still cannot be created as the attacker does not know the secret nor has a way of retrieving it from intercepted data.
To help us fix this issue lets make following three observations/assumptions about dates of requests in our system:
Once we know the above we can introduce following changes into IsAuthenticated() method:
protected async Task<bool> IsAuthenticated(HttpRequestMessage requestMessage) { //(...) var isDateValid = IsDateValid(requestMessage); if (!isDateValid) { return false; } //(...) //disallow duplicate messages being sent within validity window (5 mins) if(MemoryCache.Default.Contains(signature)) { return false; } var result = requestMessage.Headers.Authorization.Parameter == signature; if (result == true) { MemoryCache.Default.Add(signature, username, DateTimeOffset.UtcNow.AddMinutes(Configuration.ValidityPeriodInMinutes)); } return result; } private bool IsDateValid(HttpRequestMessage requestMessage) { var utcNow = DateTime.UtcNow; var date = requestMessage.Headers.Date.Value.UtcDateTime; if (date >= utcNow.AddMinutes(Configuration.ValidityPeriodInMinutes) || date <= utcNow.AddMinutes(-Configuration.ValidityPeriodInMinutes)) { return false; } return true; }
For simplicity I didn't test the example for sever and client residing in different timezones (although as long as we normalize the dates to UTC we should be save here).
The code is available as usually on bitbucket.
Hope this article helps some of you!
https://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/