DEV Community

Aaron Powell for Microsoft Azure

Posted on • Originally published at aaron-powell.com on

Extending Saturn to support Basic Authentication

Recently I needed to create a mock API for local development on a project and I decided to use Saturn, which describes itself thusly:

A modern web framework that focuses on developer productivity, performance, and maintainability

Saturn is written in F#, and given this whole project is F# it seemed like a logical fit. There’s a getting started guide, so I won’t go over that, instead I’ll focus on something I needed specifically for this projet, Basic Authentication, because the API I was mocking uses that under the hood and I wanted to simulate that.

Extending Saturn Applications

Conceptually, Saturn uses Computation Expressions for abstracting away the ASP.NET pipeline and giving you a very clean F# syntax for defining your application.

I wanted to make the application definition work like this:

let app = application {
    use_basic_auth
    // the rest of our app setup
    url (sprintf "http://0.0.0.0:%d/" port) }

Enter fullscreen mode Exit fullscreen mode

And to do this we’ll need to create a custom operation on Saturns ApplicationBuilder. Thankfully, F# makes it very easy to extend types you don’t own, so let’s get started:

type ApplicationBuilder with
    [<CustomOperationAttribute("use_basic_auth")>]
    member __.UseBasicAuth(state : ApplicationState) =
        state

Enter fullscreen mode Exit fullscreen mode

We’ll define our new attribute on the application computation expression and call it use_basic_auth and it will execute the defined function, which has a signature of ApplicationState -> ApplicationState.

Adding middleware

The first thing we’re going to need to do is to edit the middleware that Saturn uses to include authentication, and since it’s ASP.NET Core under the hood we need to add it’s middleware for Identity. Let’s update our UserBasicAuth function:

type ApplicationBuilder with
    [<CustomOperationAttribute("use_basic_auth")>]
    member __.UseBasicAuth state =
        let middleware (app : IApplicationBuilder) =
            app.UseAuthentication()

        { state with
            AppConfigs = middleware::state.AppConfigs }

Enter fullscreen mode Exit fullscreen mode

That was easy! We’ve added a new function called middleware that added the authentication middleware to the pipeline. Then we’ll use the :: List function to append our middleware to the head of the middleware collection and create a new record type using the current ApplicationState, just updating the AppConfigs property.

Implementing Basic Authentication

With Authentication enabled in our pipeline, we next need to tell it what kind of authentication we’re wanting to use and how to actually handle it!

type ApplicationBuilder with
    [<CustomOperationAttribute("use_basic_auth")>]
    member __.UseBasicAuth state =
        let middleware (app : IApplicationBuilder) =
            app.UseAuthentication()

        let service (s : IServiceCollection) =
            s.AddAuthentication("BasicAuthentication")
                .AddScheme<AuthenticationSchemeOptions, BasicAuthHandler>("BasicAuthentication", null)
                |> ignore

            s.AddTransient<IUserService, UserService>() |> ignore
            s

        { state with
            ServicesConfig = service::state.ServicesConfig
            AppConfigs = middleware::state.AppConfigs }

Enter fullscreen mode Exit fullscreen mode

Now we have a service function that takes the IServiceCollection, adds the Authentication service as BasicAuthentication (so the pipeline knows it’s that type), adds the handler (a type called BasicAuthHandler) and also registers a type in the Dependency Injection framework for accessing our users. We then modify our record type on return with this new function and it’s good to go!

Implementing the Basic Authentication Handler

Ok, we’re not quite done yet, we should have a look at how we actually implement the Basic Authentication handler in the BasicAuthHandler type, and our user store.

Let’s start with the user store, since I’ve done it quite simply, after all, it’s for a mock:

type IUserService =
    abstract member AuthenticateAsync : string -> string -> Async<bool>

type UserService() =
    let users = [("aaron", "password")] |> Map.ofList

    interface IUserService with
        member __.AuthenticateAsync username password =
            async {
                return match users.TryGetValue username with
                       | (true, user) when user = password -> true
                       | _ -> false }

Enter fullscreen mode Exit fullscreen mode

Yep, nothing glamerous here, I’ve just created a type that tests for a user and password in memory. In a non-mock system you might want to implement it more securely, but it does what I need for now. 😉 I’ve also created this as an interface so that I can inject it downcast, or I could mock it if I was to write tests (Narator: He didn’t write tests).

Now that we have a way to validate that credentials are valid for a user it’s time to implement the class that will handle authentication, BasicAuthHander:

type BasicAuthHandler(options, logger, encoder, clock, userService : IUserService) =
    inherit AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock)

Enter fullscreen mode Exit fullscreen mode

This type inherits from AuthenticationHandler within the ASP.NET Core framework and will require us to implement the HandleAuthenticateAsync function to be useful, so let’s start there:

type BasicAuthHandler(options, logger, encoder, clock, userService : IUserService) =
    inherit AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock)

    override this.HandleAuthenticateAsync() =
        task { return AuthenticateResult.Fail "Not Implemented" }

Enter fullscreen mode Exit fullscreen mode

Side note: I’m using the TaskBuilder.fs package to create a Task<T> response using the task computation expression.

This function is executed on every request as part of the middleware pipeline and I’m going to need to ensure that the Authorization header is provided and it has a valid Basic Auth token in it. Let’s start by ensuring the header exists with a match expression:

override this.HandleAuthenticateAsync() =
    let request = this.Request
    match request.Headers.TryGetValue "Authorization" with
    | (true, headerValue) ->
        task { return AuthenticateResult.Fail("Not implemented") }
    | (false, _) ->
        task { return AuthenticateResult.Fail("Missing Authorization Header") }

Enter fullscreen mode Exit fullscreen mode

The pattern matching will just check that we have the header and break into the appropriate block if the header exists, if it doesn’t we’ll just fail the challenge and result in a 401 response.

To validate the token I’m going to start with a quick function to unpack it like so:

type Credentials =
     { Username: string
       Password: string }

let getCreds headerValue =
    let value = AuthenticationHeaderValue.Parse headerValue
    let bytes = Convert.FromBase64String value.Parameter
    let creds = (Encoding.UTF8.GetString bytes).Split([|':'|])

    { Username = creds.[0]
      Password = creds.[1] }

Enter fullscreen mode Exit fullscreen mode

This will just decode the encoded string into a username:password pair that I return as a record (you could use an anonymous record type or a tuple, entirely up to you). Now we can validate it with our IUserService:

override this.HandleAuthenticateAsync() =
    let request = this.Request
    match request.Headers.TryGetValue "Authorization" with
    | (true, headerValue) ->
        async {
        let creds = getCreds headerValue.[0]

        let! userFound = userService.AuthenticateAsync creds.Username creds.Password

        return match userFound with
                | true ->
                    let claims = [| Claim(ClaimTypes.NameIdentifier, creds.Username); Claim(ClaimTypes.Name, creds.Username) |]
                    let identity = ClaimsIdentity(claims, this.Scheme.Name)
                    let principal = ClaimsPrincipal identity
                    let ticket = AuthenticationTicket(principal, this.Scheme.Name)
                    AuthenticateResult.Success ticket
                | false ->
                    AuthenticateResult.Fail("Invalid Username or Password") }
        |> Async.StartAsTask
    | (false, _) ->
        task { return AuthenticateResult.Fail("Missing Authorization Header") }

Enter fullscreen mode Exit fullscreen mode

We’ll use another match against this the result of our IUserService.AuthenticateAsync (which uses F# async), and if the user is valid we’ll create a claim ticket and return that to the pipeline successfully for the request the continue.

Wiring it up with our router

It’s now time to add authentication over the route(s) that we want to have authentication on, and we do that with the router computation expression. We’ll start with a pipeline:

let matchUpUsers : HttpHandler = fun next ctx -> next ctx

let authPipeline = pipeline {
    requires_authentication (Giraffe.Auth.challenge "BasicAuthentication")
    plug matchUpUsers }

Enter fullscreen mode Exit fullscreen mode

Setting on the pipeline the requires_authentication attribute to a BasicAuthentication challenge from Giraffe (the web framework Saturn builds on top of).

Finally, it’s time for our router:

let webApp = router {
    pipe_through authPipeline
    // define routes
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

The computation expression design of Saturn is really neat, the fact you can just extend the type that represents the part of Saturn that you want to extend. Through this we can add a custom authentication provider quite easily.

Hopefully this helps others looking to extend Saturn. 😊

Top comments (2)

Collapse
 
jeffb10011980 profile image
Jeff • Edited

Excellent article, and very usefull. However, after trying to add this to my own Saturn app, which is just a default generated template like the docs teaches you to do, the "use_basic_auth" line isn't recognized. I'm using VSCode, and it's saying "The value or constructor 'use_basic_auth' is not defined. Maybe you want one of the following:" and then proceeds to suggest other default Saturn ones like "use_cookies_authentication" and "use_jwt_authentication". I've basically just added the code that extends the ApplicationBuilder type on top of the "let app = application { ... }" declaration, and added "use_basic_auth" inside of it as the first line in there. All in the same Program.fs file. Any ideas on what I could be doing wrong? Is there maybe a git repo I can use as reference for this? Thanks!

Collapse
 
carlos_batista_71d20770ca profile image
Carlos Batista

Make sure to type the complete namespace path for the type you're trying to extend, in this case ApplicationBuilder. Sometimes if you just happen to have Saturn in your open statements it isn't enough for the inner types to be detected, so type it fully like "type Saturn.Application.ApplicationBuilder with...", or in the case you already have Saturn opened just do "type Application.ApplicationBuilder with...". And that should get your custom operation detected for use.