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:

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. ↩︎


Articles from blogs I follow around the net

Simpler encapsulation with immutability

A worked example. I've noticed that many software organizations struggle with encapsulation with 'bigger' problems. It may be understandable and easily applicable to define a NaturalNumber type or ensure that a minim…

via ploeh blog June 12, 2024

Update on our infrastructure plans

Now that Drew has provided an update on the general state of SourceHut, I would like to follow up with one focusing on our infrastructure. Much has happened under the hood, a lot has not happened, and plans come and go as reality changes its mind every now a…

via Blogs on Sourcehut June 12, 2024

OpenBSD extreme privacy setup

# Introduction This blog post explains how to configure an OpenBSD workstation with extreme privacy in mind. This is an attempt to turn OpenBSD into a Whonix or Tails alternative, although if you really need that level of privacy, use a system from this…

via Solene'% June 10, 2024

Generated by openring