Secure your Webservice with JWT in Saturn and F#
How do I secure my web service or webapp with a json web token and use the service itself as the one, which also manages the user itself. (no oauth with google or something else.)
I do not explain the JWT at all. I show only step by step, how to implement it in saturn, an awesome mvc f# web framework.
I use the saturn template base app for this tutorial.
I put all the needed code in one module except for the routes. Feel free to order your code, how do you need. For example I use the module name AuthStuff
.
module AuthStuff
What do we need.
Prepare, we need some namespaces to open in that file
open System
open System.Text
open Saturn
open System.Security.Claims
open System.IdentityModel.Tokens.Jwt
open Microsoft.IdentityModel.Tokens
open Microsoft.AspNetCore.Authentication.JwtBearer
1. We need 2 record types. One is the model for the login data and the other is the model for the token return data.
type LoginModel ={
UserName : string
Password : string
}
type TokenResult ={
Token : string
}
2. Now we need some values, like the secret key, which signs our token. The key itself has to be at least 16 characters long (128 Bit). Also an value for the issuer and the audience. (feel free to search for yourself, what for are these values.)
// min 128 Bit - min 16 character
let secret = "ineeda128bitkey!"
let issuer = "myauthapp.de"
let audience = "myauthapp.de"
3. Then we need a saturn pipeline to activate the authentication for the routes, that needed to secured
open Saturn
let onlyLoggedIn = pipeline {
requires_authentication (Giraffe.Auth.challenge JwtBearerDefaults.AuthenticationScheme)
}
4. Also we need a function, which generates the JWT Token itself
let generateToken username claims =
let claims = [|
Claim(JwtRegisteredClaimNames.Sub, username);
Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
// here you can implement other claims, which are encoded in the token itself
yield! claims
|]
let expires = Nullable(DateTime.UtcNow.AddHours(8.0))
let notBefore = Nullable(DateTime.UtcNow)
let securityKey = SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret))
let signingCredentials = SigningCredentials(key = securityKey, algorithm = SecurityAlgorithms.HmacSha256)
let token =
JwtSecurityToken(
issuer = issuer,
audience = audience,
claims = claims,
expires = expires,
notBefore = notBefore,
signingCredentials = signingCredentials)
let tokenResult = {
Token = JwtSecurityTokenHandler().WriteToken(token)
}
tokenResult
5. The next Step is to tell saturn to use JWT Token for authentication. To do this, we need to extend the Saturn ApplicationBuilder object with our own function
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
type Saturn.Application.ApplicationBuilder with
[<CustomOperation("my_own_jwt_auth")>]
member __.UseMyMehAuth(state: ApplicationState,secret:string) =
let middleware (app : IApplicationBuilder) =
app.UseAuthentication()
let service (s : IServiceCollection) =
s.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(fun options ->
options.TokenValidationParameters <- TokenValidationParameters(
ValidateActor = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
ValidAudience = audience,
IssuerSigningKey = SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)))
) |> ignore
s
{ state with
ServicesConfig = service::state.ServicesConfig
AppConfigs = middleware::state.AppConfigs
CookiesAlreadyAdded = true
}
As you see, I use the CustomOperation attribute with ne name my_own_jwt_auth
, these is the expression we use in the application setup from you saturn app.
The function itself has 1 extra parameter beside the state
(which is a necessary parameter to extend the ApplicationBuilder), and this is the secret
. You can use more to make this more flexible, if you want.
6. Now we put our new ApplicationBuilder extension to the application setup. In this example you see the app setup from the saturn template.
open AuthStuff
let app = application {
pipe_through endpointPipe
// here! you see, this is our extension!
my_own_jwt_auth AuthStuff.secret
error_handler (fun ex _ -> pipeline { render_html (InternalError.layout ex) })
use_router Router.appRouter
url "http://0.0.0.0:8085/"
memory_cache
use_static "static"
use_gzip
use_config (fun _ -> {connectionString = "DataSource=database.sqlite"} ) //TODO: Set development time configuration
}
7. So next step is actually add a endpoint, where we can get our token and one endpoint, which we secure for testing purposes. This code I put in the file "Router.fs" from the basic saturn template, if you wonder.
The Token Endpoint:
let tokenRouter = router {
post "/gettoken" (fun next ctx ->
task {
let! model = ctx.BindJsonAsync<LoginModel>()
// handle the authentication here itself. So ask the db if the user and password is correct. What ever is needed.
let tokenResult =
AuthStuff.generateToken model.UserName [| (* here add your claims *) |]
return! json tokenResult next ctx
})
}
The Test Endpoint:
Here you see, we are adding out onlyLoggedIn
pipeline
let authRouter = router {
pipe_through onlyLoggedIn
get "/checksec" (fun next ctx ->
task {
let username = ctx.User.FindFirst ClaimTypes.NameIdentifier
let txt = sprintf "Hello %s" username.Value
return! text txt next ctx
}
)
}
8. And the last Step is to add the 2 endpoints
let appRouter = router {
// here add the new endpoint
forward "/hidden" authRouter
forward "/token" tokenRouter
forward "" browserRouter
}
Let's test our stuff:
Send the request to the topken endpoint:
Now copy the token and use it in the bearer token authentication header:
Havin fun!