In the last blog post, we have seen how we can combine small functions to create a REST API in Suave and in this blog post we are going to see how can we secure the APIs using JSON web tokens(JWT).
This blog post is based on Taiseer’s blog post on JSON Web Token in ASP.NET Web API 2 using Owin and I will be covering how to implement the same in Suave. If you are interested in the theoretical background of JWT, kindly read his blog post before reading this.
Workflow
The typical workflow of JWT based application would look like this
New Audience (Resource Server) gets registered with Authorization Server.
Get the JWT access token from Authorization Server by passing Client Id of the resource server and login credentials
Use the access token obtained above to get access to the secured resources in the resource server
We are going to see how to implement all these three steps in this blog post
Project Setup
Create an empty visual studio solution and add new projects with the following name
SuaveJwt - A fsharp library project which contains all the JWT related things
SuaveJwt.AuthServerHost - A fsharp console application which is going to host the authorization server
Audience1 - A fsharp console application representing the resource server 1
Audience2 - A fsharp console application representing the resource server 2
After creating, install the following NuGet packages
Then add reference the .NET framework library System.IdentityModel in all the four projects from GAC and then add a reference to the SuaveJwt project library in all the other three projects.
New Audience Registration
As we have seen in the workflow, the first step is to enable an audience to register itself with the authorization server. Let’s begin by defining the business logic.
Add a new source file JwtToken.fs in the SuaveJwt project and update it as follows
The above snippet creates an audience record with a random client id and secret key. The secret key is of type base64 URL encoded string. This type doesn’t exist, so let’s create it.
Add a new source file Encodings.fs in the SuaveJwt project and add this type
The audienceWebPart function retrieves the AudienceCreateRequest from the JSON request payload and creates a new audience.
To make it independent of the host, we have externalized the hosting functionality using the Config record type.
The mapJsonPayload and JSON functions serialize and deserialize JSON objects across the wire respectively. These functions are not part of Suave, so let’s add them
Add a new source file SuaveJson.fs in the SuaveJwt project and add these functions
Open the Program.fs file in the SuaveJwt.AuthServerHost project and update it as below
1234567
letauthorizationServerConfig={AddAudienceUrlPath="/api/audience"SaveAudience=AudienceStorage.saveAudience}letaudienceWebPart'=audienceWebPartauthorizationServerConfigstartWebServerdefaultConfigaudienceWebPart'0// return an integer exit code
It is a straight forward self-host suave server program which exposes the audience web part.
The AudienceStorage is responsible for storing the created audiences and to add it create a new source file AudienceStorage.fs in the SuaveJwt.AuthServerHost project and add these functions
To keep this simple, we are using in-memory dictionary here and it can be easily replaced with any data store
Now we have everything to create a new audience. So, let’s run the SuaveJwt.AuthServerHost application and verify
Keep a note of this clientId and base64Secret as we will be using them while defining the resource server (“Audience1” in this case).
Generating Access Token
After registering an audience with the authorization server, the next step is to get the access token to access the resources in the given resource server.
Let’s begin by defining the business logic to create an access token.
These types are generic abstractions which decouple the token creation part from the underlying host.
The TokenCreateRequest models the underlying issuer and token lifetime.
The IdentityStore represents a generic data store in which the identity information has been stored and it also provides the security key and the signing credentials to protect the access token from misuse.
With these types in place let’s add the createToken function in the SuaveJwt.fs file
The createToken function checks the validness of the login credentials. If it is valid, then it get the claims associated with the given username and creates a JwtToken using the JSON Web Token Handler library, else it returns None
Now we have got the backend business logic ready, let’s expose it as a suave WebPart
Open AuthServer.fs and add a new record type for the incoming token create request and update Config and audienceWebPart as below
The tryCreateToken function checks whether the ClientId is a registered audience or not, if it exists, then this function creates a token using the createToken function defined above otherwise it returns the BAD_REQUSTWebPart with the appropriate error message.
The final change is modifying the Program.fs and AudienceStorage.fs files in the SuaveJwt.AuthServerHost to expose this WebPart.
To keep this simple, I am just hardcoding the credentials and claims here and it can be replaced with any backend. In this case, I am just going with accepting all the credentials as valid if the username and password are same.
Let’s run the SuaveJwt.AuthServerHost and verify the token
Securing the resources
Now we have come to the interesting part of securing the resources using the access token created in the above step
The first step to achieving this to validate the incoming access token. Let’s add this validation logic in the JwtToken.fs file.
The validate function returns a Choice type which contains either a sequence of Claims present in the token if the access token is valid or an error message describing what’s wrong with the access token
The next step is using this function to secure a Suave WebPart
Create a new source file Secure.fs in the SuaveJwt project and add the following code
12345678910111213141516171819202122232425
typeJwtConfig={Issuer:stringSecurityKey:SecurityKeyClientId:string}letjwtAuthenticatejwtConfigwebpart(ctx:HttpContext)=letupdateContextWithClaimsclaims={ctxwithuserState=ctx.userState.Remove("Claims").Add("Claims",claims)}matchctx.request.header"token"with|Choice1Of2accessToken->lettokenValidationRequest={Issuer=jwtConfig.IssuerSecurityKey=jwtConfig.SecurityKeyClientId=jwtConfig.ClientIdAccessToken=accessToken}letvalidationResult=validatetokenValidationRequestmatchvalidationResultwith|Choice1Of2claims->webpart(updateContextWithClaimsclaims)|Choice2Of2err->FORBIDDENerrctx|_->BAD_REQUEST"Invalid Request. Provide both clientid and token"ctx
The jwtAuthenticate function validates the access token present in the request header and invokes the given WebPart if it is valid. In case of invalid or absence of access token, it returns an HTTP error response instead of executing the WebPart.
Upon successful access token validation, the jwtAuthenticate function puts the claims in the userState map of incoming HttpContext so that subsequent WebParts in the pipeline can use it.
The JwtConfig record abstracts the underlying audience from the validation logic so that it can be reused across multiple audiences.
Now we have a functionality secure a web part. Let’s create an audience and leverage this
Update the Program.fs file in the Audience1 project as mentioned below
I’ve asked you to keep a note of the clientId and the securityKey while doing the registration of Audience1. We are using them here in the jwtConfig record.
Let’s see it in action
Hurray! We have made it. “/audience1/sample1” is a secured API now!
What will happen if we mess with the access token? Well, we will get an HTTP error. Let’s change the character Q in the access token from upper case to lower case and here is the result of it.
Cool, Isn’t it?
Let’s add authorization based on the claims that we have obtained from the JWT token
Open Secure.fs and update the authorization functionality
123456789101112131415161718192021222324252627
typeAuthorizationResult=|Authorized|UnAuthorizedofstringletjwtAuthorizejwtConfigauthorizeUserwebpart=letgetClaims(ctx:HttpContext)=letuserState=ctx.userStateifuserState.ContainsKey("Claims")thenmatchuserState.Item"Claims"with|:?(Claimseq)asclaims->Someclaims|_->NoneelseNoneletauthorizehttpContext=matchgetClaimshttpContextwith|Someclaims->async{let!authorizationResult=authorizeUserclaimsmatchauthorizationResultwith|Authorized->return!webparthttpContext|UnAuthorizederr->return!FORBIDDENerrhttpContext}|None->FORBIDDEN"Claims not found"httpContextjwtAuthenticatejwtConfigauthorize
The jwtConfig function is very similar to the jwtAuthenticate function which provides authorization in addition to the authentication.
The key here is the parameter authorizeUser which is a function that takes a sequence of claims and returns an AuthorizationResult.
Like jwtAuthenticate`` function, thejwtConfig“` function is also abstracted from the underlying Audience so we can use it across multiple Audiences.
Let’s use this in the Audience1.
Program.fs
12345678910111213141516
[<EntryPoint>]letmainargv=// ... existing code ...// Claim Seq -> Async<AuthorizationResult>letauthorizeAdmin(claims:Claimseq)=matchclaims|>Seq.tryFind(func->c.Type=ClaimTypes.Role&&c.Value="Admin")with|Some_->Authorized|>async.Return|None->UnAuthorized"User is not an admin"|>async.Returnletsample2=path"/audience1/sample2">=>jwtAuthorizejwtConfigauthorizeAdmin(OK"Sample 2")letapp=choose[sample1;sample2]startWebServerconfigapp0
The jwtAuthroize function in action
Note: I’ve generated a new access token here to exercise this use case.
That’s it we have successfully completed all the three steps mentioned at the beginning of this blog post.
A Supplement
One cool thing about the design of the SuaveJwt library is, it doesn’t have any assumption about the Authorization Server and the Resource Server. Because of it, we can easily extend it.
Let’s prove it by updating the Program.fs file in the Audience2 project.
123456789101112131415161718192021
[<EntryPoint>]letmainargv=letjwtConfig={Issuer="http://localhost:8083/suave"ClientId="ada9263885c440869fb484fe354de13d"SecurityKey=KeyStore.securityKey(Base64String.fromString"0RWyzyttDmJtiaYkG9rph5cqxCTI8YAOsR7stq-P_5o")}letauthorizeSuperUser(claims:Claimseq)=matchclaims|>Seq.tryFind(func->c.Type=ClaimTypes.Role&&c.Value="SuperUser")with|Some_->Authorized|>async.Return|None->UnAuthorized"User is not a Super User"|>async.Returnletauthorize=jwtAuthorizejwtConfigletsample1=path"/audience2/sample1">=>OK"Sample 1"letsample2=path"/audience2/sample2">=>authorizeauthorizeSuperUser(OK"Sample 2")letconfig={defaultConfigwithbindings=[HttpBinding.mkSimpleHTTP"127.0.0.1"8085]}letapp=choose[sample1;sample2]startWebServerconfigapp0
Note that the jwtConfig values are different from that of Audience1 and it is obtained from calling the AuthorizationServer’s audience registration API for Audience2.
Summary
Suave provides a simple and elegant way of extending its core functionality. In this blog post, we have seen how to bend it to support JWT based authorization and I believe we can do a lot of other cool things too!
You can find the complete source code of the sample application used in this blog post in my blog-samples GitHub repository.