JSON Web Token (JWT) and CBOR Web Token (CWT)

Files
README.md

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 none out 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 #

Licence #

ISC