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

The four tenets of SOA revisited

Twenty years after. In the January 2004 issue of MSDN Magazine you can find an article by Don Box titled A Guide to Developing and Running Connected Systems with Indigo. Buried within the (now dated) discussion of the technology…

via ploeh blog March 4, 2024

Building a demo of the Bleichenbacher RSA attack in Rust

Recently while reading Real-World Cryptography, I got nerd sniped1 by the mention of Bleichenbacher's attack on RSA. This is cool, how does it work? I had to understand, and to understand something, I usually have to build it. Well, friends, that is what…

via ntietz.com blog March 4, 2024

How to unbreak Dolphin on SteamOS after the QT6 update

A recent update to Dolphin made it switch to QT6. This makes it crash with this error or something like it: dolphin-emu: symbol lookup error: dolphin-emu: undefined symbol: _Zls6QDebugRK11QDockWidget, version Qt_6 This is fix…

via Xe Iaso's blog March 3, 2024

Generated by openring