jwt #
JSON Web Tokens (JWT) and CBOR Web Tokens (CWT) for OCaml.
A web service that trusts an identity provider, or a device that trusts a
controller, receives claims ("this is user 123, until 5 pm") signed by a key
it knows. A JWT (RFC 7519) carries those claims as JSON in a signed or
MAC-protected compact string; a CWT (RFC 8392) carries the same claims in
CBOR for constrained devices. nox-jwt parses, creates, verifies and
validates both, and keeps the three steps apart: Jwt.parse checks nothing
about the signature, Jwt.verify checks the signature, and Jwt.validate
checks the claims against a time the caller supplies.
It covers:
- the compact serialisation of JWS tokens, and nested JWTs up to a depth limit;
- JWS over any payload, in the compact and the flattened JSON serialisations (RFC 7515), under a protected header that can carry the signer's public key and members of the caller's, as an ACME request (RFC 8555) does;
- HMAC, RSA, ECDSA and EdDSA signatures, verified against an allow-list of
algorithms that leaves
noneout unless the caller lists it; - claims validation for expiry, not-before, issuer and audience;
- JSON Web Keys and key sets (RFC 7517), built from a parsed document or from an RSA, EC or Ed25519 key, their public half and their RFC 7638 thumbprint, and encrypted tokens (JWE) with direct, AES Key Wrap and RSA-OAEP key management;
- CBOR Web Tokens over COSE_Sign1 and COSE_Mac0, in
Jwt.Cwt. COSE (CBOR Object Signing and Encryption, RFC 9052) is the CBOR counterpart of JWS. A COSE_Sign1 is a CBOR array of a protected header, an unprotected header, the payload and one signature over the protected header and the payload; a COSE_Mac0 has the same shape with a MAC under a shared key in place of the signature.
Installation #
Install with opam:
$ opam install nox-jwt
If opam cannot find the package, it may not yet be released in the public
opam-repository. Add the overlay repository, then install it:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install nox-jwt
Usage #
Parsing and Verifying a JWT #
let verify_token token_string =
match Jwt.parse token_string with
| Ok jwt ->
let key = Jwt.Jwk.symmetric "secret-key" in
(match Jwt.verify ~key jwt with
| Ok () -> Fmt.pr "Valid signature@."
| Error e -> Fmt.pr "%a@." Jwt.pp_error e)
| Error e -> Fmt.pr "%a@." Jwt.pp_error e
Creating a JWT #
let create_token () =
let header = Jwt.Header.v ~typ:"JWT" Jwt.Algorithm.HS256 in
let claims =
Jwt.Claims.(
empty
|> set_iss "https://example.com"
|> set_sub "user123"
|> set_exp (Ptime.of_float_s 1700000000.0 |> Option.get)
|> build)
in
let key = Jwt.Jwk.symmetric "secret-key" in
match Jwt.v ~header ~claims ~key with
| Ok jwt -> Fmt.pr "%s@." (Jwt.encode jwt)
| Error e -> Fmt.pr "%a@." Jwt.pp_error e
Signing any payload #
A JWS signs bytes that need not be a claims set. Here the protected header carries the public half of the signing key and two members of the caller's, and the result is in the flattened JSON serialisation, which is the shape of an ACME request:
let sign_request ~key ~nonce ~url payload =
let jwk = Jwt.Jwk.of_private_key key in
let header =
Jwt.Header.v ~jwk
~members:[ ("nonce", Json.string nonce); ("url", Json.string url) ]
Jwt.Algorithm.ES256
in
Result.map Jwt.Jws.to_flattened (Jwt.Jws.sign ~header ~key:jwk payload)
Jwt.Jwk.thumbprint jwk is the key's RFC 7638 thumbprint, and
Jwt.Jws.of_flattened with Jwt.Jws.verify read such a request back.
Supported Algorithms #
| Algorithm | Description |
|---|---|
| HS256/384/512 | HMAC with SHA-2 |
| RS256/384/512 | RSASSA-PKCS1-v1_5 with SHA-2 |
| ES256/384/512 | ECDSA with P-256/384/521 |
| EdDSA | Ed25519 signatures |
| none | Unsecured, accepted only when listed in ~algorithms |
References #
- RFC 7519 - JSON Web Token (JWT)
- RFC 7515 - JSON Web Signature (JWS)
- RFC 7517 - JSON Web Key (JWK)
- RFC 7638 - JSON Web Key (JWK) Thumbprint
- RFC 7518 - JSON Web Algorithms (JWA)
- RFC 8392 - CBOR Web Token (CWT)
Licence #
ISC
- OCaml 100%