Custom JWT Authentication with F# and ASP.NET

At my $CURRENT_JOB we are working on introducing a new back-end service, and as usual, teams entirely composed of new-ish employees face some hard time discovering all the small pieces required to make the gears turn.

This time the challenge was to implement the authentication layer. It is actually quite simple as it is just a regular JWT token, but the devil's in the details:

  • the token is on a custom header called `x-jwt-payload`
  • the token does not contain the alg attribute
  • the validation is done internally at the reverse proxy level

OK, this doesn't sound too bad. However, it does take some tools from our hands... ASP.NET has the UseJwtBearerAuthentication middleware that would take care of this workflow for us, but this requires access to the Authority1 server which we don't have, and also requires the alg attribute to decode the token.

Having said that, let's develop another middleware to take of our authentication. I tried to reach the official documentation on how to write a custom authentication scheme for ASP.NET but it was less than useless. Then I tried to reach for blog posts, Stack Overflow questions and open source projects, but they all seemed so convoluted for such a small feature... When I was almost going to brute force the solution out of my IDE through auto completion and debugging, this answer appeared!

That's it! This is what I needed, a really concise example going through each step of the authentication workflow. I wonder why Microsoft doesn't have something like this on their docs. Or at least not something easy to find there.

Alright, time to implement piece by piece of this code. Starting with the Authentication scheme definition:

type CustomJwtAuthenticationOptions() =
    inherit AuthenticationSchemeOptions()

    member this.DefaultScheme = "CustomJwtAuthentication"
    member this.HeaderName = "x-jwt-payload"

The next missing part is the Authentication Handler. For this, I'll use the great FsToolkit.ErrorHandling package to help structure the code, so do a dotnet add package FsToolkit.ErrorHandling.

type CustomJwtAuthenticationHandler
    (
        options: IOptionsMonitor<CustomJwtAuthenticationOptions>,
        logger: ILoggerFactory,
        encoder: UrlEncoder,
        clock: ISystemClock
    ) =
    inherit AuthenticationHandler<CustomJwtAuthenticationOptions>(options, logger, encoder, clock)

    override this.HandleAuthenticationAsync() =
        result {
            let! token = this.RetrieveTokenValue this.Options.HeaderName
            let! jwt = this.DecodeToken token

            let name =
                let firstName =
                    jwt.Item("firstName") |> string
                let lastName =
                    jwt.Item("lastName") |> string

                $"{firstName} {LastName}"

            let claims =
                [ Claim(ClaimTypes.NameIdentifier, jwt.Sub)
                  Claim(ClaimTypes.Name, name) ]

            let claimIdentity =
                ClaimsIdentity(claims, this.Options.DefaultSchemeName)

            let ticket =
                AuthenticationTicket(
                    ClaimsPrincipal(claimsIdentity),
                    AuthenticationProperties(),
                    this.Options.DefaultSchemeName
                )

            return Task.FromResult(AuthenticateResult.Success(ticket))
        }
        |> function
            | Ok value -> value
            | Error e -> Task.FromResult(AuthenticateResult.Fail(e))

And that's it! I now have the custom JWT authentication I needed for my ASP.NET application. Of course, we are missing some helper methods I used on the code. Let's take a look at them.

This function is used to extract the Base 64 token from the header.

member private this.RetrieveTokenValue name =
    let found, value =
        this.Request.Headers.TryGetValue(name)

    if not found then
        Error $"Missing header '{name}'"
    else
        value.ToString()
        |> String.IsNullOrWhiteSpace
        |> function
            | false -> Ok value
            | true -> Error $"Missing header '{name}' value"

Now the function responsible to decode the JWT token itself.

member private this.DecodeToken token =
    try
        let jwt =
            token
            |> Convert.FromBase64String
            |> Encoding.UTF8.GetString
            |> Jwt.JwtPayload.Deserialize

        Ok jwt
    with
    | exn -> Error $"Error decoding token: {exn.Message}"

OK, now we have everything needed to use our brand new authentication scheme. How can we plug this together on our application's startup? Considering that we're using Saturn to configure it, it would look just like this:

let configureApp (app: IApplicationBuilder) =
    app.UseAuthentication()

let configureServices (services: IServiceCollection) =
    services
        .AddAuthentication(
            CustomJwtAuthenticationOptions().DefaultScheme
        )
        .AddScheme<CustomJwtAuthenticationOptions, CustomJwtAuthenticationHandler>(
            CustomJwtAuthenticationOptions().DefaultScheme, (fun options -> ())
        )
    |> ignore

    services

let main _ =
    let app =
        application {
            // ...
            app_config configureApp
            service_config configureServices
        }
        run app
  1. The address of the token-issuing authentication server. The JWT bearer authentication middleware will use this URI to find and retrieve the public key that can be used to validate the token’s signature. It will also confirm that the iss parameter in the token matches this URI.