Building Signature Authentication for ASP.NET Core: Part I

January 27, 2020

Introduction

One of the impressive things about ASP.NET Core is how much functionality comes out of the box. There are a number of authentication mechanisms that you can use that are part of ASP.NET Core and supported by Microsoft. However, the bulk of what’s implemented there is oriented around OAuth and specifically IdentityServer4.

IdentityServer is also impressive. It supports deep customization - but again, mostly oriented around OAuth. It’s also very complicated and difficult to configure correctly, particularly if you’re doing anything custom at all.

What’s also missing from the standard distribution of ASP.NET Core is any kind of authentication mechanism (other than Basic auth) that isn’t federated or delegated. Signature authentication in particular is a useful way to authenticate requests to an API that doesn’t involve a user interaction. One might say you could use Client Credentials grant supported by existing OAuth2.0 implementations like IdentityServer4, but for a simple non-interactive authentication mechanism I think that’s overkill. Instead, I think we can leverage the well-designed extensibility points in ASP.NET Core to build a custom implementation of known signature authentication schemes.

Note: you’ll need to already be somewhat familiar with ASP.NET Core to make much sense of this. This would probably be considered intermediate level material.

The Scheme

The scheme I’ll build in this post is the Cavage HTTP Signatures scheme, described in an RFC Draft (draft 12).

The summary of this scheme is that the client adds certain headers to the HTTP request and then uses the data in those headers to construct a string to be signed by the client’s key, which is a shared secret. The signature is then transmitted as part of the request, and the server can validate that signature using the shared secret. There are many details I’m glossing over here, but if you understand this description then the details should be pretty easy to grasp later.

Extending ASP.NET Core

Microsoft’s web framework has a tremendous degree of extensibility - more than most people realize, I think, and certainly more than you might realize if you don’t read the documentation correctly. You might check out one of my previous posts on authorization for evidence of that.

Fortunately, in this case the documentation gives us a pretty decent roadmap. The main interface we need to implement is IAuthenticationHandler. Before we start, though, take notice of something else: there’s a default implementation called AuthenticationHandler<TOptions>. This implementation will provide sensible defaults for everything except what’s specific to our code, so that will make a better starting place than implementing IAuthenticationHandler from scratch.

You should also notice that this default implementation can be specialized to the TOptions class, which we will define to suit our needs - and also has a default implementation available in AuthenticationSchemeOptions. Finally, in order to use our implementation in an ASP.NET Application, we will need to set up authentication in the ConfigureServices() and Configure() methods used in the application startup.

Now, with this outline, we can go ahead and write the beginnings of our types and wire them up. Having them set up from the beginning will help us with testing.

namespace BitThicket.SignatureAuthentication
open Microsoft.AspNetCore.Authentication

// start with something exceedingly basic
type SignatureAuthenticationOptions() =
  inherit AuthenticationSchemeOptions()
    
type SignatureAuthenticationHandler(opts, log, enc, clk) =
  inherit AuthenticationHandler<SignatureAuthenticationOptions>(options, loggerFactory, encoder, clock)

Now we can write tests for these. They won’t work at first, but then we can implement the functionality we need until the tests work. We’ll want unit tests for various parts of the implementation, but rather than focus on the guts of the signature scheme (which is what most of the unit tests would be focused on), I think integration tests will better serve our purposes here. Fortunately ASP.NET Core ships with a perfectly-suited test server. I configure my basic integration test like this:

open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.TestHost
open FSharp.Control.Tasks.V2.ContextInsensitive
open Divergic.Logging.Xunit
open Swensen.Unquote
open Xunit
open Xunit.Abstractions

open OurNamespace

type IntegrationTests(output:ITestOutputHelper) =
  [<Fact>]
  [<Trait("Category", "Integration")>]
  member __.``signature auth failure against bare request delegate``() = task {
    let builder =
      WebHostBuilder()
      .ConfigureServices(
        fun services ->
          services
            .AddAuthentication("Signature")
            .AddScheme<SignatureAuthenticationOptions, SignatureAuthenticationHandler>("Signature",
              fun (opts:SignatureAuthenticationOptions) ->
                opts.Realm <- "Test")
            |> ignore)
      .ConfigureLogging(
        fun logging ->
          logging
            .AddFilter(fun _ -> true)
            .AddXunit(output)
          |> ignore)
      .Configure(
        fun app ->
          app.UseAuthentication() |> ignore
          app.Run(
            fun context -> task {
              if Seq.isEmpty context.User.Claims then
                context.Response.StatusCode <- 401
                context.Response.Headers.["WWW-Authenticate"] <- StringValues("Signature")
              else
                do! context.Response.WriteAsync("Hello World")
            } :> Task))
    
    use server = new TestServer(builder)
    
    let! response = server.CreateClient().GetAsync("/")
    test <@ response.StatusCode = HttpStatusCode.Unauthorized @>
  }

That’s it! We’ve fully configured a very simple ASP.NET Core application that uses our signature authentication scheme.