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