The most important thing to consider when developing an API that will be exposed over the Internet is to ensure its security. In this article, I will take you through the HMAC authentication mechanism and provide the source code sample for securing an ASP.NET Web API using HMAC.
What Is HMAC?
The abbreviation of HMAC is Hash base Message Authentication Code. The HMAC implementation will ensure the following when a request is received from a client to the Web API:
- Data integrity: The data sent by the client is intact and not tampered.
- Request origination: The request comes from a trusted client.
- Not a replay request: The request is not captured by an intruder and being replayed.
The preceding points address the most critical security vulnerabilities that an API service can expose.
How It Works
HMAC basically works with a shared secret key between the client and the server. The client does a cryptographic hashing on a set of parameters such as client ID, request method, request URL, a nonce value, timestamp, and the actual content using the secret key. In the request authorization header, a few values to be used in the client side comparison like nonce, timestamp, and so forth, are separated by a special character and sent in plain text. It should be noted that this concatenated string should not contain any values that only the client and server should know. Following is the sequence of steps followed on the server to identify whether or not the request is valid.
- Client ID is validated.
- Nonce value and the timestamp are used in identifying if it is a replay request.
- The secret key is fetched for the given client ID and using all other request parameters (like nonce, content, timestamp, method, URL, and the like), the HMAC string is generated.
- The HMAC string generated on the server is compared on the server.
The .NET framework has the built-in class for HMAC. The HMAC classes support both SHA1 and MD5 cryptographic hashing. Also, the SHA1 based HMAC classes provide flexibility to increase the hashing bits like 160, 256, or 512.
Source Code Sample
In this section, let us look at a sample console client and ASP.NET Web API applications that implement HMAC authentication.
Create a console application and add the following code in the Program.cs file. The HttpClient should be passed on with a custom DelegatingHandler.
static void Main(string[] args) { Portfolio portfolio = new Portfolio() { Index = 1, Symbol = "MSFT", CompanyName = "Microsoft" }; HmacClientHandler hmacClientHandler = new HmacClientHandler(); HttpClient client = HttpClientFactory. Create(hmacClientHandler); var response = client.PostAsJsonAsync ("http://localhost:26227/api/Portfolio/", portfolio).Result; if (response.IsSuccessStatusCode) Console.WriteLine("Success"); else Console.WriteLine(response.ReasonPhrase); Console.ReadLine(); }
The PostAsJsonAsync method will be available only after you include the Microsoft.AspNet.WebApi.Client from NuGet packages. Add a custom delegating handler with the code below.
internal class HmacClientHandler : DelegatingHandler { private string _applicationId = "XYZ-123"; private string _secretKey = "MYSECRETKEY1"; protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { HttpResponseMessage response = null; string url = Uri.EscapeUriString(request. RequestUri.ToString().ToLowerInvariant()); string methodName = request.Method.Method; string timeStamp = DateTime.UtcNow.ToString(); string nonce = Guid.NewGuid().ToString(); byte[] content = await request.Content. ReadAsByteArrayAsync(); MD5 md5 = MD5.Create(); byte[] contentMd5Hash = md5.ComputeHash(content); string contentBase64String = Convert.ToBase64String(contentMd5Hash); //Formulate the keys used in plain format as //a concatenated string. //Note that the secret key is not provided here. string authenticationKeyString = string.Format("{0}{1}{2}{3}{4}{5}", _applicationId, url, methodName, timeStamp, nonce, contentBase64String); var secretKeyBase64ByteArray = Convert.FromBase64String(_secretKey); using (HMACSHA512 hmac = new HMACSHA512(secretKeyBase64ByteArray)) { byte[] authenticationKeyBytes = Encoding.UTF8.GetBytes(authenticationKeyString); byte[] authenticationHash = hmac.ComputeHash(authenticationKeyBytes); string hashedBase64String = Convert.ToBase64String(authenticationHash); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("hmac", string.Format("{0}:{1}:{2}:{3}", _applicationId, ashedBase64String, nonce, timeStamp)); } response = await base.SendAsync(request, cancellationToken); return response; } }
Create a Web API and add a Portfolio API controller. Once done, run the client application and you should see the post happening successfully. There is no validation done on the server ☺.
Now, let us go and add an AuthenticationFilter in the ASP.NET Web API that will do the HMAC authentication on the server side. Here is the code.
public class HmacAuthenticationAttribute : Attribute, IAuthenticationFilter { private string _applicationId = "XYZ-123"; private string _secretKey = "MYSECRETKEY1"; public bool AllowMultiple { get { return false; } } public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) { var request = context.Request; if (!IsValidRequest(request).Result) { context.ErrorResult = new UnauthorizedResult(new AuthenticationHeaderValue[0], context.Request); } return Task.FromResult(0); } private async Task<bool> IsValidRequest(HttpRequestMessage request) { if (request.Headers.Authorization == null || !request.Headers.Authorization.Scheme.Equals("hmac", StringComparison.OrdinalIgnoreCase) || String.IsNullOrEmpty(request.Headers. Authorization.Parameter)) return false; string tokenString = request.Headers.Authorization.Parameter; string[] authenticationParameters = tokenString.Split(new char[] { ':' }); if (authenticationParameters.Length != 4) return false; //This should actually be validated against a master set //of app ids since there will be multiple clients. //Doing a straight comparison for simplicity if (!authenticationParameters[0].Equals(_applicationId, StringComparison.OrdinalIgnoreCase)) return false; //Check for ReplayRequests starts here //Check if the nonce is already used if (MemoryCache.Default.Contains(authenticationParameters[2])) return false; //Check if the maximum allowed request time gap is exceeded, //set to 10 mins in this example if ((DateTime.UtcNow - Convert.ToDateTime(authenticationParameters[3])). Seconds > 600) return false; //Add the nonce to the cache MemoryCache.Default.Add(authenticationParameters[2], authenticationParameters[3], DateTimeOffset.UtcNow.AddSeconds(600)); //Check for ReplayRequests ends here string reformedAuthenticationToken = String.Format("{0}{1}{2}{3}{4}{5}", _applicationId, request.Method.Method, HttpUtility.UrlEncode(request.RequestUri.ToString().ToLowerInvariant()), authenticationParameters[2], authenticationParameters[3], await GetContentBase64String(request.Content)); //Each AppId should have be configured with a unique shared key on //the server!!! Hard coded in the example for simplicity. var secretKeyBytes = Convert.FromBase64String(_secretKey); var authenticationTokenBytes = Encoding.UTF8.GetBytes(reformedAuthenticationToken); //Check fo data integrity and tampering using (HMACSHA512 hmac = new HMACSHA512(secretKeyBytes)) { var hashedBytes = hmac.ComputeHash(authenticationTokenBytes); string reformedTokenBase64String = Convert.ToBase64String(hashedBytes); if (!authenticationParameters[1].Equals(reformedAuthenticationToken, StringComparison.OrdinalIgnoreCase)) return false; } return true; } private async Task<string> GetContentBase64String(HttpContent content) { using (MD5 md5 = MD5.Create()) { var bytes = await content.ReadAsByteArrayAsync(); var md5Hash = md5.ComputeHash(bytes); return Convert.ToBase64String(md5Hash); } } public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) { context.Result = new ResultWithChallenge(context.Result); eturn Task.FromResult(0); } } public class ResultWithChallenge : IHttpActionResult { private readonly IHttpActionResult next; public ResultWithChallenge(IHttpActionResult next) { this.next = next; } public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { var response = await next.ExecuteAsync(cancellationToken); if (response.StatusCode == HttpStatusCode.Unauthorized) { response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("hmac")); } return response; } }
Done! All you need to do now is to decorate the Action method with the HmacAuthentication attribute. Your ASP.NET Web API is now secure.
I hope this article gave you the knowledge needed for you to implement HMAC authorization in a Web API. Happy reading!