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 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
...