Date: 2024-10-03

# Encoding/Decoding JSON Web Tokens (JWT) in R

#### 2024-10-03

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.

- RFC7515: JSON Web Signature (JWS)
- RFC7516: JSON Web Encryption (JWE)
- RFC7517: JSON Web Key (JWK)
- RFC7518: JSON Web Algorithms (JWA)
- RFC7519: JSON Web Token (JWT)

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.eyJpYXQiOjE3Mjc5NjQ3NzMsInVzZXIiOiJqZXJvZW4iLCJzZXNzaW9uX2tleSI6MTIzNDU2fQ.IYVdPv-lidB3JRQ5xDGtYpq1J8bh9x-3QCCQsHV0l9Q"

    # Decode
    jwt_decode_hmac(jwt, secret = key)

    $iat
    [1] 1727964773

    $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.eyJpYXQiOjE3Mjc5NjQ3NzMsInVzZXIiOiJqZXJvZW4iLCJzZXNzaW9uX2tleSI6MTIzNDU2fQ.wMdpWFzYP2kdFsQ7IJ1Gtob1y3FweHw3c29hOWlBj4jbO6L9iYyxq4rgMht1SdZlqyUBE3Nw61CybBlIwe3yFQ"

    # Decode and verify using the public key
    jwt_decode_sig(jwt, pubkey = pubkey)

    $iat
    [1] 1727964773

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

- `iss` (Issuer): the principal that issued the JWT.
- `sub` (Subject): the principal that is the subject of the JWT.
- `aud` (Audience): the recipients that the JWT is intended for.
- `exp` (Expiration Time): the expiration time on or after which the JWT must not be accepted.
- `nbf` (Not Before): the time before which the JWT must not be accepted.
- `iat` (Issued At): the time at which the JWT was issued.
- `jti` (JWT ID): a unique identifier for the JWT.

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.eyJpc3MiOiJNeSB3ZWJhcHAiLCJleHAiOjE3Mjc5NjgzNzMsImlhdCI6MTcyNzk2NDc3MywibXlmaWVsZCI6IlNvbWUgYXBwbGljYXRpb24gbG9naWMiLCJjdXN0b21lciI6ImEgY293In0.Y2fmYlN8LgYrPkoEK4D4XeiYJS-gb3jXCbed7mzJzl5ZWh3UTS3E8GBEy3Ig6LAGLe-n4H3GT42iiIgoDNzadQ"

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] 1727968373

    $iat
    [1] 1727964773

    $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] "eyJpc3MiOiJNeSB3ZWJhcHAiLCJleHAiOjE3Mjc5NjgzNzMsImlhdCI6MTcyNzk2NDc3MywibXlmaWVsZCI6IlNvbWUgYXBwbGljYXRpb24gbG9naWMiLCJjdXN0b21lciI6ImEgY293In0"
    [3] "Y2fmYlN8LgYrPkoEK4D4XeiYJS-gb3jXCbed7mzJzl5ZWh3UTS3E8GBEy3Ig6LAGLe-n4H3GT42iiIgoDNzadQ"

    cat(rawToChar(base64url_decode(strings[1])))

    {"typ":"JWT","alg":"ES256"}

    cat(rawToChar(base64url_decode(strings[2])))

    {"iss":"My webapp","exp":1727968373,"iat":1727964773,"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.