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.
Linking to: OpenSSL 3.0.13 30 Jan 2024
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.eyJpYXQiOjE3Nzg1MTc0NTIsInVzZXIiOiJqZXJvZW4iLCJzZXNzaW9uX2tleSI6MTIzNDU2fQ.bpAMBg56sGqrw6t1agx3nkYnEooTLBbQO86FV-ss63U"
# Decode
jwt_decode_hmac(jwt, secret = key)$iat
[1] 1778517452
$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())
[1m
[33mError
[39m:
[22m
[33m!
[39m 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.eyJpYXQiOjE3Nzg1MTc0NTIsInVzZXIiOiJqZXJvZW4iLCJzZXNzaW9uX2tleSI6MTIzNDU2fQ.ik7DYCmD5fvLN86nj-bnTfdvyg14CgnI7pCcmMghLqJIhazdPfmIT6e5xGhuOmh7PbROdwbwXOKWuCvwhqlbqg"
# Decode and verify using the public key
jwt_decode_sig(jwt, pubkey = pubkey)$iat
[1] 1778517452
$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)
[1m
[33mError
[39m in `hash_verify()`:
[22m
[33m!
[39m 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.eyJpc3MiOiJNeSB3ZWJhcHAiLCJleHAiOjE3Nzg1MjEwNTIsImlhdCI6MTc3ODUxNzQ1MiwibXlmaWVsZCI6IlNvbWUgYXBwbGljYXRpb24gbG9naWMiLCJjdXN0b21lciI6ImEgY293In0.k-fZxZphy1bgb9ddWZB_4wU9HY21YsZSvc74rNq-feQ69jmbzgwpH78aj_W7OKRte7-XLbppp6oV_JKMUD17qA"
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] 1778521052
$iat
[1] 1778517452
$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] "eyJpc3MiOiJNeSB3ZWJhcHAiLCJleHAiOjE3Nzg1MjEwNTIsImlhdCI6MTc3ODUxNzQ1MiwibXlmaWVsZCI6IlNvbWUgYXBwbGljYXRpb24gbG9naWMiLCJjdXN0b21lciI6ImEgY293In0"
[3] "k-fZxZphy1bgb9ddWZB_4wU9HY21YsZSvc74rNq-feQ69jmbzgwpH78aj_W7OKRte7-XLbppp6oV_JKMUD17qA"
cat(rawToChar(base64url_decode(strings[1]))){"typ":"JWT","alg":"ES256"}
cat(rawToChar(base64url_decode(strings[2]))){"iss":"My webapp","exp":1778521052,"iat":1778517452,"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.