SquareWidget HMAC Middleware

The SquareWidget.HMAC.Server.Core and SquareWidget.HMAC.Client.Core NuGet packages take the pain out of service-to-service authentication middleware in ASP.NET Core 6.0. The packages compute a hash-based message authentication code (HMAC) using the SHA256 hash function in the System.Security.Cryptography namespace.  Skip down to Quick Start if you’re familiar with authentication middleware and want to dive on in.

For Beginners

The problem is simple enough. Alice wants to send a love note to Bob. However Eve is jealous and keeps trying to get in the middle of the two so she can cause trouble. Last month Eve managed to pass herself off as Alice and told Bob that he was a jerk and she was going to break up with him.

Alice and Bob have had enough. So they agree to exchange a secret key just between the two of them. Alice will use her copy of the shared key along with the current time as a nonce to create a one-way hash. She then sends the hash to Bob along with the timestamp she used:

Bob gets both the hash and the timestamp. Then he repeats the process exactly, using his own copy of the shared key to create a one-way hash of Alice’s timestamp. If his own hash and Alice’s hash match then he knows it’s her. If they do not match then he knows that Eve is up to her old tricks again.

The source code for both Alice (client) and Bob (server) is checked into GitHub here:

https://github.com/jamesstill/SquareWidget.HMAC.Client.Core

https://github.com/jamesstill/SquareWidget.HMAC.Server.Core

There is also a sample API and UI that exercises both packages and illustrates how to use them end-to-end:

https://github.com/jamesstill/SquareWidget.HMAC.Sample

Quick Start

Server

Create a new ASP.NET Core 6 Web API and install the SquareWidget.HMAC.Server.Core NuGet Package:

Install-Package SquareWidget.HMAC.Server.Core -Version 6.0.0

Create a class that implements SharedSecretStoreService. You need to fetch the shared secret for the ClientID from a data store. It could be Azure Key Vault, IdentityServer4, Azure Table Storage, SQL Azure, or even a local appsettings.json file if your needs are simple. Here I’m just returning a hard-coded value:

using SquareWidget.HMAC.Server.Core;
using System.Threading.Tasks;

namespace SampleApi
{
    public class TestSharedSecretStoreService : SharedSecretStoreService
    {
        public override Task<string> GetSharedSecretAsync(string clientId)
        {
            return Task.Run(() => "P@ssw0rd");
        }
    }
}

Next you need to register the service in your Startup.cs class:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services
        .AddAuthentication(HmacAuthenticationDefaults.AuthenticationScheme)
        .AddHmacAuthentication<TestSharedSecretStoreService>(o => { });
}

There are three configuration options available:

  • HashHeaderName
  • TimestampHeaderName
  • ReplayAttackDelayInSeconds

HashHeaderName

This is the name of the request header where the server expects the hash to be located. It defaults to “Hash” but you can change it if necessary.

TimestampHeaderName

This is the name of the request header where the server expects the timestamp to be located.  It defaults to “Timestamp” but you can change it if necessary.

ReplayAttackDelayInSeconds

This is the value in seconds that the server will allow as a threshold for the client timestamp  (UTC). It defaults to 15 seconds. If the current datetime is greater than the client datetime by more than this value then authentication fails with a 401 status code.

The purpose of this value is to discourage a replay attack from a third-party who was able to obtain the request headers from the client. It goes without saying but you should always run under SSL/TLS to protect against packet sniffing from a man in the middle.

There is an important caveat here. The clocks on the calling client and the receiving server both need to be synced up to time servers with regularity. If one or both are allowed to drift then the server could return a 401 even if the request is received well within the threshold period. This will not be a problem if both client and server are hosted on Azure or they sync up to a common domain controller.

Any or all of these configuration options may be overridden in the Startup.cs class:

services
    .AddAuthentication(HmacAuthenticationDefaults.AuthenticationScheme)
    .AddHmacAuthentication<TestSharedSecretStoreService>(o => 
    {
        o.ReplayAttackDelayInSeconds = 60;
        o.HashHeaderName = "MyHashHeaderName";
        o.TimestampHeaderName = "MyTimestampHeaderName";
    });

The last step is to decorate all controllers you want to secure with the AuthorizeAttribute:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace SampleApi.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class WidgetsController : ControllerBase

Client

Create a new ASP.NET Core 6 application and install the SquareWidget.HMAC.Client.Core NuGet Package:

Install-Package SquareWidget.HMAC.Client.Core -Version 6.0.0

Then you simply instantiate a HmacHttpClient and pass the Client ID and shared secret into it when making web requests:

public IActionResult Index()
{
    var baseAddress = "https://localhost:XXXX";
    var credentials = new ClientCredentials
    {
        ClientId = "TestClient",
        ClientSecret = "P@ssw0rd"
    };

    using (var client = new HmacHttpClient(baseAddress, credentials))
    {
        var requestUri = "api/widgets";
        var widgets = _client.Get<List<Widget>>(requestUri).Result;
        return View(widgets);
    }
}

The HmacHttpClient class will hash the current UTC timestamp and put the values in the request header. Since authentication happens automatically on each request this frees you up to concentrate on the business logic.