Encoding/Decoding JSON Web Tokens (JWT) in R

JavaScript Object Signing and Encryption (JOSE) consists of a set of specifications for encryption and signatures based on the popular JSON format. This is work in progress, the IETF jose workgroup usually has the latest information.

The jose package implements some of these specifications, in particular for working with JSON web tokens and keys.

JSON Web Token: HMAC tagging

The most common use of JSON Web Tokens is combining a small payload (the ‘claim’) with a HMAC tag or RSA/ECDSA signature. See also https://jwt.io for short introduction.

library(openssl)
library(jose)

# Example payload
claim <- jwt_claim(user = "jeroen", session_key = 123456)

# Encode with hmac
key <- charToRaw("SuperSecret")
(jwt <- jwt_encode_hmac(claim, secret = key))
[1] "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MzA1MjcxMTUsInVzZXIiOiJqZXJvZW4iLCJzZXNzaW9uX2tleSI6MTIzNDU2fQ.PqEQDmDnnSXkyULSFWGp5pJXPCJIe-a04mDtky3ETjE"
# Decode 
jwt_decode_hmac(jwt, secret = key)
$iat
[1] 1730527115

$user
[1] "jeroen"

$session_key
[1] 123456

The decoding errors if the tag verification fails.

# What happens if we decode with the wrong key
jwt_decode_hmac(jwt, secret = raw())
Error: HMAC signature verification failed!

JSON Web Token: RSA/ECDSA signature

Similarly, we can use an RSA or ECDSA key pair we to verify a signature from someone’s public key.

# Generate ECDSA keypair
key <- ec_keygen()
pubkey <- as.list(key)$pubkey

# Sign with the private key
(jwt <- jwt_encode_sig(claim, key = key))
[1] "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MzA1MjcxMTUsInVzZXIiOiJqZXJvZW4iLCJzZXNzaW9uX2tleSI6MTIzNDU2fQ.KOfiNqciez4BS45bWWqCe8dk6eBKnE-K6Y1f6iOhKSqRlVgJRxlT8XiSVQiE4rqO7XI36Vf2FsmdPFFRF7Td5A"
# Decode and verify using the public key
jwt_decode_sig(jwt, pubkey = pubkey)
$iat
[1] 1730527115

$user
[1] "jeroen"

$session_key
[1] 123456

Again decoding will error if the signature verification fails.

wrong_key <- ec_keygen()
jwt_decode_sig(jwt, pubkey = wrong_key)
Error in hash_verify(md, sig, pk): Verification failed: incorrect signature

The spec also describes methods for encrypting the payload, but this is currently not widely in use yet.

Reserved jwt-claim names

You can include custom fields in your jwt payload, but the spec names a few registered claims that are reserved for specific uses.

Each of these are optional, by default only iat is set. The jwt_claim() function will automatically do basic validation when you set additional fields from this list. For any other fields you can use any value. For example:

# Note that this token expires in 1 hour!
myclaim <- jwt_claim(
  iss = "My webapp",
  exp = Sys.time() + 3600,
  myfield = "Some application logic",
  customer = "a cow"
)
(jwt <- jwt_encode_sig(myclaim, key = key))
[1] "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJNeSB3ZWJhcHAiLCJleHAiOjE3MzA1MzA3MTUsImlhdCI6MTczMDUyNzExNSwibXlmaWVsZCI6IlNvbWUgYXBwbGljYXRpb24gbG9naWMiLCJjdXN0b21lciI6ImEgY293In0.1BkxgDemtc2YcR5-nf5mz2pck4ZaDpJxEexgjbzFKqakCFOFmGjveSM3xePT50pRYaaGmMjkYV5wbKTYlzwvkA"

The decode functions will automatically verify that the token has not expired (with a 60s grace period to account for inaccurate clocks), and error otherwise:

jwt_decode_sig(jwt, pubkey = pubkey)
$iss
[1] "My webapp"

$exp
[1] 1730530715

$iat
[1] 1730527115

$myfield
[1] "Some application logic"

$customer
[1] "a cow"

Where is the JSON

The jwt payloads consists of a head, body and signature which are separated with a dot into a single string. Both the header and body are actually base64url encoded JSON objects.

(strings <- strsplit(jwt, ".", fixed = TRUE)[[1]])
[1] "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9"                                                                                                           
[2] "eyJpc3MiOiJNeSB3ZWJhcHAiLCJleHAiOjE3MzA1MzA3MTUsImlhdCI6MTczMDUyNzExNSwibXlmaWVsZCI6IlNvbWUgYXBwbGljYXRpb24gbG9naWMiLCJjdXN0b21lciI6ImEgY293In0"
[3] "1BkxgDemtc2YcR5-nf5mz2pck4ZaDpJxEexgjbzFKqakCFOFmGjveSM3xePT50pRYaaGmMjkYV5wbKTYlzwvkA"                                                         
cat(rawToChar(base64url_decode(strings[1])))
{"typ":"JWT","alg":"ES256"}
cat(rawToChar(base64url_decode(strings[2])))
{"iss":"My webapp","exp":1730530715,"iat":1730527115,"myfield":"Some application logic","customer":"a cow"}

However you should never trust this information without verifying the signature. This is what the jwt_decode functions do for you.