F# JWT with RSA

I haven’t blogged in some time, as I was busy with another project. In said project I need JSON Web Tokens (JWT) with asynchronous cryptography to be able to validate the JWT without sharing a secret key.

I’m not going to explain JWTs themselves and concentrate on how to use them with F#. If you want to learn more about JWT check out jwt.io. The code is based on a C# example from here: .NET Core 3.1 signing JWT with RSA

The example script uses the RC1 of .NET 5 with F# 5 features, so make sure you have installed the correct SDK to be able to run it.

RSA Key-Pair

To be able to sign and validate a JWT with RSA, we need a public and a private key, just like you know it from SSH or TLS in the browser. You can generate a key-pair using openssl on the command line or use an online tool like Online RSA Key Generator to generate the key pair. If you use openssl, make sure to export the private key without a password for the example to run.

If you are going to use the code in production, it is better to have an encrypted private key. To be able to use one, just switch the import method to one that supports encrypted keys. For more information check the RSA.Create Method documentation.

You should now have a private and a public RSA key which look like this:

-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw
kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr
...
-----END RSA PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
...
-----END PUBLIC KEY-----

Save the private key as key.priv and the public key as key.pub.

As we need the keys as a byte array without any headers, footers or line breaks, we define a function that reads a key file and strips it of all unwanted information and converts it to an byte array.

let readKey file =
    let content = File.ReadAllText file
    content.Replace("-----BEGIN RSA PRIVATE KEY-----", "").Replace("-----END RSA PRIVATE KEY-----", "")
           .Replace("-----BEGIN PUBLIC KEY-----", "").Replace("-----END PUBLIC KEY-----", "").Replace("\r\n", "")
           .Replace("\n", "")
    |> Convert.FromBase64String

The method is not pretty and I’m sure the is a more elegant way to achieve the same result, but it works and is easy to understand.

Now lets read the key-pair into values to be used later.

let pubKey = readKey "key.pub"
let privKey = readKey "key.priv"

Payload

As we don’t want to create an JWT just for the lulz, we need some payload to sign and later validate. For the example, we make it as easy a possible and define a payload with just two members.

type Payload = { Name: String; Admin: bool }
let payload = { Name = "John Doe"; Admin = true }

That is the content of our JWT, besides the header which I’ll not explain in this blog post.

Create and sign the JWT

Lets create a function that takes a payload and a private key and returns a signed JWT. In that function, we create a new RSA object and import the private key. After that we create the needed credentials and disable the cache for the signatures, as we don’t need that in our case. Next we add the claims, which is out payload. Note that all values have to be of type String. As a last step we create a JWT and write the token to a string which is returned by the function.

let sign payload privKey =
    use rsa = RSA.Create()
    let mutable bytesRead = 0
    rsa.ImportRSAPrivateKey(new ReadOnlySpan<byte>(privKey), &bytesRead)

    let crypProvFactory =
        new CryptoProviderFactory(CacheSignatureProviders = false)

    let signingCredentials =
        new SigningCredentials(new RsaSecurityKey(rsa),
                               SecurityAlgorithms.RsaSha256,
                               CryptoProviderFactory = crypProvFactory)

    let claims =
        [| new Claim(nameof (payload.Name), payload.Name)
           new Claim(nameof (payload.Admin), payload.Admin.ToString()) |]

    let jwt =
        new JwtSecurityToken(claims = claims, signingCredentials = signingCredentials)

    let jwtHandler = new JwtSecurityTokenHandler()
    jwtHandler.WriteToken(jwt)

Validate the JWT

Imagen we sent the token over the network to some other service and want to know if the token is valid and authenticated through the private-key. To do so, we need to create a validation function which checks the RSA signature with the public-key.

Again, we are creating a RSA object and importing the corresponding key. After that we configure what should be validated in the token. As we did not set any Issuer, Audience or Lifetime for the token, we don’t validate them. Unfortunately the ValidateToken function does not return a true or false, but instead throws an exception if the validation failed. As such we need to misuse a try-catch for control flow. Our method returns true if the token signature is valid and false if not.

let validate (token: string) pubKey =
    CryptoProviderFactory.Default.CacheSignatureProviders <- false
    use rsa = RSA.Create()
    let mutable bytesRead = 0
    rsa.ImportSubjectPublicKeyInfo(new ReadOnlySpan<byte>(pubKey), &bytesRead)

    let validationParameters =
        new TokenValidationParameters(ValidateIssuer = false,
                                      ValidateAudience = false,
                                      ValidateLifetime = false,
                                      ValidateIssuerSigningKey = true,
                                      IssuerSigningKey = new RsaSecurityKey(rsa))

    let handler = new JwtSecurityTokenHandler()
    let mutable validatedToken: SecurityToken = upcast new JwtSecurityToken()

    try
        handler.ValidateToken(token, validationParameters, &validatedToken)
        |> ignore
        true
    with ex ->
        printfn $"Exception: {ex.Message}"
        false

That’s all we need to do, to sign a JWT with a private key and validate the signature with a public key.

Full Code

The working example can be found on GitHub: F# JWT RSA Example

The example on GitHub contains an example key-pair. Make sure to never use them in production as everyone has access to the private key. Always create a fresh pair!

#r "nuget: System.IdentityModel.Tokens.Jwt"
#r "nuget: Microsoft.IdentityModel.Tokens"

open System.IdentityModel.Tokens.Jwt
open System
open System.IO
open System.Text
open System.Security.Claims
open System.IdentityModel.Tokens.Jwt
open Microsoft.IdentityModel.Tokens
open System.Security.Cryptography

type Payload = { Name: String; Admin: bool }

let readKey file =
    let content = File.ReadAllText file
    content.Replace("-----BEGIN RSA PRIVATE KEY-----", "").Replace("-----END RSA PRIVATE KEY-----", "")
           .Replace("-----BEGIN PUBLIC KEY-----", "").Replace("-----END PUBLIC KEY-----", "").Replace("\r\n", "")
           .Replace("\n", "")
    |> Convert.FromBase64String

let pubKey = readKey "key.pub"
let privKey = readKey "key.priv"

let sign payload privKey =
    use rsa = RSA.Create()
    let mutable bytesRead = 0
    rsa.ImportRSAPrivateKey(new ReadOnlySpan<byte>(privKey), &bytesRead)

    let crypProvFactory =
        new CryptoProviderFactory(CacheSignatureProviders = false)

    let signingCredentials =
        new SigningCredentials(new RsaSecurityKey(rsa),
                               SecurityAlgorithms.RsaSha256,
                               CryptoProviderFactory = crypProvFactory)

    let claims =
        [| new Claim(nameof (payload.Name), payload.Name)
           new Claim(nameof (payload.Admin), payload.Admin.ToString()) |]

    let jwt =
        new JwtSecurityToken(claims = claims, signingCredentials = signingCredentials)

    let jwtHandler = new JwtSecurityTokenHandler()
    jwtHandler.WriteToken(jwt)


let validate (token: string) pubKey =
    CryptoProviderFactory.Default.CacheSignatureProviders <- false
    use rsa = RSA.Create()
    let mutable bytesRead = 0
    rsa.ImportSubjectPublicKeyInfo(new ReadOnlySpan<byte>(pubKey), &bytesRead)

    let validationParameters =
        new TokenValidationParameters(ValidateIssuer = false,
                                      ValidateAudience = false,
                                      ValidateLifetime = false,
                                      ValidateIssuerSigningKey = true,
                                      IssuerSigningKey = new RsaSecurityKey(rsa))

    let handler = new JwtSecurityTokenHandler()
    let mutable validatedToken: SecurityToken = upcast new JwtSecurityToken()

    try
        handler.ValidateToken(token, validationParameters, &validatedToken)
        |> ignore
        true
    with ex ->
        printfn $"Exception: {ex.Message}"
        false

let payload = { Name = "John Doe"; Admin = true }
printfn $"Payload: {payload}"

let token = sign payload privKey
printfn $"Signed JWT: {token}"

let isValid = "Valid token"
let isInvalid = "Invalid token"
printfn $"Validate: {if validate token pubKey then isValid else isInvalid}"

Run the Script

We can run the script, called jwt.fsx, with the following command:

dotnet fsi --langversion:preview .\jwt.fsx

The result should look like this:

Payload: { Name = "John Doe"
  Admin = true }
Signed JWT: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiSm9obiBEb2UiLCJBZG1pbiI6IlRydWUifQ.ma49T3npXQJVMa-afyfFgIPW5PEYhrrYvX2mUA6rmzXHXq_Wy-ij9MLc0b6UxZX8STcRrSC93meIMa4a8LI7UBe0Pxn8IQBrhXztcElMfktMQoQWb7Osx9XwmqD1CaQWwz3FX963B4fQFdxx7GpxdLPj-CSOJZ4OZbk8fWpurVX1QXMLokaJ8C-gLB026jFVJjIV1APSMOnAzx9lcZfU5m3jwVP8HMIc0yJkm4d7IJO1lQjYnUWQkY_DmwR8-vysqo3N5yY57xQUFRoyHwFofDb25fA6SkKcNHrOX0_bc7KzxzWacoPWgtUolThKasWpXgqHipR-uJ4hz6zahInCmw
Validate: Valid token

The token consists of three parts separated by an dot. The first part is the header, the second the payload and third the signature.

If we decode (base64) the token we get three pars:

// The JWT header
{
  "alg": "RS256",
  "typ": "JWT"
}
// The payload
{
  "Name": "John Doe",
  "Admin": "True"
}
// Signature
...

One thought on “F# JWT with RSA

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s