OCaml HTML5 parser/serialiser based on Python's JustHTML

Merge commit '618bad0bb3ff179cff9630c8005299f3cba57da3' as 'ocaml-jsonwt'

+3949
+17
ocaml-jsonwt/.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Third-party sources (fetch locally with opam source) 7 + third_party/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+1
ocaml-jsonwt/.ocamlformat
··· 1 + version=0.28.1
+54
ocaml-jsonwt/.tangled/workflows/build.yml
··· 1 + when: 2 + - event: ["push", "pull_request"] 3 + branch: ["main"] 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - shell 10 + - stdenv 11 + - findutils 12 + - binutils 13 + - libunwind 14 + - ncurses 15 + - opam 16 + - git 17 + - gawk 18 + - gnupatch 19 + - gnum4 20 + - gnumake 21 + - gnutar 22 + - gnused 23 + - gnugrep 24 + - diffutils 25 + - gzip 26 + - bzip2 27 + - gcc 28 + - ocaml 29 + - pkg-config 30 + - gmp 31 + 32 + steps: 33 + - name: opam 34 + command: | 35 + opam init --disable-sandboxing -a -y 36 + - name: repo 37 + command: | 38 + opam repo add aoah https://tangled.org/anil.recoil.org/aoah-opam-repo.git 39 + - name: switch 40 + command: | 41 + opam install . --confirm-level=unsafe-yes --deps-only 42 + - name: build 43 + command: | 44 + opam exec -- dune build 45 + - name: switch-test 46 + command: | 47 + opam install . --confirm-level=unsafe-yes --deps-only --with-test 48 + - name: test 49 + command: | 50 + opam exec -- dune runtest --verbose 51 + - name: doc 52 + command: | 53 + opam install -y odoc 54 + opam exec -- dune build @doc
+15
ocaml-jsonwt/LICENSE.md
··· 1 + ISC License 2 + 3 + Copyright (c) 2026 Anil Madhavapeddy <anil@recoil.org> 4 + 5 + Permission to use, copy, modify, and distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+151
ocaml-jsonwt/TODO.md
··· 1 + # JWT Implementation TODO 2 + 3 + RFC 7519 compliance tracking for ocaml-jwt. 4 + 5 + ## Implementation Status 6 + 7 + - [x] Type definitions for all registered claims 8 + - [x] Type definitions for JOSE header parameters 9 + - [x] Type definitions for algorithms 10 + - [x] Base64url encoding/decoding 11 + - [x] Basic JWT structure parsing (3-part split) 12 + - [x] JSON parsing with jsont 13 + - [x] Signature creation and verification (HMAC, ECDSA, EdDSA) 14 + - [x] Claims validation 15 + - [x] JWK parsing and serialization 16 + - [x] Structured error types 17 + - [x] Comprehensive tests (30 tests passing) 18 + 19 + --- 20 + 21 + ## Completed Phases 22 + 23 + ### Phase 0: Error Types and Core Infrastructure - DONE 24 + 25 + - [x] Structured error type with all RFC-compliant variants 26 + - [x] `pp_error` and `error_to_string` functions 27 + - [x] StringOrURI validation (validates URIs per RFC 3986) 28 + 29 + ### Phase 1: JSON Parsing with jsont - DONE 30 + 31 + - [x] Header JSON codec (`Header.of_json`, `Header.to_json`) 32 + - [x] Claims JSON codec with strict mode for duplicate detection 33 + - [x] JWK JSON codec for all key types (Oct, RSA, EC, OKP) 34 + 35 + ### Phase 2: Signature Operations - PARTIALLY DONE 36 + 37 + - [x] **HMAC Signatures** (HS256, HS384, HS512) - using digestif 38 + - [x] **ECDSA Signatures** (ES256, ES384, ES512) - using mirage-crypto-ec 39 + - [x] **EdDSA Signatures** (Ed25519) - using mirage-crypto-ec 40 + - [x] **Unsecured JWT** ("none") - with explicit `~allow_none:true` opt-in 41 + - [x] **Nested JWT Support** - `parse_nested` with max_depth protection 42 + - [ ] **RSA Signatures** (RS256, RS384, RS512) - STUBBED, needs JWK-to-RSA key parsing 43 + 44 + ### Phase 3: Claims Validation - DONE 45 + 46 + - [x] Time-based claims (exp, nbf) with leeway support 47 + - [x] Issuer validation (iss) 48 + - [x] Audience validation (aud) 49 + - [x] `is_expired` helper function 50 + - [x] `time_to_expiry` helper function 51 + 52 + ### Phase 4: Full JWT Creation Flow - DONE 53 + 54 + - [x] `create` function for signing JWTs 55 + - [x] Algorithm/key type validation 56 + 57 + ### Phase 5: Tests - DONE (30 tests passing) 58 + 59 + #### RFC Test Vectors 60 + - [x] RFC 7519 Section 3.1 HS256 JWT 61 + - [x] RFC 7519 Section 6.1 Unsecured JWT 62 + 63 + #### Algorithm Coverage 64 + - [x] HS256 sign/verify 65 + - [x] HS384 sign/verify 66 + - [x] HS512 sign/verify 67 + - [ ] RS256 sign/verify (stubbed) 68 + - [ ] RS384 sign/verify (stubbed) 69 + - [ ] RS512 sign/verify (stubbed) 70 + - [x] ES256 sign/verify 71 + - [x] ES384 sign/verify 72 + - [x] ES512 sign/verify 73 + - [x] EdDSA sign/verify 74 + - [x] none (unsecured) with opt-in 75 + 76 + #### Validation Tests 77 + - [x] Expired token rejection 78 + - [x] Not-yet-valid token rejection 79 + - [x] Issuer mismatch rejection 80 + - [x] Audience mismatch rejection 81 + - [x] Leeway handling 82 + 83 + #### Error Cases 84 + - [x] Invalid base64url 85 + - [x] Invalid JSON 86 + - [x] Wrong number of parts 87 + - [x] Signature mismatch 88 + - [x] Algorithm not in allowed list 89 + - [x] Unsecured JWT without allow_none 90 + 91 + --- 92 + 93 + ## Remaining Work 94 + 95 + ### RSA Signatures (RS256, RS384, RS512) 96 + 97 + **Status:** Stubbed - returns `Key_type_mismatch "RSA signing/verification not yet implemented"` 98 + 99 + **Required:** 100 + 1. Implement JWK-to-RSA key parsing for `n`, `e` (public) and `d`, `p`, `q`, `dp`, `dq`, `qi` (private) fields 101 + 2. Use `mirage-crypto-pk` for RSASSA-PKCS1-v1_5 signatures 102 + 3. Add tests with RFC test vectors 103 + 104 + ### Future Work (Not in Current Scope) 105 + 106 + 1. **JWK Set (JWKS)**: RFC 7517 Section 5 support for multiple keys 107 + - Useful for key rotation and fetching keys from JWKS endpoints 108 + - Example: `/.well-known/jwks.json` 109 + - Consider adding `Jwks.t` type and `Jwks.find_key : kid:string -> Jwks.t -> Jwk.t option` 110 + 111 + 2. **JWE Support**: RFC 7516 JSON Web Encryption 112 + - Required for encrypted JWTs (as opposed to signed JWTs) 113 + - Lower priority unless needed for specific use cases 114 + 115 + --- 116 + 117 + ## Design Decisions (Implemented) 118 + 119 + 1. **StringOrURI validation**: YES - Validates that `iss`/`sub` values containing ":" are valid URIs per RFC 3986. 120 + 121 + 2. **Duplicate claims**: STRICT MODE - Rejects JWTs with duplicate claim names by default. `~strict:false` allows lenient parsing. 122 + 123 + 3. **"none" algorithm**: REQUIRE OPT-IN - `~allow_none:bool` parameter to `verify` (default false). Unsecured JWTs rejected unless explicitly allowed. 124 + 125 + 4. **Error types**: STRUCTURED - Proper error variant type for pattern matching and error handling. 126 + 127 + 5. **Algorithm allowlist**: YES - `~allowed_algs` parameter to `verify`, defaulting to all algorithms (except none). 128 + 129 + 6. **Clock source**: EXPLICIT - Always requires `~now:Ptime.t` parameter. No implicit system clock usage. 130 + 131 + 7. **Nested JWTs**: YES - Support via `parse_nested` with `~max_depth` protection (default 2). 132 + 133 + --- 134 + 135 + ## File Summary 136 + 137 + | File | Lines | Description | 138 + |------|-------|-------------| 139 + | `lib/jwt.ml` | ~1000 | Full implementation | 140 + | `lib/jwt.mli` | ~470 | Interface with RFC documentation | 141 + | `test/test_jwt.ml` | ~440 | 30 comprehensive tests | 142 + 143 + --- 144 + 145 + ## References 146 + 147 + - [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519) - JSON Web Token (JWT) 148 + - [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515) - JSON Web Signature (JWS) 149 + - [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517) - JSON Web Key (JWK) 150 + - [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518) - JSON Web Algorithms (JWA) 151 + - [RFC 8037](https://datatracker.ietf.org/doc/html/rfc8037) - CFRG Elliptic Curve (EdDSA)
+1
ocaml-jsonwt/dune
··· 1 + (data_only_dirs third_party)
+41
ocaml-jsonwt/dune-project
··· 1 + (lang dune 3.20) 2 + 3 + (name jsonwt) 4 + 5 + (generate_opam_files true) 6 + 7 + (license ISC) 8 + 9 + (authors "Anil Madhavapeddy") 10 + 11 + (homepage "https://tangled.org/@anil.recoil.org/ocaml-jsonwt") 12 + 13 + (maintainers "Anil Madhavapeddy <anil@recoil.org>") 14 + 15 + (bug_reports "https://tangled.org/@anil.recoil.org/ocaml-jsonwt/issues") 16 + 17 + (maintenance_intent "(latest)") 18 + 19 + (package 20 + (name jsonwt) 21 + (synopsis "JSON Web Token (JWT) implementation for OCaml") 22 + (description 23 + "An implementation of RFC 7519 JSON Web Tokens (JWT) for OCaml. 24 + Supports JWT parsing, validation, and creation with HS256, RS256, 25 + ES256, and EdDSA signature algorithms. Also includes JWK (RFC 7517) 26 + support for key representation.") 27 + (depends 28 + (ocaml (>= 5.1)) 29 + (jsont (>= 0.2.0)) 30 + (bytesrw (>= 0.1.0)) 31 + (mirage-crypto (>= 1.0.0)) 32 + (mirage-crypto-pk (>= 1.0.0)) 33 + (mirage-crypto-ec (>= 1.0.0)) 34 + (mirage-crypto-rng (>= 1.0.0)) 35 + (digestif (>= 1.0.0)) 36 + (eqaf (>= 0.9)) 37 + (cstruct (>= 6.0.0)) 38 + (base64 (>= 3.0.0)) 39 + (ptime (>= 1.0.0)) 40 + (alcotest :with-test) 41 + (odoc :with-doc)))
+45
ocaml-jsonwt/jsonwt.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "JSON Web Token (JWT) implementation for OCaml" 4 + description: """ 5 + An implementation of RFC 7519 JSON Web Tokens (JWT) for OCaml. 6 + Supports JWT parsing, validation, and creation with HS256, RS256, 7 + ES256, and EdDSA signature algorithms. Also includes JWK (RFC 7517) 8 + support for key representation.""" 9 + maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 10 + authors: ["Anil Madhavapeddy"] 11 + license: "ISC" 12 + homepage: "https://tangled.org/@anil.recoil.org/ocaml-jsonwt" 13 + bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-jsonwt/issues" 14 + depends: [ 15 + "dune" {>= "3.20"} 16 + "ocaml" {>= "5.1"} 17 + "jsont" {>= "0.2.0"} 18 + "bytesrw" {>= "0.1.0"} 19 + "mirage-crypto" {>= "1.0.0"} 20 + "mirage-crypto-pk" {>= "1.0.0"} 21 + "mirage-crypto-ec" {>= "1.0.0"} 22 + "mirage-crypto-rng" {>= "1.0.0"} 23 + "digestif" {>= "1.0.0"} 24 + "eqaf" {>= "0.9"} 25 + "cstruct" {>= "6.0.0"} 26 + "base64" {>= "3.0.0"} 27 + "ptime" {>= "1.0.0"} 28 + "alcotest" {with-test} 29 + "odoc" {with-doc} 30 + ] 31 + build: [ 32 + ["dune" "subst"] {dev} 33 + [ 34 + "dune" 35 + "build" 36 + "-p" 37 + name 38 + "-j" 39 + jobs 40 + "@install" 41 + "@runtest" {with-test} 42 + "@doc" {with-doc} 43 + ] 44 + ] 45 + x-maintenance-intent: ["(latest)"]
+16
ocaml-jsonwt/lib/dune
··· 1 + (library 2 + (name jsonwt) 3 + (public_name jsonwt) 4 + (libraries 5 + jsont 6 + jsont.bytesrw 7 + bytesrw 8 + mirage-crypto 9 + mirage-crypto-pk 10 + mirage-crypto-ec 11 + mirage-crypto-rng 12 + digestif 13 + eqaf 14 + cstruct 15 + base64 16 + ptime))
+1006
ocaml-jsonwt/lib/jsonwt.ml
··· 1 + (** JSON Web Token (JWT) - RFC 7519 *) 2 + 3 + (* Error types *) 4 + type error = 5 + | Invalid_json of string 6 + | Invalid_base64url of string 7 + | Invalid_structure of string 8 + | Invalid_header of string 9 + | Invalid_claims of string 10 + | Invalid_uri of string 11 + | Duplicate_claim of string 12 + | Unsupported_algorithm of string 13 + | Algorithm_not_allowed of string 14 + | Signature_mismatch 15 + | Token_expired 16 + | Token_not_yet_valid 17 + | Invalid_issuer 18 + | Invalid_audience 19 + | Key_type_mismatch of string 20 + | Unsecured_not_allowed 21 + | Nesting_too_deep 22 + 23 + let pp_error fmt = function 24 + | Invalid_json s -> Format.fprintf fmt "Invalid JSON: %s" s 25 + | Invalid_base64url s -> Format.fprintf fmt "Invalid base64url: %s" s 26 + | Invalid_structure s -> Format.fprintf fmt "Invalid structure: %s" s 27 + | Invalid_header s -> Format.fprintf fmt "Invalid header: %s" s 28 + | Invalid_claims s -> Format.fprintf fmt "Invalid claims: %s" s 29 + | Invalid_uri s -> Format.fprintf fmt "Invalid URI: %s" s 30 + | Duplicate_claim s -> Format.fprintf fmt "Duplicate claim: %s" s 31 + | Unsupported_algorithm s -> Format.fprintf fmt "Unsupported algorithm: %s" s 32 + | Algorithm_not_allowed s -> Format.fprintf fmt "Algorithm not allowed: %s" s 33 + | Signature_mismatch -> Format.fprintf fmt "Signature mismatch" 34 + | Token_expired -> Format.fprintf fmt "Token expired" 35 + | Token_not_yet_valid -> Format.fprintf fmt "Token not yet valid" 36 + | Invalid_issuer -> Format.fprintf fmt "Invalid issuer" 37 + | Invalid_audience -> Format.fprintf fmt "Invalid audience" 38 + | Key_type_mismatch s -> Format.fprintf fmt "Key type mismatch: %s" s 39 + | Unsecured_not_allowed -> Format.fprintf fmt "Unsecured JWT not allowed" 40 + | Nesting_too_deep -> Format.fprintf fmt "Nested JWT too deep" 41 + 42 + let error_to_string e = 43 + Format.asprintf "%a" pp_error e 44 + 45 + (* Base64url encoding/decoding per RFC 7515 Appendix C *) 46 + let base64url_encode s = 47 + Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet s 48 + 49 + let base64url_decode s = 50 + (* Add padding if needed *) 51 + let len = String.length s in 52 + let pad_len = (4 - (len mod 4)) mod 4 in 53 + let padded = s ^ String.make pad_len '=' in 54 + match Base64.decode ~alphabet:Base64.uri_safe_alphabet padded with 55 + | Ok v -> Ok v 56 + | Error (`Msg m) -> Error (Invalid_base64url m) 57 + 58 + (* StringOrURI validation per RFC 7519 Section 2 *) 59 + let validate_string_or_uri s = 60 + if String.contains s ':' then 61 + (* Must be a valid URI - basic check for scheme *) 62 + match String.index_opt s ':' with 63 + | Some i when i > 0 -> 64 + let scheme = String.sub s 0 i in 65 + (* Check scheme is alphanumeric with +.- allowed after first char *) 66 + let valid_scheme = 67 + String.length scheme > 0 && 68 + (match scheme.[0] with 'a'..'z' | 'A'..'Z' -> true | _ -> false) && 69 + String.for_all (fun c -> 70 + match c with 71 + | 'a'..'z' | 'A'..'Z' | '0'..'9' | '+' | '-' | '.' -> true 72 + | _ -> false 73 + ) scheme 74 + in 75 + if valid_scheme then Ok s 76 + else Error (Invalid_uri (Printf.sprintf "Invalid URI scheme in: %s" s)) 77 + | _ -> Error (Invalid_uri (Printf.sprintf "Invalid URI: %s" s)) 78 + else 79 + Ok s 80 + 81 + (* Algorithm module *) 82 + module Algorithm = struct 83 + type t = 84 + | None 85 + | HS256 86 + | HS384 87 + | HS512 88 + | RS256 89 + | RS384 90 + | RS512 91 + | ES256 92 + | ES384 93 + | ES512 94 + | EdDSA 95 + 96 + let to_string = function 97 + | None -> "none" 98 + | HS256 -> "HS256" 99 + | HS384 -> "HS384" 100 + | HS512 -> "HS512" 101 + | RS256 -> "RS256" 102 + | RS384 -> "RS384" 103 + | RS512 -> "RS512" 104 + | ES256 -> "ES256" 105 + | ES384 -> "ES384" 106 + | ES512 -> "ES512" 107 + | EdDSA -> "EdDSA" 108 + 109 + let of_string = function 110 + | "none" -> Ok None 111 + | "HS256" -> Ok HS256 112 + | "HS384" -> Ok HS384 113 + | "HS512" -> Ok HS512 114 + | "RS256" -> Ok RS256 115 + | "RS384" -> Ok RS384 116 + | "RS512" -> Ok RS512 117 + | "ES256" -> Ok ES256 118 + | "ES384" -> Ok ES384 119 + | "ES512" -> Ok ES512 120 + | "EdDSA" -> Ok EdDSA 121 + | s -> Error (Unsupported_algorithm s) 122 + 123 + let all = [ HS256; HS384; HS512; RS256; RS384; RS512; ES256; ES384; ES512; EdDSA ] 124 + let all_with_none = None :: all 125 + end 126 + 127 + (* JWK module *) 128 + module Jwk = struct 129 + type kty = Oct | Rsa | Ec | Okp 130 + 131 + type crv = P256 | P384 | P521 | Ed25519 132 + 133 + type key_data = 134 + | Symmetric of { k : string } 135 + | Ed25519_pub of { x : string } 136 + | Ed25519_priv of { x : string; d : string } 137 + | P256_pub of { x : string; y : string } 138 + | P256_priv of { x : string; y : string; d : string } 139 + | P384_pub of { x : string; y : string } 140 + | P384_priv of { x : string; y : string; d : string } 141 + | P521_pub of { x : string; y : string } 142 + | P521_priv of { x : string; y : string; d : string } 143 + | Rsa_pub of { n : string; e : string } 144 + | Rsa_priv of { 145 + n : string; 146 + e : string; 147 + d : string; 148 + p : string; 149 + q : string; 150 + dp : string; 151 + dq : string; 152 + qi : string; 153 + } 154 + 155 + type t = { 156 + key_data : key_data; 157 + kid : string option; 158 + alg : Algorithm.t option; 159 + } 160 + 161 + let symmetric k = { key_data = Symmetric { k }; kid = None; alg = None } 162 + 163 + let ed25519_pub x = 164 + { key_data = Ed25519_pub { x }; kid = None; alg = Some Algorithm.EdDSA } 165 + 166 + let ed25519_priv ~pub ~priv = 167 + { key_data = Ed25519_priv { x = pub; d = priv }; kid = None; alg = Some Algorithm.EdDSA } 168 + 169 + let p256_pub ~x ~y = 170 + { key_data = P256_pub { x; y }; kid = None; alg = Some Algorithm.ES256 } 171 + 172 + let p256_priv ~x ~y ~d = 173 + { key_data = P256_priv { x; y; d }; kid = None; alg = Some Algorithm.ES256 } 174 + 175 + let p384_pub ~x ~y = 176 + { key_data = P384_pub { x; y }; kid = None; alg = Some Algorithm.ES384 } 177 + 178 + let p384_priv ~x ~y ~d = 179 + { key_data = P384_priv { x; y; d }; kid = None; alg = Some Algorithm.ES384 } 180 + 181 + let p521_pub ~x ~y = 182 + { key_data = P521_pub { x; y }; kid = None; alg = Some Algorithm.ES512 } 183 + 184 + let p521_priv ~x ~y ~d = 185 + { key_data = P521_priv { x; y; d }; kid = None; alg = Some Algorithm.ES512 } 186 + 187 + let rsa_pub ~n ~e = 188 + { key_data = Rsa_pub { n; e }; kid = None; alg = Some Algorithm.RS256 } 189 + 190 + let rsa_priv ~n ~e ~d ~p ~q ~dp ~dq ~qi = 191 + { key_data = Rsa_priv { n; e; d; p; q; dp; dq; qi }; kid = None; alg = Some Algorithm.RS256 } 192 + 193 + let kty t = 194 + match t.key_data with 195 + | Symmetric _ -> Oct 196 + | Ed25519_pub _ | Ed25519_priv _ -> Okp 197 + | P256_pub _ | P256_priv _ | P384_pub _ | P384_priv _ | P521_pub _ | P521_priv _ -> Ec 198 + | Rsa_pub _ | Rsa_priv _ -> Rsa 199 + 200 + let kid t = t.kid 201 + let alg t = t.alg 202 + 203 + let with_kid id t = { t with kid = Some id } 204 + let with_alg a t = { t with alg = Some a } 205 + 206 + (* Helper to extract string from Jsont.json object members *) 207 + let get_json_string members name = 208 + List.find_map (fun ((n, _), v) -> 209 + if n = name then 210 + match v with 211 + | Jsont.String (s, _) -> Some s 212 + | _ -> None 213 + else None 214 + ) members 215 + 216 + let get_json_string_req members name = 217 + match get_json_string members name with 218 + | Some s -> Ok s 219 + | None -> Error (Invalid_json (Printf.sprintf "missing required field: %s" name)) 220 + 221 + let of_json s = 222 + (* Parse the JSON to determine key type first *) 223 + match Jsont_bytesrw.decode_string Jsont.json s with 224 + | Error e -> Error (Invalid_json e) 225 + | Ok (Jsont.Null _) -> Error (Invalid_json "null is not a valid JWK") 226 + | Ok (Jsont.Object (members, _)) -> 227 + let ( let* ) = Result.bind in 228 + let* kty_s = get_json_string_req members "kty" in 229 + let kid = get_json_string members "kid" in 230 + let alg_opt = 231 + match get_json_string members "alg" with 232 + | None -> Ok None 233 + | Some s -> 234 + match Algorithm.of_string s with 235 + | Ok a -> Ok (Some a) 236 + | Error _ -> Ok None (* ignore unknown alg in JWK *) 237 + in 238 + let* alg = alg_opt in 239 + (match kty_s with 240 + | "oct" -> 241 + let* k_b64 = get_json_string_req members "k" in 242 + let* k = base64url_decode k_b64 in 243 + Ok { key_data = Symmetric { k }; kid; alg } 244 + | "OKP" -> 245 + let* crv = get_json_string_req members "crv" in 246 + if crv <> "Ed25519" then 247 + Error (Invalid_json (Printf.sprintf "unsupported curve: %s" crv)) 248 + else 249 + let* x_b64 = get_json_string_req members "x" in 250 + let* x = base64url_decode x_b64 in 251 + (match get_json_string members "d" with 252 + | None -> Ok { key_data = Ed25519_pub { x }; kid; alg } 253 + | Some d_b64 -> 254 + let* d = base64url_decode d_b64 in 255 + Ok { key_data = Ed25519_priv { x; d }; kid; alg }) 256 + | "EC" -> 257 + let* crv = get_json_string_req members "crv" in 258 + let* x_b64 = get_json_string_req members "x" in 259 + let* y_b64 = get_json_string_req members "y" in 260 + let* x = base64url_decode x_b64 in 261 + let* y = base64url_decode y_b64 in 262 + let has_d = Option.is_some (get_json_string members "d") in 263 + let get_d () = 264 + let* d_b64 = get_json_string_req members "d" in 265 + base64url_decode d_b64 266 + in 267 + (match crv with 268 + | "P-256" -> 269 + if has_d then 270 + let* d = get_d () in 271 + Ok { key_data = P256_priv { x; y; d }; kid; alg } 272 + else 273 + Ok { key_data = P256_pub { x; y }; kid; alg } 274 + | "P-384" -> 275 + if has_d then 276 + let* d = get_d () in 277 + Ok { key_data = P384_priv { x; y; d }; kid; alg } 278 + else 279 + Ok { key_data = P384_pub { x; y }; kid; alg } 280 + | "P-521" -> 281 + if has_d then 282 + let* d = get_d () in 283 + Ok { key_data = P521_priv { x; y; d }; kid; alg } 284 + else 285 + Ok { key_data = P521_pub { x; y }; kid; alg } 286 + | _ -> Error (Invalid_json (Printf.sprintf "unsupported curve: %s" crv))) 287 + | "RSA" -> 288 + let* n_b64 = get_json_string_req members "n" in 289 + let* e_b64 = get_json_string_req members "e" in 290 + let* n = base64url_decode n_b64 in 291 + let* e = base64url_decode e_b64 in 292 + (match get_json_string members "d" with 293 + | None -> Ok { key_data = Rsa_pub { n; e }; kid; alg } 294 + | Some d_b64 -> 295 + let* d = base64url_decode d_b64 in 296 + let* p_b64 = get_json_string_req members "p" in 297 + let* q_b64 = get_json_string_req members "q" in 298 + let* dp_b64 = get_json_string_req members "dp" in 299 + let* dq_b64 = get_json_string_req members "dq" in 300 + let* qi_b64 = get_json_string_req members "qi" in 301 + let* p = base64url_decode p_b64 in 302 + let* q = base64url_decode q_b64 in 303 + let* dp = base64url_decode dp_b64 in 304 + let* dq = base64url_decode dq_b64 in 305 + let* qi = base64url_decode qi_b64 in 306 + Ok { key_data = Rsa_priv { n; e; d; p; q; dp; dq; qi }; kid; alg }) 307 + | _ -> Error (Invalid_json (Printf.sprintf "unsupported kty: %s" kty_s))) 308 + | Ok _ -> Error (Invalid_json "JWK must be a JSON object") 309 + 310 + (* Helper to create JSON members *) 311 + let meta = Jsont.Meta.none 312 + let json_string s = Jsont.String (s, meta) 313 + let json_mem name value = ((name, meta), value) 314 + 315 + let to_json t = 316 + let add_opt name v_opt acc = 317 + match v_opt with 318 + | None -> acc 319 + | Some v -> json_mem name (json_string v) :: acc 320 + in 321 + let members = [] in 322 + let members = add_opt "kid" t.kid members in 323 + let members = add_opt "alg" (Option.map Algorithm.to_string t.alg) members in 324 + let members = 325 + match t.key_data with 326 + | Symmetric { k } -> 327 + json_mem "kty" (json_string "oct") :: 328 + json_mem "k" (json_string (base64url_encode k)) :: members 329 + | Ed25519_pub { x } -> 330 + json_mem "kty" (json_string "OKP") :: 331 + json_mem "crv" (json_string "Ed25519") :: 332 + json_mem "x" (json_string (base64url_encode x)) :: members 333 + | Ed25519_priv { x; d } -> 334 + json_mem "kty" (json_string "OKP") :: 335 + json_mem "crv" (json_string "Ed25519") :: 336 + json_mem "x" (json_string (base64url_encode x)) :: 337 + json_mem "d" (json_string (base64url_encode d)) :: members 338 + | P256_pub { x; y } -> 339 + json_mem "kty" (json_string "EC") :: 340 + json_mem "crv" (json_string "P-256") :: 341 + json_mem "x" (json_string (base64url_encode x)) :: 342 + json_mem "y" (json_string (base64url_encode y)) :: members 343 + | P256_priv { x; y; d } -> 344 + json_mem "kty" (json_string "EC") :: 345 + json_mem "crv" (json_string "P-256") :: 346 + json_mem "x" (json_string (base64url_encode x)) :: 347 + json_mem "y" (json_string (base64url_encode y)) :: 348 + json_mem "d" (json_string (base64url_encode d)) :: members 349 + | P384_pub { x; y } -> 350 + json_mem "kty" (json_string "EC") :: 351 + json_mem "crv" (json_string "P-384") :: 352 + json_mem "x" (json_string (base64url_encode x)) :: 353 + json_mem "y" (json_string (base64url_encode y)) :: members 354 + | P384_priv { x; y; d } -> 355 + json_mem "kty" (json_string "EC") :: 356 + json_mem "crv" (json_string "P-384") :: 357 + json_mem "x" (json_string (base64url_encode x)) :: 358 + json_mem "y" (json_string (base64url_encode y)) :: 359 + json_mem "d" (json_string (base64url_encode d)) :: members 360 + | P521_pub { x; y } -> 361 + json_mem "kty" (json_string "EC") :: 362 + json_mem "crv" (json_string "P-521") :: 363 + json_mem "x" (json_string (base64url_encode x)) :: 364 + json_mem "y" (json_string (base64url_encode y)) :: members 365 + | P521_priv { x; y; d } -> 366 + json_mem "kty" (json_string "EC") :: 367 + json_mem "crv" (json_string "P-521") :: 368 + json_mem "x" (json_string (base64url_encode x)) :: 369 + json_mem "y" (json_string (base64url_encode y)) :: 370 + json_mem "d" (json_string (base64url_encode d)) :: members 371 + | Rsa_pub { n; e } -> 372 + json_mem "kty" (json_string "RSA") :: 373 + json_mem "n" (json_string (base64url_encode n)) :: 374 + json_mem "e" (json_string (base64url_encode e)) :: members 375 + | Rsa_priv { n; e; d; p; q; dp; dq; qi } -> 376 + json_mem "kty" (json_string "RSA") :: 377 + json_mem "n" (json_string (base64url_encode n)) :: 378 + json_mem "e" (json_string (base64url_encode e)) :: 379 + json_mem "d" (json_string (base64url_encode d)) :: 380 + json_mem "p" (json_string (base64url_encode p)) :: 381 + json_mem "q" (json_string (base64url_encode q)) :: 382 + json_mem "dp" (json_string (base64url_encode dp)) :: 383 + json_mem "dq" (json_string (base64url_encode dq)) :: 384 + json_mem "qi" (json_string (base64url_encode qi)) :: members 385 + in 386 + match Jsont_bytesrw.encode_string Jsont.json (Jsont.Object (members, meta)) with 387 + | Ok s -> s 388 + | Error _ -> "{}" (* Should not happen *) 389 + end 390 + 391 + (* Header module *) 392 + module Header = struct 393 + type t = { 394 + alg : Algorithm.t; 395 + typ : string option; 396 + kid : string option; 397 + cty : string option; 398 + } 399 + 400 + let make ?typ ?kid ?cty alg = { alg; typ; kid; cty } 401 + 402 + let is_nested t = 403 + match t.cty with 404 + | Some s -> String.uppercase_ascii s = "JWT" 405 + | None -> false 406 + 407 + (* Helper to extract string from Jsont.json object members *) 408 + let get_json_string members name = 409 + List.find_map (fun ((n, _), v) -> 410 + if n = name then 411 + match v with 412 + | Jsont.String (s, _) -> Some s 413 + | _ -> None 414 + else None 415 + ) members 416 + 417 + let of_json s = 418 + match Jsont_bytesrw.decode_string Jsont.json s with 419 + | Error e -> Error (Invalid_json e) 420 + | Ok (Jsont.Null _) -> Error (Invalid_header "null is not a valid header") 421 + | Ok (Jsont.Object (members, _)) -> 422 + let ( let* ) = Result.bind in 423 + let alg_s = get_json_string members "alg" in 424 + (match alg_s with 425 + | None -> Error (Invalid_header "missing required 'alg' field") 426 + | Some alg_str -> 427 + let* alg = Algorithm.of_string alg_str in 428 + let typ = get_json_string members "typ" in 429 + let kid = get_json_string members "kid" in 430 + let cty = get_json_string members "cty" in 431 + Ok { alg; typ; kid; cty }) 432 + | Ok _ -> Error (Invalid_header "header must be a JSON object") 433 + 434 + let meta = Jsont.Meta.none 435 + let json_string s = Jsont.String (s, meta) 436 + let json_mem name value = ((name, meta), value) 437 + 438 + let to_json h = 439 + let members = [ json_mem "alg" (json_string (Algorithm.to_string h.alg)) ] in 440 + let add_opt name v_opt acc = 441 + match v_opt with 442 + | None -> acc 443 + | Some v -> json_mem name (json_string v) :: acc 444 + in 445 + let members = add_opt "typ" h.typ members in 446 + let members = add_opt "kid" h.kid members in 447 + let members = add_opt "cty" h.cty members in 448 + match Jsont_bytesrw.encode_string Jsont.json (Jsont.Object (List.rev members, meta)) with 449 + | Ok s -> s 450 + | Error _ -> "{}" 451 + end 452 + 453 + (* Claims module *) 454 + module Claims = struct 455 + type t = { 456 + iss : string option; 457 + sub : string option; 458 + aud : string list; 459 + exp : Ptime.t option; 460 + nbf : Ptime.t option; 461 + iat : Ptime.t option; 462 + jti : string option; 463 + custom : (string * Jsont.json) list; 464 + } 465 + 466 + let iss t = t.iss 467 + let sub t = t.sub 468 + let aud t = t.aud 469 + let exp t = t.exp 470 + let nbf t = t.nbf 471 + let iat t = t.iat 472 + let jti t = t.jti 473 + 474 + let get name t = List.assoc_opt name t.custom 475 + 476 + let get_string name t = 477 + match get name t with 478 + | Some (Jsont.String (s, _)) -> Some s 479 + | _ -> None 480 + 481 + let get_int name t = 482 + match get name t with 483 + | Some (Jsont.Number (n, _)) -> (try Some (int_of_float n) with _ -> None) 484 + | _ -> None 485 + 486 + let get_bool name t = 487 + match get name t with 488 + | Some (Jsont.Bool (b, _)) -> Some b 489 + | _ -> None 490 + 491 + let meta = Jsont.Meta.none 492 + let json_string s = Jsont.String (s, meta) 493 + let json_number n = Jsont.Number (n, meta) 494 + let json_bool b = Jsont.Bool (b, meta) 495 + let json_mem name value = ((name, meta), value) 496 + 497 + type builder = t 498 + 499 + let empty = { 500 + iss = None; 501 + sub = None; 502 + aud = []; 503 + exp = None; 504 + nbf = None; 505 + iat = None; 506 + jti = None; 507 + custom = []; 508 + } 509 + 510 + let set_iss v t = { t with iss = Some v } 511 + let set_sub v t = { t with sub = Some v } 512 + let set_aud v t = { t with aud = v } 513 + let set_exp v t = { t with exp = Some v } 514 + let set_nbf v t = { t with nbf = Some v } 515 + let set_iat v t = { t with iat = Some v } 516 + let set_jti v t = { t with jti = Some v } 517 + let set name value t = { t with custom = (name, value) :: t.custom } 518 + let set_string name value t = set name (json_string value) t 519 + let set_int name value t = set name (json_number (float_of_int value)) t 520 + let set_bool name value t = set name (json_bool value) t 521 + let build t = t 522 + 523 + let ptime_of_numeric_date n = 524 + let span = Ptime.Span.of_float_s n in 525 + Option.bind span (fun s -> Ptime.of_span s) 526 + 527 + let numeric_date_of_ptime t = 528 + Ptime.to_span t |> Ptime.Span.to_float_s 529 + 530 + (* Helper to extract values from Jsont.json object members *) 531 + let get_json_string members name = 532 + List.find_map (fun ((n, _), v) -> 533 + if n = name then 534 + match v with 535 + | Jsont.String (s, _) -> Some s 536 + | _ -> None 537 + else None 538 + ) members 539 + 540 + let get_json_number members name = 541 + List.find_map (fun ((n, _), v) -> 542 + if n = name then 543 + match v with 544 + | Jsont.Number (n, _) -> Some n 545 + | _ -> None 546 + else None 547 + ) members 548 + 549 + let get_json_aud members = 550 + List.find_map (fun ((n, _), v) -> 551 + if n = "aud" then 552 + match v with 553 + | Jsont.String (s, _) -> Some [ s ] 554 + | Jsont.Array (arr, _) -> 555 + Some (List.filter_map (function 556 + | Jsont.String (s, _) -> Some s 557 + | _ -> None 558 + ) arr) 559 + | _ -> None 560 + else None 561 + ) members |> Option.value ~default:[] 562 + 563 + let of_json ?(strict = true) s = 564 + match Jsont_bytesrw.decode_string Jsont.json s with 565 + | Error e -> Error (Invalid_json e) 566 + | Ok (Jsont.Null _) -> Error (Invalid_claims "null is not a valid claims set") 567 + | Ok (Jsont.Object (members, _)) -> 568 + let ( let* ) = Result.bind in 569 + (* Check for duplicates in strict mode *) 570 + let* () = 571 + if strict then 572 + let names = List.map (fun ((n, _), _) -> n) members in 573 + let rec check_dups = function 574 + | [] -> Ok () 575 + | n :: rest -> 576 + if List.mem n rest then Error (Duplicate_claim n) 577 + else check_dups rest 578 + in 579 + check_dups names 580 + else Ok () 581 + in 582 + (* Validate StringOrURI for iss and sub *) 583 + let* iss = 584 + match get_json_string members "iss" with 585 + | None -> Ok None 586 + | Some s -> 587 + let* _ = validate_string_or_uri s in 588 + Ok (Some s) 589 + in 590 + let* sub = 591 + match get_json_string members "sub" with 592 + | None -> Ok None 593 + | Some s -> 594 + let* _ = validate_string_or_uri s in 595 + Ok (Some s) 596 + in 597 + let exp = Option.bind (get_json_number members "exp") ptime_of_numeric_date in 598 + let nbf = Option.bind (get_json_number members "nbf") ptime_of_numeric_date in 599 + let iat = Option.bind (get_json_number members "iat") ptime_of_numeric_date in 600 + let jti = get_json_string members "jti" in 601 + let aud = get_json_aud members in 602 + (* Collect custom claims (everything not registered) *) 603 + let registered = [ "iss"; "sub"; "aud"; "exp"; "nbf"; "iat"; "jti" ] in 604 + let custom = 605 + List.filter_map (fun ((n, _), v) -> 606 + if List.mem n registered then None 607 + else Some (n, v) 608 + ) members 609 + in 610 + Ok { iss; sub; aud; exp; nbf; iat; jti; custom } 611 + | Ok _ -> Error (Invalid_claims "claims must be a JSON object") 612 + 613 + let to_json t = 614 + let members = [] in 615 + let add_string name v_opt acc = 616 + match v_opt with 617 + | None -> acc 618 + | Some v -> json_mem name (json_string v) :: acc 619 + in 620 + let add_time name v_opt acc = 621 + match v_opt with 622 + | None -> acc 623 + | Some v -> json_mem name (json_number (numeric_date_of_ptime v)) :: acc 624 + in 625 + let members = add_string "iss" t.iss members in 626 + let members = add_string "sub" t.sub members in 627 + let members = 628 + match t.aud with 629 + | [] -> members 630 + | [ single ] -> json_mem "aud" (json_string single) :: members 631 + | many -> 632 + let arr = List.map json_string many in 633 + json_mem "aud" (Jsont.Array (arr, meta)) :: members 634 + in 635 + let members = add_time "exp" t.exp members in 636 + let members = add_time "nbf" t.nbf members in 637 + let members = add_time "iat" t.iat members in 638 + let members = add_string "jti" t.jti members in 639 + let members = 640 + List.fold_left (fun acc (name, value) -> 641 + json_mem name value :: acc 642 + ) members t.custom 643 + in 644 + match Jsont_bytesrw.encode_string Jsont.json (Jsont.Object (List.rev members, meta)) with 645 + | Ok s -> s 646 + | Error _ -> "{}" 647 + end 648 + 649 + (* JWT type *) 650 + type t = { 651 + header : Header.t; 652 + claims : Claims.t; 653 + signature : string; 654 + raw : string; 655 + } 656 + 657 + let header t = t.header 658 + let claims t = t.claims 659 + let signature t = t.signature 660 + let raw t = t.raw 661 + 662 + let is_nested t = Header.is_nested t.header 663 + 664 + (* Parsing *) 665 + let parse ?(strict = true) token = 666 + let ( let* ) = Result.bind in 667 + (* RFC 7519 Section 7.2 step 1: verify at least one period *) 668 + if not (String.contains token '.') then 669 + Error (Invalid_structure "JWT must contain at least one period character") 670 + else 671 + match String.split_on_char '.' token with 672 + | [ header_b64; payload_b64; sig_b64 ] -> 673 + (* JWS compact serialization: 3 parts *) 674 + let* header_json = base64url_decode header_b64 in 675 + let* payload_json = base64url_decode payload_b64 in 676 + let* signature = base64url_decode sig_b64 in 677 + let* header = Header.of_json header_json in 678 + let* claims = Claims.of_json ~strict payload_json in 679 + Ok { header; claims; signature; raw = token } 680 + | parts when List.length parts = 5 -> 681 + (* JWE compact serialization - not yet supported *) 682 + Error (Invalid_structure "JWE (encrypted JWT) not yet supported") 683 + | _ -> 684 + Error (Invalid_structure "JWT must have 3 parts (JWS) or 5 parts (JWE)") 685 + 686 + let parse_unsafe = parse ~strict:false 687 + 688 + let parse_nested ?(strict = true) ?(max_depth = 5) token = 689 + let ( let* ) = Result.bind in 690 + let rec loop depth acc tok = 691 + if depth > max_depth then 692 + Error Nesting_too_deep 693 + else 694 + let* jwt = parse ~strict tok in 695 + let acc = jwt :: acc in 696 + if is_nested jwt then 697 + (* The payload is another JWT - decode and parse it *) 698 + match String.split_on_char '.' tok with 699 + | [ _; payload_b64; _ ] -> 700 + let* inner_token = base64url_decode payload_b64 in 701 + loop (depth + 1) acc inner_token 702 + | _ -> Ok (List.rev acc) 703 + else 704 + Ok (List.rev acc) 705 + in 706 + loop 1 [] token 707 + 708 + (* Signature operations *) 709 + module Sign = struct 710 + let hmac_sha256 ~key data = 711 + let key = Cstruct.of_string key in 712 + let data = Cstruct.of_string data in 713 + Digestif.SHA256.hmac_string ~key:(Cstruct.to_string key) (Cstruct.to_string data) 714 + |> Digestif.SHA256.to_raw_string 715 + 716 + let hmac_sha384 ~key data = 717 + let key = Cstruct.of_string key in 718 + let data = Cstruct.of_string data in 719 + Digestif.SHA384.hmac_string ~key:(Cstruct.to_string key) (Cstruct.to_string data) 720 + |> Digestif.SHA384.to_raw_string 721 + 722 + let hmac_sha512 ~key data = 723 + let key = Cstruct.of_string key in 724 + let data = Cstruct.of_string data in 725 + Digestif.SHA512.hmac_string ~key:(Cstruct.to_string key) (Cstruct.to_string data) 726 + |> Digestif.SHA512.to_raw_string 727 + 728 + (* EdDSA signing using mirage-crypto-ec *) 729 + let ed25519_sign ~priv data = 730 + match Mirage_crypto_ec.Ed25519.priv_of_octets priv with 731 + | Error _ -> Error (Key_type_mismatch "Invalid Ed25519 private key") 732 + | Ok priv -> 733 + let sig_ = Mirage_crypto_ec.Ed25519.sign ~key:priv data in 734 + Ok sig_ 735 + 736 + let ed25519_verify ~pub ~signature data = 737 + match Mirage_crypto_ec.Ed25519.pub_of_octets pub with 738 + | Error _ -> Error (Key_type_mismatch "Invalid Ed25519 public key") 739 + | Ok pub -> 740 + let valid = Mirage_crypto_ec.Ed25519.verify ~key:pub signature ~msg:data in 741 + if valid then Ok () else Error Signature_mismatch 742 + 743 + (* P-256 ECDSA *) 744 + let p256_sign ~priv data = 745 + match Mirage_crypto_ec.P256.Dsa.priv_of_octets priv with 746 + | Error _ -> Error (Key_type_mismatch "Invalid P-256 private key") 747 + | Ok priv -> 748 + let hash = Digestif.SHA256.digest_string data |> Digestif.SHA256.to_raw_string in 749 + let (r, s) = Mirage_crypto_ec.P256.Dsa.sign ~key:priv hash in 750 + (* JWS uses raw R||S format, each 32 bytes for P-256 *) 751 + (* Pad to 32 bytes each *) 752 + let pad32 s = 753 + let len = String.length s in 754 + if len >= 32 then String.sub s (len - 32) 32 755 + else String.make (32 - len) '\x00' ^ s 756 + in 757 + Ok (pad32 r ^ pad32 s) 758 + 759 + let p256_verify ~pub ~signature data = 760 + if String.length signature <> 64 then 761 + Error Signature_mismatch 762 + else 763 + let r = String.sub signature 0 32 in 764 + let s = String.sub signature 32 32 in 765 + match Mirage_crypto_ec.P256.Dsa.pub_of_octets pub with 766 + | Error _ -> Error (Key_type_mismatch "Invalid P-256 public key") 767 + | Ok pub -> 768 + let hash = Digestif.SHA256.digest_string data |> Digestif.SHA256.to_raw_string in 769 + let valid = Mirage_crypto_ec.P256.Dsa.verify ~key:pub (r, s) hash in 770 + if valid then Ok () else Error Signature_mismatch 771 + 772 + (* P-384 ECDSA *) 773 + let p384_sign ~priv data = 774 + match Mirage_crypto_ec.P384.Dsa.priv_of_octets priv with 775 + | Error _ -> Error (Key_type_mismatch "Invalid P-384 private key") 776 + | Ok priv -> 777 + let hash = Digestif.SHA384.digest_string data |> Digestif.SHA384.to_raw_string in 778 + let (r, s) = Mirage_crypto_ec.P384.Dsa.sign ~key:priv hash in 779 + let pad48 s = 780 + let len = String.length s in 781 + if len >= 48 then String.sub s (len - 48) 48 782 + else String.make (48 - len) '\x00' ^ s 783 + in 784 + Ok (pad48 r ^ pad48 s) 785 + 786 + let p384_verify ~pub ~signature data = 787 + if String.length signature <> 96 then 788 + Error Signature_mismatch 789 + else 790 + let r = String.sub signature 0 48 in 791 + let s = String.sub signature 48 48 in 792 + match Mirage_crypto_ec.P384.Dsa.pub_of_octets pub with 793 + | Error _ -> Error (Key_type_mismatch "Invalid P-384 public key") 794 + | Ok pub -> 795 + let hash = Digestif.SHA384.digest_string data |> Digestif.SHA384.to_raw_string in 796 + let valid = Mirage_crypto_ec.P384.Dsa.verify ~key:pub (r, s) hash in 797 + if valid then Ok () else Error Signature_mismatch 798 + 799 + (* P-521 ECDSA *) 800 + let p521_sign ~priv data = 801 + match Mirage_crypto_ec.P521.Dsa.priv_of_octets priv with 802 + | Error _ -> Error (Key_type_mismatch "Invalid P-521 private key") 803 + | Ok priv -> 804 + let hash = Digestif.SHA512.digest_string data |> Digestif.SHA512.to_raw_string in 805 + let (r, s) = Mirage_crypto_ec.P521.Dsa.sign ~key:priv hash in 806 + let pad66 s = 807 + let len = String.length s in 808 + if len >= 66 then String.sub s (len - 66) 66 809 + else String.make (66 - len) '\x00' ^ s 810 + in 811 + Ok (pad66 r ^ pad66 s) 812 + 813 + let p521_verify ~pub ~signature data = 814 + if String.length signature <> 132 then 815 + Error Signature_mismatch 816 + else 817 + let r = String.sub signature 0 66 in 818 + let s = String.sub signature 66 66 in 819 + match Mirage_crypto_ec.P521.Dsa.pub_of_octets pub with 820 + | Error _ -> Error (Key_type_mismatch "Invalid P-521 public key") 821 + | Ok pub -> 822 + let hash = Digestif.SHA512.digest_string data |> Digestif.SHA512.to_raw_string in 823 + let valid = Mirage_crypto_ec.P521.Dsa.verify ~key:pub (r, s) hash in 824 + if valid then Ok () else Error Signature_mismatch 825 + 826 + (* RSA PKCS#1 v1.5 - stub implementations *) 827 + (* TODO: Implement proper RSA signing/verification with JWK key parsing *) 828 + let _rsa_sign _hash_type ~priv:_ _data = 829 + Error (Key_type_mismatch "RSA signing not yet implemented") 830 + 831 + let _rsa_verify _hash_type ~pub:_ ~signature:_ _data = 832 + Error (Key_type_mismatch "RSA verification not yet implemented") 833 + end 834 + 835 + (* Get signing input from token *) 836 + let signing_input token = 837 + match String.rindex_opt token '.' with 838 + | None -> token 839 + | Some i -> String.sub token 0 i 840 + 841 + (* Verification *) 842 + let verify ~key ?(allow_none = false) ?(allowed_algs = Algorithm.all) t = 843 + let ( let* ) = Result.bind in 844 + let alg = t.header.alg in 845 + let alg_str = Algorithm.to_string alg in 846 + (* Check if algorithm is allowed *) 847 + let* () = 848 + if alg = Algorithm.None then 849 + (* For alg:none, only allow_none flag matters *) 850 + if allow_none then Ok () 851 + else Error Unsecured_not_allowed 852 + else if List.mem alg allowed_algs then Ok () 853 + else Error (Algorithm_not_allowed alg_str) 854 + in 855 + let input = signing_input t.raw in 856 + match alg, key.Jwk.key_data with 857 + | Algorithm.None, _ -> 858 + (* Unsecured JWT - signature must be empty *) 859 + if t.signature = "" then Ok () 860 + else Error Signature_mismatch 861 + | Algorithm.HS256, Jwk.Symmetric { k } -> 862 + let expected = Sign.hmac_sha256 ~key:k input in 863 + if Eqaf.equal expected t.signature then Ok () 864 + else Error Signature_mismatch 865 + | Algorithm.HS384, Jwk.Symmetric { k } -> 866 + let expected = Sign.hmac_sha384 ~key:k input in 867 + if Eqaf.equal expected t.signature then Ok () 868 + else Error Signature_mismatch 869 + | Algorithm.HS512, Jwk.Symmetric { k } -> 870 + let expected = Sign.hmac_sha512 ~key:k input in 871 + if Eqaf.equal expected t.signature then Ok () 872 + else Error Signature_mismatch 873 + | Algorithm.EdDSA, Jwk.Ed25519_pub { x } -> 874 + Sign.ed25519_verify ~pub:x ~signature:t.signature input 875 + | Algorithm.EdDSA, Jwk.Ed25519_priv { x; d = _ } -> 876 + Sign.ed25519_verify ~pub:x ~signature:t.signature input 877 + | Algorithm.ES256, Jwk.P256_pub { x; y } -> 878 + let pub = x ^ y in (* Uncompressed point *) 879 + Sign.p256_verify ~pub ~signature:t.signature input 880 + | Algorithm.ES256, Jwk.P256_priv { x; y; d = _ } -> 881 + let pub = x ^ y in 882 + Sign.p256_verify ~pub ~signature:t.signature input 883 + | Algorithm.ES384, Jwk.P384_pub { x; y } -> 884 + let pub = x ^ y in 885 + Sign.p384_verify ~pub ~signature:t.signature input 886 + | Algorithm.ES384, Jwk.P384_priv { x; y; d = _ } -> 887 + let pub = x ^ y in 888 + Sign.p384_verify ~pub ~signature:t.signature input 889 + | Algorithm.ES512, Jwk.P521_pub { x; y } -> 890 + let pub = x ^ y in 891 + Sign.p521_verify ~pub ~signature:t.signature input 892 + | Algorithm.ES512, Jwk.P521_priv { x; y; d = _ } -> 893 + let pub = x ^ y in 894 + Sign.p521_verify ~pub ~signature:t.signature input 895 + | Algorithm.RS256, Jwk.Rsa_pub _ -> 896 + Error (Key_type_mismatch "RSA verification not yet implemented") 897 + | Algorithm.RS384, Jwk.Rsa_pub _ -> 898 + Error (Key_type_mismatch "RSA verification not yet implemented") 899 + | Algorithm.RS512, Jwk.Rsa_pub _ -> 900 + Error (Key_type_mismatch "RSA verification not yet implemented") 901 + | alg, _ -> 902 + Error (Key_type_mismatch 903 + (Printf.sprintf "Key type doesn't match algorithm %s" (Algorithm.to_string alg))) 904 + 905 + (* Claims validation *) 906 + let validate ~now ?iss ?aud ?(leeway = Ptime.Span.zero) t = 907 + let ( let* ) = Result.bind in 908 + let claims = t.claims in 909 + (* Check exp claim *) 910 + let* () = 911 + match Claims.exp claims with 912 + | None -> Ok () 913 + | Some exp_time -> 914 + let exp_with_leeway = Ptime.add_span exp_time leeway |> Option.value ~default:exp_time in 915 + if Ptime.is_later now ~than:exp_with_leeway then 916 + Error Token_expired 917 + else Ok () 918 + in 919 + (* Check nbf claim *) 920 + let* () = 921 + match Claims.nbf claims with 922 + | None -> Ok () 923 + | Some nbf_time -> 924 + let nbf_with_leeway = Ptime.sub_span nbf_time leeway |> Option.value ~default:nbf_time in 925 + if Ptime.is_earlier now ~than:nbf_with_leeway then 926 + Error Token_not_yet_valid 927 + else Ok () 928 + in 929 + (* Check iss claim *) 930 + let* () = 931 + match iss with 932 + | None -> Ok () 933 + | Some expected_iss -> 934 + match Claims.iss claims with 935 + | None -> Error Invalid_issuer 936 + | Some actual_iss -> 937 + if String.equal expected_iss actual_iss then Ok () 938 + else Error Invalid_issuer 939 + in 940 + (* Check aud claim *) 941 + let* () = 942 + match aud with 943 + | None -> Ok () 944 + | Some expected_aud -> 945 + let actual_aud = Claims.aud claims in 946 + if List.mem expected_aud actual_aud then Ok () 947 + else Error Invalid_audience 948 + in 949 + Ok () 950 + 951 + let verify_and_validate ~key ~now ?allow_none ?allowed_algs ?iss ?aud ?leeway t = 952 + let ( let* ) = Result.bind in 953 + let* () = verify ~key ?allow_none ?allowed_algs t in 954 + validate ~now ?iss ?aud ?leeway t 955 + 956 + (* Creation *) 957 + let create ~header ~claims ~key = 958 + let ( let* ) = Result.bind in 959 + let header_json = Header.to_json header in 960 + let claims_json = Claims.to_json claims in 961 + let header_b64 = base64url_encode header_json in 962 + let payload_b64 = base64url_encode claims_json in 963 + let signing_input = header_b64 ^ "." ^ payload_b64 in 964 + let* signature = 965 + match header.Header.alg, key.Jwk.key_data with 966 + | Algorithm.None, _ -> Ok "" 967 + | Algorithm.HS256, Jwk.Symmetric { k } -> 968 + Ok (Sign.hmac_sha256 ~key:k signing_input) 969 + | Algorithm.HS384, Jwk.Symmetric { k } -> 970 + Ok (Sign.hmac_sha384 ~key:k signing_input) 971 + | Algorithm.HS512, Jwk.Symmetric { k } -> 972 + Ok (Sign.hmac_sha512 ~key:k signing_input) 973 + | Algorithm.EdDSA, Jwk.Ed25519_priv { x = _; d } -> 974 + Sign.ed25519_sign ~priv:d signing_input 975 + | Algorithm.ES256, Jwk.P256_priv { x = _; y = _; d } -> 976 + Sign.p256_sign ~priv:d signing_input 977 + | Algorithm.ES384, Jwk.P384_priv { x = _; y = _; d } -> 978 + Sign.p384_sign ~priv:d signing_input 979 + | Algorithm.ES512, Jwk.P521_priv { x = _; y = _; d } -> 980 + Sign.p521_sign ~priv:d signing_input 981 + | alg, _ -> 982 + Error (Key_type_mismatch 983 + (Printf.sprintf "Cannot sign with algorithm %s and given key" 984 + (Algorithm.to_string alg))) 985 + in 986 + let sig_b64 = base64url_encode signature in 987 + let raw = signing_input ^ "." ^ sig_b64 in 988 + Ok { header; claims; signature; raw } 989 + 990 + let encode t = t.raw 991 + 992 + (* Utilities *) 993 + let is_expired ~now ?(leeway = Ptime.Span.zero) t = 994 + match Claims.exp t.claims with 995 + | None -> false 996 + | Some exp_time -> 997 + let exp_with_leeway = Ptime.add_span exp_time leeway |> Option.value ~default:exp_time in 998 + Ptime.is_later now ~than:exp_with_leeway 999 + 1000 + let time_to_expiry ~now t = 1001 + match Claims.exp t.claims with 1002 + | None -> None 1003 + | Some exp_time -> 1004 + let diff = Ptime.diff exp_time now in 1005 + if Ptime.Span.compare diff Ptime.Span.zero <= 0 then None 1006 + else Some diff
+472
ocaml-jsonwt/lib/jsonwt.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** JSON Web Token (JWT) - RFC 7519 7 + 8 + This module implements JSON Web Tokens as specified in 9 + {{:https://datatracker.ietf.org/doc/html/rfc7519}RFC 7519}. 10 + 11 + JWTs are compact, URL-safe means of representing claims to be transferred 12 + between two parties. The claims are encoded as a JSON object that is used 13 + as the payload of a JSON Web Signature (JWS) structure, enabling the claims 14 + to be digitally signed or integrity protected with a Message Authentication 15 + Code (MAC). 16 + 17 + {2 References} 18 + {ul 19 + {- {{:https://datatracker.ietf.org/doc/html/rfc7519}RFC 7519} - JSON Web Token (JWT)} 20 + {- {{:https://datatracker.ietf.org/doc/html/rfc7515}RFC 7515} - JSON Web Signature (JWS)} 21 + {- {{:https://datatracker.ietf.org/doc/html/rfc7517}RFC 7517} - JSON Web Key (JWK)} 22 + {- {{:https://datatracker.ietf.org/doc/html/rfc7518}RFC 7518} - JSON Web Algorithms (JWA)}} *) 23 + 24 + (** {1 Error Handling} *) 25 + 26 + type error = 27 + | Invalid_json of string 28 + (** JSON parsing failed *) 29 + | Invalid_base64url of string 30 + (** Base64url decoding failed *) 31 + | Invalid_structure of string 32 + (** Wrong number of parts or malformed structure *) 33 + | Invalid_header of string 34 + (** Header validation failed *) 35 + | Invalid_claims of string 36 + (** Claims validation failed *) 37 + | Invalid_uri of string 38 + (** StringOrURI validation failed per 39 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-2}RFC 7519 Section 2} *) 40 + | Duplicate_claim of string 41 + (** Duplicate claim name found in strict mode per 42 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4}RFC 7519 Section 4} *) 43 + | Unsupported_algorithm of string 44 + (** Unknown algorithm identifier *) 45 + | Algorithm_not_allowed of string 46 + (** Algorithm rejected by allowed_algs policy *) 47 + | Signature_mismatch 48 + (** Signature verification failed *) 49 + | Token_expired 50 + (** exp claim validation failed per 51 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4}RFC 7519 Section 4.1.4} *) 52 + | Token_not_yet_valid 53 + (** nbf claim validation failed per 54 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5}RFC 7519 Section 4.1.5} *) 55 + | Invalid_issuer 56 + (** iss claim mismatch per 57 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1}RFC 7519 Section 4.1.1} *) 58 + | Invalid_audience 59 + (** aud claim mismatch per 60 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3}RFC 7519 Section 4.1.3} *) 61 + | Key_type_mismatch of string 62 + (** Key doesn't match algorithm *) 63 + | Unsecured_not_allowed 64 + (** alg:none used without explicit opt-in per 65 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-6}RFC 7519 Section 6} *) 66 + | Nesting_too_deep 67 + (** Nested JWT exceeds max_depth *) 68 + 69 + val pp_error : Format.formatter -> error -> unit 70 + (** Pretty-print an error. *) 71 + 72 + val error_to_string : error -> string 73 + (** Convert error to human-readable string. *) 74 + 75 + (** {1 Algorithms} 76 + 77 + Signature and MAC algorithms for JWT. 78 + See {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3}RFC 7518 Section 3}. *) 79 + 80 + module Algorithm : sig 81 + type t = 82 + | None (** No digital signature or MAC per 83 + {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.6}RFC 7518 Section 3.6} *) 84 + | HS256 (** HMAC using SHA-256 per 85 + {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.2}RFC 7518 Section 3.2} *) 86 + | HS384 (** HMAC using SHA-384 *) 87 + | HS512 (** HMAC using SHA-512 *) 88 + | RS256 (** RSASSA-PKCS1-v1_5 using SHA-256 per 89 + {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.3}RFC 7518 Section 3.3} *) 90 + | RS384 (** RSASSA-PKCS1-v1_5 using SHA-384 *) 91 + | RS512 (** RSASSA-PKCS1-v1_5 using SHA-512 *) 92 + | ES256 (** ECDSA using P-256 and SHA-256 per 93 + {{:https://datatracker.ietf.org/doc/html/rfc7518#section-3.4}RFC 7518 Section 3.4} *) 94 + | ES384 (** ECDSA using P-384 and SHA-384 *) 95 + | ES512 (** ECDSA using P-521 and SHA-512 *) 96 + | EdDSA (** EdDSA using Ed25519 per 97 + {{:https://datatracker.ietf.org/doc/html/rfc8037}RFC 8037} *) 98 + 99 + val to_string : t -> string 100 + (** Convert algorithm to JWA identifier string. *) 101 + 102 + val of_string : string -> (t, error) result 103 + (** Parse algorithm from JWA identifier string. *) 104 + 105 + val all : t list 106 + (** All supported algorithms (excluding None). *) 107 + 108 + val all_with_none : t list 109 + (** All supported algorithms (including None). *) 110 + end 111 + 112 + (** {1 JSON Web Key} 113 + 114 + Key representation for JWT signature verification. 115 + See {{:https://datatracker.ietf.org/doc/html/rfc7517}RFC 7517}. *) 116 + 117 + module Jwk : sig 118 + 119 + (** Key type per {{:https://datatracker.ietf.org/doc/html/rfc7517#section-4.1}RFC 7517 Section 4.1}. *) 120 + type kty = 121 + | Oct (** Octet sequence (symmetric key) *) 122 + | Rsa (** RSA key *) 123 + | Ec (** Elliptic Curve key *) 124 + | Okp (** Octet Key Pair (Ed25519, X25519) *) 125 + 126 + (** Elliptic curve identifiers per {{:https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.1}RFC 7518 Section 6.2.1.1}. *) 127 + type crv = 128 + | P256 (** NIST P-256 curve *) 129 + | P384 (** NIST P-384 curve *) 130 + | P521 (** NIST P-521 curve *) 131 + | Ed25519 (** Ed25519 curve per {{:https://datatracker.ietf.org/doc/html/rfc8037}RFC 8037} *) 132 + 133 + (** A JSON Web Key. *) 134 + type t 135 + 136 + (** {2 Constructors} *) 137 + 138 + val symmetric : string -> t 139 + (** [symmetric k] creates a symmetric key from raw bytes. 140 + Used for HMAC algorithms (HS256, HS384, HS512). *) 141 + 142 + val ed25519_pub : string -> t 143 + (** [ed25519_pub pub] creates an Ed25519 public key from 32-byte public key. *) 144 + 145 + val ed25519_priv : pub:string -> priv:string -> t 146 + (** [ed25519_priv ~pub ~priv] creates an Ed25519 private key. *) 147 + 148 + val p256_pub : x:string -> y:string -> t 149 + (** [p256_pub ~x ~y] creates a P-256 public key from coordinates. *) 150 + 151 + val p256_priv : x:string -> y:string -> d:string -> t 152 + (** [p256_priv ~x ~y ~d] creates a P-256 private key. *) 153 + 154 + val p384_pub : x:string -> y:string -> t 155 + (** [p384_pub ~x ~y] creates a P-384 public key from coordinates. *) 156 + 157 + val p384_priv : x:string -> y:string -> d:string -> t 158 + (** [p384_priv ~x ~y ~d] creates a P-384 private key. *) 159 + 160 + val p521_pub : x:string -> y:string -> t 161 + (** [p521_pub ~x ~y] creates a P-521 public key from coordinates. *) 162 + 163 + val p521_priv : x:string -> y:string -> d:string -> t 164 + (** [p521_priv ~x ~y ~d] creates a P-521 private key. *) 165 + 166 + val rsa_pub : n:string -> e:string -> t 167 + (** [rsa_pub ~n ~e] creates an RSA public key from modulus and exponent. *) 168 + 169 + val rsa_priv : 170 + n:string -> e:string -> d:string -> p:string -> q:string -> 171 + dp:string -> dq:string -> qi:string -> t 172 + (** [rsa_priv ~n ~e ~d ~p ~q ~dp ~dq ~qi] creates an RSA private key. *) 173 + 174 + (** {2 Accessors} *) 175 + 176 + val kty : t -> kty 177 + (** Get the key type. *) 178 + 179 + val kid : t -> string option 180 + (** Get the key ID if set. *) 181 + 182 + val alg : t -> Algorithm.t option 183 + (** Get the intended algorithm if set. *) 184 + 185 + val with_kid : string -> t -> t 186 + (** [with_kid id key] returns key with kid set to [id]. *) 187 + 188 + val with_alg : Algorithm.t -> t -> t 189 + (** [with_alg alg key] returns key with alg set to [alg]. *) 190 + 191 + (** {2 Serialization} *) 192 + 193 + val of_json : string -> (t, error) result 194 + (** Parse a JWK from JSON string. *) 195 + 196 + val to_json : t -> string 197 + (** Serialize a JWK to JSON string. *) 198 + end 199 + 200 + (** {1 JOSE Header} 201 + 202 + The JOSE (JSON Object Signing and Encryption) Header. 203 + See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-5}RFC 7519 Section 5}. *) 204 + 205 + module Header : sig 206 + type t = { 207 + alg : Algorithm.t; (** Algorithm used (REQUIRED) *) 208 + typ : string option; (** Type - RECOMMENDED to be "JWT" per 209 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-5.1}RFC 7519 Section 5.1} *) 210 + kid : string option; (** Key ID for key lookup *) 211 + cty : string option; (** Content type - MUST be "JWT" for nested JWTs per 212 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-5.2}RFC 7519 Section 5.2} *) 213 + } 214 + 215 + val make : ?typ:string -> ?kid:string -> ?cty:string -> Algorithm.t -> t 216 + (** [make ?typ ?kid ?cty alg] creates a JOSE header. *) 217 + 218 + val of_json : string -> (t, error) result 219 + (** Parse header from JSON string. *) 220 + 221 + val to_json : t -> string 222 + (** Serialize header to JSON string. *) 223 + 224 + val is_nested : t -> bool 225 + (** [is_nested h] returns true if [cty] is "JWT" (case-insensitive), 226 + indicating a nested JWT. *) 227 + end 228 + 229 + (** {1 Claims} 230 + 231 + JWT Claims Set. 232 + See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4}RFC 7519 Section 4}. *) 233 + 234 + module Claims : sig 235 + type t 236 + 237 + (** {2 Registered Claim Names} 238 + 239 + See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1}RFC 7519 Section 4.1}. *) 240 + 241 + val iss : t -> string option 242 + (** Issuer claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1}Section 4.1.1}. *) 243 + 244 + val sub : t -> string option 245 + (** Subject claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2}Section 4.1.2}. *) 246 + 247 + val aud : t -> string list 248 + (** Audience claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3}Section 4.1.3}. 249 + Returns empty list if not present. May be single string or array in JWT. *) 250 + 251 + val exp : t -> Ptime.t option 252 + (** Expiration time claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4}Section 4.1.4}. *) 253 + 254 + val nbf : t -> Ptime.t option 255 + (** Not Before claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5}Section 4.1.5}. *) 256 + 257 + val iat : t -> Ptime.t option 258 + (** Issued At claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6}Section 4.1.6}. *) 259 + 260 + val jti : t -> string option 261 + (** JWT ID claim per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7}Section 4.1.7}. *) 262 + 263 + (** {2 Custom Claims} 264 + 265 + For Public and Private claims per 266 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.2}Sections 4.2} and 267 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4.3}4.3}. *) 268 + 269 + val get : string -> t -> Jsont.json option 270 + (** [get name claims] returns the value of custom claim [name]. *) 271 + 272 + val get_string : string -> t -> string option 273 + (** [get_string name claims] returns the string value of custom claim [name]. *) 274 + 275 + val get_int : string -> t -> int option 276 + (** [get_int name claims] returns the integer value of custom claim [name]. *) 277 + 278 + val get_bool : string -> t -> bool option 279 + (** [get_bool name claims] returns the boolean value of custom claim [name]. *) 280 + 281 + (** {2 Construction} *) 282 + 283 + type builder 284 + (** Builder for constructing claims. *) 285 + 286 + val empty : builder 287 + (** Empty claims builder. *) 288 + 289 + val set_iss : string -> builder -> builder 290 + (** Set issuer claim. Value is validated as StringOrURI. *) 291 + 292 + val set_sub : string -> builder -> builder 293 + (** Set subject claim. Value is validated as StringOrURI. *) 294 + 295 + val set_aud : string list -> builder -> builder 296 + (** Set audience claim. *) 297 + 298 + val set_exp : Ptime.t -> builder -> builder 299 + (** Set expiration time claim. *) 300 + 301 + val set_nbf : Ptime.t -> builder -> builder 302 + (** Set not-before claim. *) 303 + 304 + val set_iat : Ptime.t -> builder -> builder 305 + (** Set issued-at claim. *) 306 + 307 + val set_jti : string -> builder -> builder 308 + (** Set JWT ID claim. *) 309 + 310 + val set : string -> Jsont.json -> builder -> builder 311 + (** [set name value builder] sets a custom claim. *) 312 + 313 + val set_string : string -> string -> builder -> builder 314 + (** Set a custom string claim. *) 315 + 316 + val set_int : string -> int -> builder -> builder 317 + (** Set a custom integer claim. *) 318 + 319 + val set_bool : string -> bool -> builder -> builder 320 + (** Set a custom boolean claim. *) 321 + 322 + val build : builder -> t 323 + (** Build the claims set. *) 324 + 325 + (** {2 Serialization} *) 326 + 327 + val of_json : ?strict:bool -> string -> (t, error) result 328 + (** [of_json ?strict json] parses claims from JSON string. 329 + @param strict If true (default), reject duplicate claim names per 330 + {{:https://datatracker.ietf.org/doc/html/rfc7519#section-4}RFC 7519 Section 4}. 331 + If false, use lexically last duplicate. *) 332 + 333 + val to_json : t -> string 334 + (** Serialize claims to JSON string. *) 335 + end 336 + 337 + (** {1 JWT Token} *) 338 + 339 + type t = { 340 + header : Header.t; (** JOSE header *) 341 + claims : Claims.t; (** Claims set *) 342 + signature : string; (** Raw signature bytes *) 343 + raw : string; (** Original compact serialization *) 344 + } 345 + (** A parsed JWT token. *) 346 + 347 + (** {2 Parsing} 348 + 349 + See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.2}RFC 7519 Section 7.2}. *) 350 + 351 + val parse : ?strict:bool -> string -> (t, error) result 352 + (** [parse ?strict token_string] parses a JWT from its compact serialization. 353 + 354 + This parses the token structure but does NOT verify the signature. 355 + Use {!verify} to validate the signature after parsing. 356 + 357 + @param strict If true (default), reject duplicate claim names. *) 358 + 359 + val parse_unsafe : string -> (t, error) result 360 + (** [parse_unsafe token_string] parses a JWT without strict validation. 361 + Equivalent to [parse ~strict:false]. *) 362 + 363 + (** {2 Nested JWTs} 364 + 365 + See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.2}RFC 7519 Section 7.2 step 8} 366 + and {{:https://datatracker.ietf.org/doc/html/rfc7519#appendix-A.2}Appendix A.2}. *) 367 + 368 + val parse_nested : 369 + ?strict:bool -> 370 + ?max_depth:int -> 371 + string -> 372 + (t list, error) result 373 + (** [parse_nested ?strict ?max_depth token] parses a potentially nested JWT. 374 + Returns a list of JWTs from outermost to innermost. 375 + @param max_depth Maximum nesting depth (default 5). *) 376 + 377 + val is_nested : t -> bool 378 + (** [is_nested t] returns true if the JWT has [cty: "JWT"] header, 379 + indicating it contains a nested JWT. *) 380 + 381 + (** {2 Accessors} *) 382 + 383 + val header : t -> Header.t 384 + (** [header t] returns the JOSE header. *) 385 + 386 + val claims : t -> Claims.t 387 + (** [claims t] returns the claims set. *) 388 + 389 + val signature : t -> string 390 + (** [signature t] returns the raw signature bytes. *) 391 + 392 + val raw : t -> string 393 + (** [raw t] returns the original token string. *) 394 + 395 + (** {2 Verification} 396 + 397 + See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.2}RFC 7519 Section 7.2}. *) 398 + 399 + val verify : 400 + key:Jwk.t -> 401 + ?allow_none:bool -> 402 + ?allowed_algs:Algorithm.t list -> 403 + t -> 404 + (unit, error) result 405 + (** [verify ~key ?allow_none ?allowed_algs t] verifies the JWT signature. 406 + 407 + @param key The key to verify with (must match algorithm) 408 + @param allow_none If true, accept [alg:"none"]. Default: false. 409 + Per {{:https://datatracker.ietf.org/doc/html/rfc7519#section-6}RFC 7519 Section 6}, 410 + unsecured JWTs should only be used when security is provided by other means. 411 + @param allowed_algs List of acceptable algorithms. Default: all except none. 412 + Note: "none" is only allowed if BOTH in this list AND [allow_none=true]. *) 413 + 414 + val validate : 415 + now:Ptime.t -> 416 + ?iss:string -> 417 + ?aud:string -> 418 + ?leeway:Ptime.Span.t -> 419 + t -> 420 + (unit, error) result 421 + (** [validate ~now ?iss ?aud ?leeway t] validates JWT claims. 422 + 423 + @param now Current time (required, no implicit clock) 424 + @param iss Expected issuer (if provided, must match exactly) 425 + @param aud Expected audience (if provided, must be in audience list) 426 + @param leeway Clock skew tolerance for exp/nbf checks (default 0s) *) 427 + 428 + val verify_and_validate : 429 + key:Jwk.t -> 430 + now:Ptime.t -> 431 + ?allow_none:bool -> 432 + ?allowed_algs:Algorithm.t list -> 433 + ?iss:string -> 434 + ?aud:string -> 435 + ?leeway:Ptime.Span.t -> 436 + t -> 437 + (unit, error) result 438 + (** [verify_and_validate ~key ~now ...] verifies signature and validates claims. *) 439 + 440 + (** {2 Creation} 441 + 442 + See {{:https://datatracker.ietf.org/doc/html/rfc7519#section-7.1}RFC 7519 Section 7.1}. *) 443 + 444 + val create : header:Header.t -> claims:Claims.t -> key:Jwk.t -> (t, error) result 445 + (** [create ~header ~claims ~key] creates and signs a new JWT. 446 + 447 + The [key] must be appropriate for the algorithm specified in [header]. 448 + For [alg:none], pass any key (it will be ignored). *) 449 + 450 + val encode : t -> string 451 + (** [encode t] returns the compact serialization of the JWT. *) 452 + 453 + (** {1 Utilities} *) 454 + 455 + val is_expired : now:Ptime.t -> ?leeway:Ptime.Span.t -> t -> bool 456 + (** [is_expired ~now ?leeway t] checks if the token has expired. 457 + Returns false if no exp claim present. *) 458 + 459 + val time_to_expiry : now:Ptime.t -> t -> Ptime.Span.t option 460 + (** [time_to_expiry ~now t] returns time until expiration, or [None] if 461 + no expiration claim or already expired. *) 462 + 463 + (** {1 Base64url Utilities} 464 + 465 + Exposed for testing with RFC test vectors. *) 466 + 467 + val base64url_encode : string -> string 468 + (** Base64url encode without padding per 469 + {{:https://datatracker.ietf.org/doc/html/rfc7515#appendix-C}RFC 7515 Appendix C}. *) 470 + 471 + val base64url_decode : string -> (string, error) result 472 + (** Base64url decode, handling missing padding. *)
+1683
ocaml-jsonwt/spec/rfc7519.txt
··· 1 + 2 + 3 + 4 + 5 + 6 + 7 + Internet Engineering Task Force (IETF) M. Jones 8 + Request for Comments: 7519 Microsoft 9 + Category: Standards Track J. Bradley 10 + ISSN: 2070-1721 Ping Identity 11 + N. Sakimura 12 + NRI 13 + May 2015 14 + 15 + 16 + JSON Web Token (JWT) 17 + 18 + Abstract 19 + 20 + JSON Web Token (JWT) is a compact, URL-safe means of representing 21 + claims to be transferred between two parties. The claims in a JWT 22 + are encoded as a JSON object that is used as the payload of a JSON 23 + Web Signature (JWS) structure or as the plaintext of a JSON Web 24 + Encryption (JWE) structure, enabling the claims to be digitally 25 + signed or integrity protected with a Message Authentication Code 26 + (MAC) and/or encrypted. 27 + 28 + Status of This Memo 29 + 30 + This is an Internet Standards Track document. 31 + 32 + This document is a product of the Internet Engineering Task Force 33 + (IETF). It represents the consensus of the IETF community. It has 34 + received public review and has been approved for publication by the 35 + Internet Engineering Steering Group (IESG). Further information on 36 + Internet Standards is available in Section 2 of RFC 5741. 37 + 38 + Information about the current status of this document, any errata, 39 + and how to provide feedback on it may be obtained at 40 + http://www.rfc-editor.org/info/rfc7519. 41 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 50 + 51 + 52 + 53 + 54 + 55 + 56 + 57 + 58 + Jones, et al. Standards Track [Page 1] 59 + 60 + RFC 7519 JSON Web Token (JWT) May 2015 61 + 62 + 63 + Copyright Notice 64 + 65 + Copyright (c) 2015 IETF Trust and the persons identified as the 66 + document authors. All rights reserved. 67 + 68 + This document is subject to BCP 78 and the IETF Trust's Legal 69 + Provisions Relating to IETF Documents 70 + (http://trustee.ietf.org/license-info) in effect on the date of 71 + publication of this document. Please review these documents 72 + carefully, as they describe your rights and restrictions with respect 73 + to this document. Code Components extracted from this document must 74 + include Simplified BSD License text as described in Section 4.e of 75 + the Trust Legal Provisions and are provided without warranty as 76 + described in the Simplified BSD License. 77 + 78 + 79 + 80 + 81 + 82 + 83 + 84 + 85 + 86 + 87 + 88 + 89 + 90 + 91 + 92 + 93 + 94 + 95 + 96 + 97 + 98 + 99 + 100 + 101 + 102 + 103 + 104 + 105 + 106 + 107 + 108 + 109 + 110 + 111 + 112 + 113 + 114 + Jones, et al. Standards Track [Page 2] 115 + 116 + RFC 7519 JSON Web Token (JWT) May 2015 117 + 118 + 119 + Table of Contents 120 + 121 + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4 122 + 1.1. Notational Conventions . . . . . . . . . . . . . . . . . 4 123 + 2. Terminology . . . . . . . . . . . . . . . . . . . . . . . . . 4 124 + 3. JSON Web Token (JWT) Overview . . . . . . . . . . . . . . . . 6 125 + 3.1. Example JWT . . . . . . . . . . . . . . . . . . . . . . . 7 126 + 4. JWT Claims . . . . . . . . . . . . . . . . . . . . . . . . . 8 127 + 4.1. Registered Claim Names . . . . . . . . . . . . . . . . . 9 128 + 4.1.1. "iss" (Issuer) Claim . . . . . . . . . . . . . . . . 9 129 + 4.1.2. "sub" (Subject) Claim . . . . . . . . . . . . . . . . 9 130 + 4.1.3. "aud" (Audience) Claim . . . . . . . . . . . . . . . 9 131 + 4.1.4. "exp" (Expiration Time) Claim . . . . . . . . . . . . 9 132 + 4.1.5. "nbf" (Not Before) Claim . . . . . . . . . . . . . . 10 133 + 4.1.6. "iat" (Issued At) Claim . . . . . . . . . . . . . . . 10 134 + 4.1.7. "jti" (JWT ID) Claim . . . . . . . . . . . . . . . . 10 135 + 4.2. Public Claim Names . . . . . . . . . . . . . . . . . . . 10 136 + 4.3. Private Claim Names . . . . . . . . . . . . . . . . . . . 10 137 + 5. JOSE Header . . . . . . . . . . . . . . . . . . . . . . . . . 11 138 + 5.1. "typ" (Type) Header Parameter . . . . . . . . . . . . . . 11 139 + 5.2. "cty" (Content Type) Header Parameter . . . . . . . . . . 11 140 + 5.3. Replicating Claims as Header Parameters . . . . . . . . . 12 141 + 6. Unsecured JWTs . . . . . . . . . . . . . . . . . . . . . . . 12 142 + 6.1. Example Unsecured JWT . . . . . . . . . . . . . . . . . . 12 143 + 7. Creating and Validating JWTs . . . . . . . . . . . . . . . . 13 144 + 7.1. Creating a JWT . . . . . . . . . . . . . . . . . . . . . 13 145 + 7.2. Validating a JWT . . . . . . . . . . . . . . . . . . . . 14 146 + 7.3. String Comparison Rules . . . . . . . . . . . . . . . . . 15 147 + 8. Implementation Requirements . . . . . . . . . . . . . . . . . 16 148 + 9. URI for Declaring that Content is a JWT . . . . . . . . . . . 17 149 + 10. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 17 150 + 10.1. JSON Web Token Claims Registry . . . . . . . . . . . . . 17 151 + 10.1.1. Registration Template . . . . . . . . . . . . . . . 18 152 + 10.1.2. Initial Registry Contents . . . . . . . . . . . . . 18 153 + 10.2. Sub-Namespace Registration of 154 + urn:ietf:params:oauth:token-type:jwt . . . . . . . . . . 19 155 + 10.2.1. Registry Contents . . . . . . . . . . . . . . . . . 19 156 + 10.3. Media Type Registration . . . . . . . . . . . . . . . . 20 157 + 10.3.1. Registry Contents . . . . . . . . . . . . . . . . . 20 158 + 10.4. Header Parameter Names Registration . . . . . . . . . . 20 159 + 10.4.1. Registry Contents . . . . . . . . . . . . . . . . . 21 160 + 11. Security Considerations . . . . . . . . . . . . . . . . . . . 21 161 + 11.1. Trust Decisions . . . . . . . . . . . . . . . . . . . . 21 162 + 11.2. Signing and Encryption Order . . . . . . . . . . . . . . 21 163 + 12. Privacy Considerations . . . . . . . . . . . . . . . . . . . 22 164 + 13. References . . . . . . . . . . . . . . . . . . . . . . . . . 22 165 + 13.1. Normative References . . . . . . . . . . . . . . . . . . 22 166 + 13.2. Informative References . . . . . . . . . . . . . . . . . 23 167 + 168 + 169 + 170 + Jones, et al. Standards Track [Page 3] 171 + 172 + RFC 7519 JSON Web Token (JWT) May 2015 173 + 174 + 175 + Appendix A. JWT Examples . . . . . . . . . . . . . . . . . . . . 26 176 + A.1. Example Encrypted JWT . . . . . . . . . . . . . . . . . . 26 177 + A.2. Example Nested JWT . . . . . . . . . . . . . . . . . . . 26 178 + Appendix B. Relationship of JWTs to SAML Assertions . . . . . . 28 179 + Appendix C. Relationship of JWTs to Simple Web Tokens (SWTs) . . 28 180 + Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . 28 181 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 29 182 + 183 + 1. Introduction 184 + 185 + JSON Web Token (JWT) is a compact claims representation format 186 + intended for space constrained environments such as HTTP 187 + Authorization headers and URI query parameters. JWTs encode claims 188 + to be transmitted as a JSON [RFC7159] object that is used as the 189 + payload of a JSON Web Signature (JWS) [JWS] structure or as the 190 + plaintext of a JSON Web Encryption (JWE) [JWE] structure, enabling 191 + the claims to be digitally signed or integrity protected with a 192 + Message Authentication Code (MAC) and/or encrypted. JWTs are always 193 + represented using the JWS Compact Serialization or the JWE Compact 194 + Serialization. 195 + 196 + The suggested pronunciation of JWT is the same as the English word 197 + "jot". 198 + 199 + 1.1. Notational Conventions 200 + 201 + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 202 + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 203 + "OPTIONAL" in this document are to be interpreted as described in 204 + "Key words for use in RFCs to Indicate Requirement Levels" [RFC2119]. 205 + The interpretation should only be applied when the terms appear in 206 + all capital letters. 207 + 208 + 2. Terminology 209 + 210 + The terms "JSON Web Signature (JWS)", "Base64url Encoding", "Header 211 + Parameter", "JOSE Header", "JWS Compact Serialization", "JWS 212 + Payload", "JWS Signature", and "Unsecured JWS" are defined by the JWS 213 + specification [JWS]. 214 + 215 + The terms "JSON Web Encryption (JWE)", "Content Encryption Key 216 + (CEK)", "JWE Compact Serialization", "JWE Encrypted Key", and "JWE 217 + Initialization Vector" are defined by the JWE specification [JWE]. 218 + 219 + The terms "Ciphertext", "Digital Signature", "Message Authentication 220 + Code (MAC)", and "Plaintext" are defined by the "Internet Security 221 + Glossary, Version 2" [RFC4949]. 222 + 223 + 224 + 225 + 226 + Jones, et al. Standards Track [Page 4] 227 + 228 + RFC 7519 JSON Web Token (JWT) May 2015 229 + 230 + 231 + These terms are defined by this specification: 232 + 233 + JSON Web Token (JWT) 234 + A string representing a set of claims as a JSON object that is 235 + encoded in a JWS or JWE, enabling the claims to be digitally 236 + signed or MACed and/or encrypted. 237 + 238 + JWT Claims Set 239 + A JSON object that contains the claims conveyed by the JWT. 240 + 241 + Claim 242 + A piece of information asserted about a subject. A claim is 243 + represented as a name/value pair consisting of a Claim Name and a 244 + Claim Value. 245 + 246 + Claim Name 247 + The name portion of a claim representation. A Claim Name is 248 + always a string. 249 + 250 + Claim Value 251 + The value portion of a claim representation. A Claim Value can be 252 + any JSON value. 253 + 254 + Nested JWT 255 + A JWT in which nested signing and/or encryption are employed. In 256 + Nested JWTs, a JWT is used as the payload or plaintext value of an 257 + enclosing JWS or JWE structure, respectively. 258 + 259 + Unsecured JWT 260 + A JWT whose claims are not integrity protected or encrypted. 261 + 262 + Collision-Resistant Name 263 + A name in a namespace that enables names to be allocated in a 264 + manner such that they are highly unlikely to collide with other 265 + names. Examples of collision-resistant namespaces include: Domain 266 + Names, Object Identifiers (OIDs) as defined in the ITU-T X.660 and 267 + X.670 Recommendation series, and Universally Unique IDentifiers 268 + (UUIDs) [RFC4122]. When using an administratively delegated 269 + namespace, the definer of a name needs to take reasonable 270 + precautions to ensure they are in control of the portion of the 271 + namespace they use to define the name. 272 + 273 + StringOrURI 274 + A JSON string value, with the additional requirement that while 275 + arbitrary string values MAY be used, any value containing a ":" 276 + character MUST be a URI [RFC3986]. StringOrURI values are 277 + compared as case-sensitive strings with no transformations or 278 + canonicalizations applied. 279 + 280 + 281 + 282 + Jones, et al. Standards Track [Page 5] 283 + 284 + RFC 7519 JSON Web Token (JWT) May 2015 285 + 286 + 287 + NumericDate 288 + A JSON numeric value representing the number of seconds from 289 + 1970-01-01T00:00:00Z UTC until the specified UTC date/time, 290 + ignoring leap seconds. This is equivalent to the IEEE Std 1003.1, 291 + 2013 Edition [POSIX.1] definition "Seconds Since the Epoch", in 292 + which each day is accounted for by exactly 86400 seconds, other 293 + than that non-integer values can be represented. See RFC 3339 294 + [RFC3339] for details regarding date/times in general and UTC in 295 + particular. 296 + 297 + 3. JSON Web Token (JWT) Overview 298 + 299 + JWTs represent a set of claims as a JSON object that is encoded in a 300 + JWS and/or JWE structure. This JSON object is the JWT Claims Set. 301 + As per Section 4 of RFC 7159 [RFC7159], the JSON object consists of 302 + zero or more name/value pairs (or members), where the names are 303 + strings and the values are arbitrary JSON values. These members are 304 + the claims represented by the JWT. This JSON object MAY contain 305 + whitespace and/or line breaks before or after any JSON values or 306 + structural characters, in accordance with Section 2 of RFC 7159 307 + [RFC7159]. 308 + 309 + The member names within the JWT Claims Set are referred to as Claim 310 + Names. The corresponding values are referred to as Claim Values. 311 + 312 + The contents of the JOSE Header describe the cryptographic operations 313 + applied to the JWT Claims Set. If the JOSE Header is for a JWS, the 314 + JWT is represented as a JWS and the claims are digitally signed or 315 + MACed, with the JWT Claims Set being the JWS Payload. If the JOSE 316 + Header is for a JWE, the JWT is represented as a JWE and the claims 317 + are encrypted, with the JWT Claims Set being the plaintext encrypted 318 + by the JWE. A JWT may be enclosed in another JWE or JWS structure to 319 + create a Nested JWT, enabling nested signing and encryption to be 320 + performed. 321 + 322 + A JWT is represented as a sequence of URL-safe parts separated by 323 + period ('.') characters. Each part contains a base64url-encoded 324 + value. The number of parts in the JWT is dependent upon the 325 + representation of the resulting JWS using the JWS Compact 326 + Serialization or JWE using the JWE Compact Serialization. 327 + 328 + 329 + 330 + 331 + 332 + 333 + 334 + 335 + 336 + 337 + 338 + Jones, et al. Standards Track [Page 6] 339 + 340 + RFC 7519 JSON Web Token (JWT) May 2015 341 + 342 + 343 + 3.1. Example JWT 344 + 345 + The following example JOSE Header declares that the encoded object is 346 + a JWT, and the JWT is a JWS that is MACed using the HMAC SHA-256 347 + algorithm: 348 + 349 + {"typ":"JWT", 350 + "alg":"HS256"} 351 + 352 + To remove potential ambiguities in the representation of the JSON 353 + object above, the octet sequence for the actual UTF-8 representation 354 + used in this example for the JOSE Header above is also included 355 + below. (Note that ambiguities can arise due to differing platform 356 + representations of line breaks (CRLF versus LF), differing spacing at 357 + the beginning and ends of lines, whether the last line has a 358 + terminating line break or not, and other causes. In the 359 + representation used in this example, the first line has no leading or 360 + trailing spaces, a CRLF line break (13, 10) occurs between the first 361 + and second lines, the second line has one leading space (32) and no 362 + trailing spaces, and the last line does not have a terminating line 363 + break.) The octets representing the UTF-8 representation of the JOSE 364 + Header in this example (using JSON array notation) are: 365 + 366 + [123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32, 367 + 34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125] 368 + 369 + Base64url encoding the octets of the UTF-8 representation of the JOSE 370 + Header yields this encoded JOSE Header value: 371 + 372 + eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 373 + 374 + The following is an example of a JWT Claims Set: 375 + 376 + {"iss":"joe", 377 + "exp":1300819380, 378 + "http://example.com/is_root":true} 379 + 380 + The following octet sequence, which is the UTF-8 representation used 381 + in this example for the JWT Claims Set above, is the JWS Payload: 382 + 383 + [123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 384 + 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 385 + 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 386 + 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 387 + 111, 116, 34, 58, 116, 114, 117, 101, 125] 388 + 389 + 390 + 391 + 392 + 393 + 394 + Jones, et al. Standards Track [Page 7] 395 + 396 + RFC 7519 JSON Web Token (JWT) May 2015 397 + 398 + 399 + Base64url encoding the JWS Payload yields this encoded JWS Payload 400 + (with line breaks for display purposes only): 401 + 402 + eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly 403 + 9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ 404 + 405 + Computing the MAC of the encoded JOSE Header and encoded JWS Payload 406 + with the HMAC SHA-256 algorithm and base64url encoding the HMAC value 407 + in the manner specified in [JWS] yields this encoded JWS Signature: 408 + 409 + dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk 410 + 411 + Concatenating these encoded parts in this order with period ('.') 412 + characters between the parts yields this complete JWT (with line 413 + breaks for display purposes only): 414 + 415 + eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9 416 + . 417 + eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt 418 + cGxlLmNvbS9pc19yb290Ijp0cnVlfQ 419 + . 420 + dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk 421 + 422 + This computation is illustrated in more detail in Appendix A.1 of 423 + [JWS]. See Appendix A.1 for an example of an encrypted JWT. 424 + 425 + 4. JWT Claims 426 + 427 + The JWT Claims Set represents a JSON object whose members are the 428 + claims conveyed by the JWT. The Claim Names within a JWT Claims Set 429 + MUST be unique; JWT parsers MUST either reject JWTs with duplicate 430 + Claim Names or use a JSON parser that returns only the lexically last 431 + duplicate member name, as specified in Section 15.12 ("The JSON 432 + Object") of ECMAScript 5.1 [ECMAScript]. 433 + 434 + The set of claims that a JWT must contain to be considered valid is 435 + context dependent and is outside the scope of this specification. 436 + Specific applications of JWTs will require implementations to 437 + understand and process some claims in particular ways. However, in 438 + the absence of such requirements, all claims that are not understood 439 + by implementations MUST be ignored. 440 + 441 + There are three classes of JWT Claim Names: Registered Claim Names, 442 + Public Claim Names, and Private Claim Names. 443 + 444 + 445 + 446 + 447 + 448 + 449 + 450 + Jones, et al. Standards Track [Page 8] 451 + 452 + RFC 7519 JSON Web Token (JWT) May 2015 453 + 454 + 455 + 4.1. Registered Claim Names 456 + 457 + The following Claim Names are registered in the IANA "JSON Web Token 458 + Claims" registry established by Section 10.1. None of the claims 459 + defined below are intended to be mandatory to use or implement in all 460 + cases, but rather they provide a starting point for a set of useful, 461 + interoperable claims. Applications using JWTs should define which 462 + specific claims they use and when they are required or optional. All 463 + the names are short because a core goal of JWTs is for the 464 + representation to be compact. 465 + 466 + 4.1.1. "iss" (Issuer) Claim 467 + 468 + The "iss" (issuer) claim identifies the principal that issued the 469 + JWT. The processing of this claim is generally application specific. 470 + The "iss" value is a case-sensitive string containing a StringOrURI 471 + value. Use of this claim is OPTIONAL. 472 + 473 + 4.1.2. "sub" (Subject) Claim 474 + 475 + The "sub" (subject) claim identifies the principal that is the 476 + subject of the JWT. The claims in a JWT are normally statements 477 + about the subject. The subject value MUST either be scoped to be 478 + locally unique in the context of the issuer or be globally unique. 479 + The processing of this claim is generally application specific. The 480 + "sub" value is a case-sensitive string containing a StringOrURI 481 + value. Use of this claim is OPTIONAL. 482 + 483 + 4.1.3. "aud" (Audience) Claim 484 + 485 + The "aud" (audience) claim identifies the recipients that the JWT is 486 + intended for. Each principal intended to process the JWT MUST 487 + identify itself with a value in the audience claim. If the principal 488 + processing the claim does not identify itself with a value in the 489 + "aud" claim when this claim is present, then the JWT MUST be 490 + rejected. In the general case, the "aud" value is an array of case- 491 + sensitive strings, each containing a StringOrURI value. In the 492 + special case when the JWT has one audience, the "aud" value MAY be a 493 + single case-sensitive string containing a StringOrURI value. The 494 + interpretation of audience values is generally application specific. 495 + Use of this claim is OPTIONAL. 496 + 497 + 4.1.4. "exp" (Expiration Time) Claim 498 + 499 + The "exp" (expiration time) claim identifies the expiration time on 500 + or after which the JWT MUST NOT be accepted for processing. The 501 + processing of the "exp" claim requires that the current date/time 502 + MUST be before the expiration date/time listed in the "exp" claim. 503 + 504 + 505 + 506 + Jones, et al. Standards Track [Page 9] 507 + 508 + RFC 7519 JSON Web Token (JWT) May 2015 509 + 510 + 511 + Implementers MAY provide for some small leeway, usually no more than 512 + a few minutes, to account for clock skew. Its value MUST be a number 513 + containing a NumericDate value. Use of this claim is OPTIONAL. 514 + 515 + 4.1.5. "nbf" (Not Before) Claim 516 + 517 + The "nbf" (not before) claim identifies the time before which the JWT 518 + MUST NOT be accepted for processing. The processing of the "nbf" 519 + claim requires that the current date/time MUST be after or equal to 520 + the not-before date/time listed in the "nbf" claim. Implementers MAY 521 + provide for some small leeway, usually no more than a few minutes, to 522 + account for clock skew. Its value MUST be a number containing a 523 + NumericDate value. Use of this claim is OPTIONAL. 524 + 525 + 4.1.6. "iat" (Issued At) Claim 526 + 527 + The "iat" (issued at) claim identifies the time at which the JWT was 528 + issued. This claim can be used to determine the age of the JWT. Its 529 + value MUST be a number containing a NumericDate value. Use of this 530 + claim is OPTIONAL. 531 + 532 + 4.1.7. "jti" (JWT ID) Claim 533 + 534 + The "jti" (JWT ID) claim provides a unique identifier for the JWT. 535 + The identifier value MUST be assigned in a manner that ensures that 536 + there is a negligible probability that the same value will be 537 + accidentally assigned to a different data object; if the application 538 + uses multiple issuers, collisions MUST be prevented among values 539 + produced by different issuers as well. The "jti" claim can be used 540 + to prevent the JWT from being replayed. The "jti" value is a case- 541 + sensitive string. Use of this claim is OPTIONAL. 542 + 543 + 4.2. Public Claim Names 544 + 545 + Claim Names can be defined at will by those using JWTs. However, in 546 + order to prevent collisions, any new Claim Name should either be 547 + registered in the IANA "JSON Web Token Claims" registry established 548 + by Section 10.1 or be a Public Name: a value that contains a 549 + Collision-Resistant Name. In each case, the definer of the name or 550 + value needs to take reasonable precautions to make sure they are in 551 + control of the part of the namespace they use to define the Claim 552 + Name. 553 + 554 + 4.3. Private Claim Names 555 + 556 + A producer and consumer of a JWT MAY agree to use Claim Names that 557 + are Private Names: names that are not Registered Claim Names 558 + (Section 4.1) or Public Claim Names (Section 4.2). Unlike Public 559 + 560 + 561 + 562 + Jones, et al. Standards Track [Page 10] 563 + 564 + RFC 7519 JSON Web Token (JWT) May 2015 565 + 566 + 567 + Claim Names, Private Claim Names are subject to collision and should 568 + be used with caution. 569 + 570 + 5. JOSE Header 571 + 572 + For a JWT object, the members of the JSON object represented by the 573 + JOSE Header describe the cryptographic operations applied to the JWT 574 + and optionally, additional properties of the JWT. Depending upon 575 + whether the JWT is a JWS or JWE, the corresponding rules for the JOSE 576 + Header values apply. 577 + 578 + This specification further specifies the use of the following Header 579 + Parameters in both the cases where the JWT is a JWS and where it is a 580 + JWE. 581 + 582 + 5.1. "typ" (Type) Header Parameter 583 + 584 + The "typ" (type) Header Parameter defined by [JWS] and [JWE] is used 585 + by JWT applications to declare the media type [IANA.MediaTypes] of 586 + this complete JWT. This is intended for use by the JWT application 587 + when values that are not JWTs could also be present in an application 588 + data structure that can contain a JWT object; the application can use 589 + this value to disambiguate among the different kinds of objects that 590 + might be present. It will typically not be used by applications when 591 + it is already known that the object is a JWT. This parameter is 592 + ignored by JWT implementations; any processing of this parameter is 593 + performed by the JWT application. If present, it is RECOMMENDED that 594 + its value be "JWT" to indicate that this object is a JWT. While 595 + media type names are not case sensitive, it is RECOMMENDED that "JWT" 596 + always be spelled using uppercase characters for compatibility with 597 + legacy implementations. Use of this Header Parameter is OPTIONAL. 598 + 599 + 5.2. "cty" (Content Type) Header Parameter 600 + 601 + The "cty" (content type) Header Parameter defined by [JWS] and [JWE] 602 + is used by this specification to convey structural information about 603 + the JWT. 604 + 605 + In the normal case in which nested signing or encryption operations 606 + are not employed, the use of this Header Parameter is NOT 607 + RECOMMENDED. In the case that nested signing or encryption is 608 + employed, this Header Parameter MUST be present; in this case, the 609 + value MUST be "JWT", to indicate that a Nested JWT is carried in this 610 + JWT. While media type names are not case sensitive, it is 611 + RECOMMENDED that "JWT" always be spelled using uppercase characters 612 + for compatibility with legacy implementations. See Appendix A.2 for 613 + an example of a Nested JWT. 614 + 615 + 616 + 617 + 618 + Jones, et al. Standards Track [Page 11] 619 + 620 + RFC 7519 JSON Web Token (JWT) May 2015 621 + 622 + 623 + 5.3. Replicating Claims as Header Parameters 624 + 625 + In some applications using encrypted JWTs, it is useful to have an 626 + unencrypted representation of some claims. This might be used, for 627 + instance, in application processing rules to determine whether and 628 + how to process the JWT before it is decrypted. 629 + 630 + This specification allows claims present in the JWT Claims Set to be 631 + replicated as Header Parameters in a JWT that is a JWE, as needed by 632 + the application. If such replicated claims are present, the 633 + application receiving them SHOULD verify that their values are 634 + identical, unless the application defines other specific processing 635 + rules for these claims. It is the responsibility of the application 636 + to ensure that only claims that are safe to be transmitted in an 637 + unencrypted manner are replicated as Header Parameter values in the 638 + JWT. 639 + 640 + Section 10.4.1 of this specification registers the "iss" (issuer), 641 + "sub" (subject), and "aud" (audience) Header Parameter names for the 642 + purpose of providing unencrypted replicas of these claims in 643 + encrypted JWTs for applications that need them. Other specifications 644 + MAY similarly register other names that are registered Claim Names as 645 + Header Parameter names, as needed. 646 + 647 + 6. Unsecured JWTs 648 + 649 + To support use cases in which the JWT content is secured by a means 650 + other than a signature and/or encryption contained within the JWT 651 + (such as a signature on a data structure containing the JWT), JWTs 652 + MAY also be created without a signature or encryption. An Unsecured 653 + JWT is a JWS using the "alg" Header Parameter value "none" and with 654 + the empty string for its JWS Signature value, as defined in the JWA 655 + specification [JWA]; it is an Unsecured JWS with the JWT Claims Set 656 + as its JWS Payload. 657 + 658 + 6.1. Example Unsecured JWT 659 + 660 + The following example JOSE Header declares that the encoded object is 661 + an Unsecured JWT: 662 + 663 + {"alg":"none"} 664 + 665 + Base64url encoding the octets of the UTF-8 representation of the JOSE 666 + Header yields this encoded JOSE Header value: 667 + 668 + eyJhbGciOiJub25lIn0 669 + 670 + 671 + 672 + 673 + 674 + Jones, et al. Standards Track [Page 12] 675 + 676 + RFC 7519 JSON Web Token (JWT) May 2015 677 + 678 + 679 + The following is an example of a JWT Claims Set: 680 + 681 + {"iss":"joe", 682 + "exp":1300819380, 683 + "http://example.com/is_root":true} 684 + 685 + Base64url encoding the octets of the UTF-8 representation of the JWT 686 + Claims Set yields this encoded JWS Payload (with line breaks for 687 + display purposes only): 688 + 689 + eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt 690 + cGxlLmNvbS9pc19yb290Ijp0cnVlfQ 691 + 692 + The encoded JWS Signature is the empty string. 693 + 694 + Concatenating these encoded parts in this order with period ('.') 695 + characters between the parts yields this complete JWT (with line 696 + breaks for display purposes only): 697 + 698 + eyJhbGciOiJub25lIn0 699 + . 700 + eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt 701 + cGxlLmNvbS9pc19yb290Ijp0cnVlfQ 702 + . 703 + 704 + 7. Creating and Validating JWTs 705 + 706 + 7.1. Creating a JWT 707 + 708 + To create a JWT, the following steps are performed. The order of the 709 + steps is not significant in cases where there are no dependencies 710 + between the inputs and outputs of the steps. 711 + 712 + 1. Create a JWT Claims Set containing the desired claims. Note that 713 + whitespace is explicitly allowed in the representation and no 714 + canonicalization need be performed before encoding. 715 + 716 + 2. Let the Message be the octets of the UTF-8 representation of the 717 + JWT Claims Set. 718 + 719 + 3. Create a JOSE Header containing the desired set of Header 720 + Parameters. The JWT MUST conform to either the [JWS] or [JWE] 721 + specification. Note that whitespace is explicitly allowed in the 722 + representation and no canonicalization need be performed before 723 + encoding. 724 + 725 + 726 + 727 + 728 + 729 + 730 + Jones, et al. Standards Track [Page 13] 731 + 732 + RFC 7519 JSON Web Token (JWT) May 2015 733 + 734 + 735 + 4. Depending upon whether the JWT is a JWS or JWE, there are two 736 + cases: 737 + 738 + * If the JWT is a JWS, create a JWS using the Message as the JWS 739 + Payload; all steps specified in [JWS] for creating a JWS MUST 740 + be followed. 741 + 742 + * Else, if the JWT is a JWE, create a JWE using the Message as 743 + the plaintext for the JWE; all steps specified in [JWE] for 744 + creating a JWE MUST be followed. 745 + 746 + 5. If a nested signing or encryption operation will be performed, 747 + let the Message be the JWS or JWE, and return to Step 3, using a 748 + "cty" (content type) value of "JWT" in the new JOSE Header 749 + created in that step. 750 + 751 + 6. Otherwise, let the resulting JWT be the JWS or JWE. 752 + 753 + 7.2. Validating a JWT 754 + 755 + When validating a JWT, the following steps are performed. The order 756 + of the steps is not significant in cases where there are no 757 + dependencies between the inputs and outputs of the steps. If any of 758 + the listed steps fail, then the JWT MUST be rejected -- that is, 759 + treated by the application as an invalid input. 760 + 761 + 1. Verify that the JWT contains at least one period ('.') 762 + character. 763 + 764 + 2. Let the Encoded JOSE Header be the portion of the JWT before the 765 + first period ('.') character. 766 + 767 + 3. Base64url decode the Encoded JOSE Header following the 768 + restriction that no line breaks, whitespace, or other additional 769 + characters have been used. 770 + 771 + 4. Verify that the resulting octet sequence is a UTF-8-encoded 772 + representation of a completely valid JSON object conforming to 773 + RFC 7159 [RFC7159]; let the JOSE Header be this JSON object. 774 + 775 + 5. Verify that the resulting JOSE Header includes only parameters 776 + and values whose syntax and semantics are both understood and 777 + supported or that are specified as being ignored when not 778 + understood. 779 + 780 + 6. Determine whether the JWT is a JWS or a JWE using any of the 781 + methods described in Section 9 of [JWE]. 782 + 783 + 784 + 785 + 786 + Jones, et al. Standards Track [Page 14] 787 + 788 + RFC 7519 JSON Web Token (JWT) May 2015 789 + 790 + 791 + 7. Depending upon whether the JWT is a JWS or JWE, there are two 792 + cases: 793 + 794 + * If the JWT is a JWS, follow the steps specified in [JWS] for 795 + validating a JWS. Let the Message be the result of base64url 796 + decoding the JWS Payload. 797 + 798 + * Else, if the JWT is a JWE, follow the steps specified in 799 + [JWE] for validating a JWE. Let the Message be the resulting 800 + plaintext. 801 + 802 + 8. If the JOSE Header contains a "cty" (content type) value of 803 + "JWT", then the Message is a JWT that was the subject of nested 804 + signing or encryption operations. In this case, return to Step 805 + 1, using the Message as the JWT. 806 + 807 + 9. Otherwise, base64url decode the Message following the 808 + restriction that no line breaks, whitespace, or other additional 809 + characters have been used. 810 + 811 + 10. Verify that the resulting octet sequence is a UTF-8-encoded 812 + representation of a completely valid JSON object conforming to 813 + RFC 7159 [RFC7159]; let the JWT Claims Set be this JSON object. 814 + 815 + Finally, note that it is an application decision which algorithms may 816 + be used in a given context. Even if a JWT can be successfully 817 + validated, unless the algorithms used in the JWT are acceptable to 818 + the application, it SHOULD reject the JWT. 819 + 820 + 7.3. String Comparison Rules 821 + 822 + Processing a JWT inevitably requires comparing known strings to 823 + members and values in JSON objects. For example, in checking what 824 + the algorithm is, the Unicode [UNICODE] string encoding "alg" will be 825 + checked against the member names in the JOSE Header to see if there 826 + is a matching Header Parameter name. 827 + 828 + The JSON rules for doing member name comparison are described in 829 + Section 8.3 of RFC 7159 [RFC7159]. Since the only string comparison 830 + operations that are performed are equality and inequality, the same 831 + rules can be used for comparing both member names and member values 832 + against known strings. 833 + 834 + These comparison rules MUST be used for all JSON string comparisons 835 + except in cases where the definition of the member explicitly calls 836 + out that a different comparison rule is to be used for that member 837 + value. In this specification, only the "typ" and "cty" member values 838 + do not use these comparison rules. 839 + 840 + 841 + 842 + Jones, et al. Standards Track [Page 15] 843 + 844 + RFC 7519 JSON Web Token (JWT) May 2015 845 + 846 + 847 + Some applications may include case-insensitive information in a case- 848 + sensitive value, such as including a DNS name as part of the "iss" 849 + (issuer) claim value. In those cases, the application may need to 850 + define a convention for the canonical case to use for representing 851 + the case-insensitive portions, such as lowercasing them, if more than 852 + one party might need to produce the same value so that they can be 853 + compared. (However, if all other parties consume whatever value the 854 + producing party emitted verbatim without attempting to compare it to 855 + an independently produced value, then the case used by the producer 856 + will not matter.) 857 + 858 + 8. Implementation Requirements 859 + 860 + This section defines which algorithms and features of this 861 + specification are mandatory to implement. Applications using this 862 + specification can impose additional requirements upon implementations 863 + that they use. For instance, one application might require support 864 + for encrypted JWTs and Nested JWTs, while another might require 865 + support for signing JWTs with the Elliptic Curve Digital Signature 866 + Algorithm (ECDSA) using the P-256 curve and the SHA-256 hash 867 + algorithm ("ES256"). 868 + 869 + Of the signature and MAC algorithms specified in JSON Web Algorithms 870 + [JWA], only HMAC SHA-256 ("HS256") and "none" MUST be implemented by 871 + conforming JWT implementations. It is RECOMMENDED that 872 + implementations also support RSASSA-PKCS1-v1_5 with the SHA-256 hash 873 + algorithm ("RS256") and ECDSA using the P-256 curve and the SHA-256 874 + hash algorithm ("ES256"). Support for other algorithms and key sizes 875 + is OPTIONAL. 876 + 877 + Support for encrypted JWTs is OPTIONAL. If an implementation 878 + provides encryption capabilities, of the encryption algorithms 879 + specified in [JWA], only RSAES-PKCS1-v1_5 with 2048-bit keys 880 + ("RSA1_5"), AES Key Wrap with 128- and 256-bit keys ("A128KW" and 881 + "A256KW"), and the composite authenticated encryption algorithm using 882 + AES-CBC and HMAC SHA-2 ("A128CBC-HS256" and "A256CBC-HS512") MUST be 883 + implemented by conforming implementations. It is RECOMMENDED that 884 + implementations also support using Elliptic Curve Diffie-Hellman 885 + Ephemeral Static (ECDH-ES) to agree upon a key used to wrap the 886 + Content Encryption Key ("ECDH-ES+A128KW" and "ECDH-ES+A256KW") and 887 + AES in Galois/Counter Mode (GCM) with 128- and 256-bit keys 888 + ("A128GCM" and "A256GCM"). Support for other algorithms and key 889 + sizes is OPTIONAL. 890 + 891 + Support for Nested JWTs is OPTIONAL. 892 + 893 + 894 + 895 + 896 + 897 + 898 + Jones, et al. Standards Track [Page 16] 899 + 900 + RFC 7519 JSON Web Token (JWT) May 2015 901 + 902 + 903 + 9. URI for Declaring that Content is a JWT 904 + 905 + This specification registers the URN 906 + "urn:ietf:params:oauth:token-type:jwt" for use by applications that 907 + declare content types using URIs (rather than, for instance, media 908 + types) to indicate that the content referred to is a JWT. 909 + 910 + 10. IANA Considerations 911 + 912 + 10.1. JSON Web Token Claims Registry 913 + 914 + This section establishes the IANA "JSON Web Token Claims" registry 915 + for JWT Claim Names. The registry records the Claim Name and a 916 + reference to the specification that defines it. This section 917 + registers the Claim Names defined in Section 4.1. 918 + 919 + Values are registered on a Specification Required [RFC5226] basis 920 + after a three-week review period on the jwt-reg-review@ietf.org 921 + mailing list, on the advice of one or more Designated Experts. 922 + However, to allow for the allocation of values prior to publication, 923 + the Designated Experts may approve registration once they are 924 + satisfied that such a specification will be published. 925 + 926 + Registration requests sent to the mailing list for review should use 927 + an appropriate subject (e.g., "Request to register claim: example"). 928 + 929 + Within the review period, the Designated Experts will either approve 930 + or deny the registration request, communicating this decision to the 931 + review list and IANA. Denials should include an explanation and, if 932 + applicable, suggestions as to how to make the request successful. 933 + Registration requests that are undetermined for a period longer than 934 + 21 days can be brought to the IESG's attention (using the 935 + iesg@ietf.org mailing list) for resolution. 936 + 937 + Criteria that should be applied by the Designated Experts includes 938 + determining whether the proposed registration duplicates existing 939 + functionality, whether it is likely to be of general applicability or 940 + whether it is useful only for a single application, and whether the 941 + registration description is clear. 942 + 943 + IANA must only accept registry updates from the Designated Experts 944 + and should direct all requests for registration to the review mailing 945 + list. 946 + 947 + It is suggested that multiple Designated Experts be appointed who are 948 + able to represent the perspectives of different applications using 949 + this specification, in order to enable broadly informed review of 950 + registration decisions. In cases where a registration decision could 951 + 952 + 953 + 954 + Jones, et al. Standards Track [Page 17] 955 + 956 + RFC 7519 JSON Web Token (JWT) May 2015 957 + 958 + 959 + be perceived as creating a conflict of interest for a particular 960 + Expert, that Expert should defer to the judgment of the other 961 + Experts. 962 + 963 + 10.1.1. Registration Template 964 + 965 + Claim Name: 966 + The name requested (e.g., "iss"). Because a core goal of this 967 + specification is for the resulting representations to be compact, 968 + it is RECOMMENDED that the name be short -- that is, not to exceed 969 + 8 characters without a compelling reason to do so. This name is 970 + case sensitive. Names may not match other registered names in a 971 + case-insensitive manner unless the Designated Experts state that 972 + there is a compelling reason to allow an exception. 973 + 974 + Claim Description: 975 + Brief description of the claim (e.g., "Issuer"). 976 + 977 + Change Controller: 978 + For Standards Track RFCs, list the "IESG". For others, give the 979 + name of the responsible party. Other details (e.g., postal 980 + address, email address, home page URI) may also be included. 981 + 982 + Specification Document(s): 983 + Reference to the document or documents that specify the parameter, 984 + preferably including URIs that can be used to retrieve copies of 985 + the documents. An indication of the relevant sections may also be 986 + included but is not required. 987 + 988 + 10.1.2. Initial Registry Contents 989 + 990 + o Claim Name: "iss" 991 + o Claim Description: Issuer 992 + o Change Controller: IESG 993 + o Specification Document(s): Section 4.1.1 of RFC 7519 994 + 995 + o Claim Name: "sub" 996 + o Claim Description: Subject 997 + o Change Controller: IESG 998 + o Specification Document(s): Section 4.1.2 of RFC 7519 999 + 1000 + o Claim Name: "aud" 1001 + o Claim Description: Audience 1002 + o Change Controller: IESG 1003 + o Specification Document(s): Section 4.1.3 of RFC 7519 1004 + 1005 + 1006 + 1007 + 1008 + 1009 + 1010 + Jones, et al. Standards Track [Page 18] 1011 + 1012 + RFC 7519 JSON Web Token (JWT) May 2015 1013 + 1014 + 1015 + o Claim Name: "exp" 1016 + o Claim Description: Expiration Time 1017 + o Change Controller: IESG 1018 + o Specification Document(s): Section 4.1.4 of RFC 7519 1019 + 1020 + o Claim Name: "nbf" 1021 + o Claim Description: Not Before 1022 + o Change Controller: IESG 1023 + o Specification Document(s): Section 4.1.5 of RFC 7519 1024 + 1025 + o Claim Name: "iat" 1026 + o Claim Description: Issued At 1027 + o Change Controller: IESG 1028 + o Specification Document(s): Section 4.1.6 of RFC 7519 1029 + 1030 + o Claim Name: "jti" 1031 + o Claim Description: JWT ID 1032 + o Change Controller: IESG 1033 + o Specification Document(s): Section 4.1.7 of RFC 7519 1034 + 1035 + 10.2. Sub-Namespace Registration of 1036 + urn:ietf:params:oauth:token-type:jwt 1037 + 1038 + 10.2.1. Registry Contents 1039 + 1040 + This section registers the value "token-type:jwt" in the IANA "OAuth 1041 + URI" registry established by "An IETF URN Sub-Namespace for OAuth" 1042 + [RFC6755], which can be used to indicate that the content is a JWT. 1043 + 1044 + o URN: urn:ietf:params:oauth:token-type:jwt 1045 + o Common Name: JSON Web Token (JWT) Token Type 1046 + o Change Controller: IESG 1047 + o Specification Document(s): RFC 7519 1048 + 1049 + 1050 + 1051 + 1052 + 1053 + 1054 + 1055 + 1056 + 1057 + 1058 + 1059 + 1060 + 1061 + 1062 + 1063 + 1064 + 1065 + 1066 + Jones, et al. Standards Track [Page 19] 1067 + 1068 + RFC 7519 JSON Web Token (JWT) May 2015 1069 + 1070 + 1071 + 10.3. Media Type Registration 1072 + 1073 + 10.3.1. Registry Contents 1074 + 1075 + This section registers the "application/jwt" media type [RFC2046] in 1076 + the "Media Types" registry [IANA.MediaTypes] in the manner described 1077 + in RFC 6838 [RFC6838], which can be used to indicate that the content 1078 + is a JWT. 1079 + 1080 + o Type name: application 1081 + o Subtype name: jwt 1082 + o Required parameters: n/a 1083 + o Optional parameters: n/a 1084 + o Encoding considerations: 8bit; JWT values are encoded as a series 1085 + of base64url-encoded values (some of which may be the empty 1086 + string) separated by period ('.') characters. 1087 + o Security considerations: See the Security Considerations section 1088 + of RFC 7519 1089 + o Interoperability considerations: n/a 1090 + o Published specification: RFC 7519 1091 + o Applications that use this media type: OpenID Connect, Mozilla 1092 + Persona, Salesforce, Google, Android, Windows Azure, Amazon Web 1093 + Services, and numerous others 1094 + o Fragment identifier considerations: n/a 1095 + o Additional information: 1096 + 1097 + Magic number(s): n/a 1098 + File extension(s): n/a 1099 + Macintosh file type code(s): n/a 1100 + 1101 + o Person & email address to contact for further information: 1102 + Michael B. Jones, mbj@microsoft.com 1103 + o Intended usage: COMMON 1104 + o Restrictions on usage: none 1105 + o Author: Michael B. Jones, mbj@microsoft.com 1106 + o Change controller: IESG 1107 + o Provisional registration? No 1108 + 1109 + 10.4. Header Parameter Names Registration 1110 + 1111 + This section registers specific Claim Names defined in Section 4.1 in 1112 + the IANA "JSON Web Signature and Encryption Header Parameters" 1113 + registry established by [JWS] for use by claims replicated as Header 1114 + Parameters in JWEs, per Section 5.3. 1115 + 1116 + 1117 + 1118 + 1119 + 1120 + 1121 + 1122 + Jones, et al. Standards Track [Page 20] 1123 + 1124 + RFC 7519 JSON Web Token (JWT) May 2015 1125 + 1126 + 1127 + 10.4.1. Registry Contents 1128 + 1129 + o Header Parameter Name: "iss" 1130 + o Header Parameter Description: Issuer 1131 + o Header Parameter Usage Location(s): JWE 1132 + o Change Controller: IESG 1133 + o Specification Document(s): Section 4.1.1 of RFC 7519 1134 + 1135 + o Header Parameter Name: "sub" 1136 + o Header Parameter Description: Subject 1137 + o Header Parameter Usage Location(s): JWE 1138 + o Change Controller: IESG 1139 + o Specification Document(s): Section 4.1.2 of RFC 7519 1140 + 1141 + o Header Parameter Name: "aud" 1142 + o Header Parameter Description: Audience 1143 + o Header Parameter Usage Location(s): JWE 1144 + o Change Controller: IESG 1145 + o Specification Document(s): Section 4.1.3 of RFC 7519 1146 + 1147 + 11. Security Considerations 1148 + 1149 + All of the security issues that are pertinent to any cryptographic 1150 + application must be addressed by JWT/JWS/JWE/JWK agents. Among these 1151 + issues are protecting the user's asymmetric private and symmetric 1152 + secret keys and employing countermeasures to various attacks. 1153 + 1154 + All the security considerations in the JWS specification also apply 1155 + to JWT, as do the JWE security considerations when encryption is 1156 + employed. In particular, Sections 10.12 ("JSON Security 1157 + Considerations") and 10.13 ("Unicode Comparison Security 1158 + Considerations") of [JWS] apply equally to the JWT Claims Set in the 1159 + same manner that they do to the JOSE Header. 1160 + 1161 + 11.1. Trust Decisions 1162 + 1163 + The contents of a JWT cannot be relied upon in a trust decision 1164 + unless its contents have been cryptographically secured and bound to 1165 + the context necessary for the trust decision. In particular, the 1166 + key(s) used to sign and/or encrypt the JWT will typically need to 1167 + verifiably be under the control of the party identified as the issuer 1168 + of the JWT. 1169 + 1170 + 11.2. Signing and Encryption Order 1171 + 1172 + While syntactically the signing and encryption operations for Nested 1173 + JWTs may be applied in any order, if both signing and encryption are 1174 + necessary, normally producers should sign the message and then 1175 + 1176 + 1177 + 1178 + Jones, et al. Standards Track [Page 21] 1179 + 1180 + RFC 7519 JSON Web Token (JWT) May 2015 1181 + 1182 + 1183 + encrypt the result (thus encrypting the signature). This prevents 1184 + attacks in which the signature is stripped, leaving just an encrypted 1185 + message, as well as providing privacy for the signer. Furthermore, 1186 + signatures over encrypted text are not considered valid in many 1187 + jurisdictions. 1188 + 1189 + Note that potential concerns about security issues related to the 1190 + order of signing and encryption operations are already addressed by 1191 + the underlying JWS and JWE specifications; in particular, because JWE 1192 + only supports the use of authenticated encryption algorithms, 1193 + cryptographic concerns about the potential need to sign after 1194 + encryption that apply in many contexts do not apply to this 1195 + specification. 1196 + 1197 + 12. Privacy Considerations 1198 + 1199 + A JWT may contain privacy-sensitive information. When this is the 1200 + case, measures MUST be taken to prevent disclosure of this 1201 + information to unintended parties. One way to achieve this is to use 1202 + an encrypted JWT and authenticate the recipient. Another way is to 1203 + ensure that JWTs containing unencrypted privacy-sensitive information 1204 + are only transmitted using protocols utilizing encryption that 1205 + support endpoint authentication, such as Transport Layer Security 1206 + (TLS). Omitting privacy-sensitive information from a JWT is the 1207 + simplest way of minimizing privacy issues. 1208 + 1209 + 13. References 1210 + 1211 + 13.1. Normative References 1212 + 1213 + [ECMAScript] 1214 + Ecma International, "ECMAScript Language Specification, 1215 + 5.1 Edition", ECMA Standard 262, June 2011, 1216 + <http://www.ecma-international.org/ecma-262/5.1/ 1217 + ECMA-262.pdf>. 1218 + 1219 + [IANA.MediaTypes] 1220 + IANA, "Media Types", 1221 + <http://www.iana.org/assignments/media-types>. 1222 + 1223 + [JWA] Jones, M., "JSON Web Algorithms (JWA)", RFC 7518, 1224 + DOI 10.17487/RFC7518, May 2015, 1225 + <http://www.rfc-editor.org/info/rfc7518>. 1226 + 1227 + [JWE] Jones, M. and J. Hildebrand, "JSON Web Encryption (JWE)", 1228 + RFC 7516, DOI 10.17487/RFC7516, May 2015, 1229 + <http://www.rfc-editor.org/info/rfc7516>. 1230 + 1231 + 1232 + 1233 + 1234 + Jones, et al. Standards Track [Page 22] 1235 + 1236 + RFC 7519 JSON Web Token (JWT) May 2015 1237 + 1238 + 1239 + [JWS] Jones, M., Bradley, J., and N. Sakimura, "JSON Web 1240 + Signature (JWS)", RFC 7515, DOI 10.17487/RFC, May 2015, 1241 + <http://www.rfc-editor.org/info/rfc7515>. 1242 + 1243 + [RFC20] Cerf, V., "ASCII format for Network Interchange", STD 80, 1244 + RFC 20, DOI 10.17487/RFC0020, October 1969, 1245 + <http://www.rfc-editor.org/info/rfc20>. 1246 + 1247 + [RFC2046] Freed, N. and N. Borenstein, "Multipurpose Internet Mail 1248 + Extensions (MIME) Part Two: Media Types", RFC 2046, 1249 + DOI 10.17487/RFC2046, November 1996, 1250 + <http://www.rfc-editor.org/info/rfc2046>. 1251 + 1252 + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate 1253 + Requirement Levels", BCP 14, RFC 2119, 1254 + DOI 10.17487/RFC2119, March 1997, 1255 + <http://www.rfc-editor.org/info/rfc2119>. 1256 + 1257 + [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform 1258 + Resource Identifier (URI): Generic Syntax", STD 66, 1259 + RFC 3986, DOI 10.17487/RFC3986, January 2005, 1260 + <http://www.rfc-editor.org/info/rfc3986>. 1261 + 1262 + [RFC4949] Shirey, R., "Internet Security Glossary, Version 2", 1263 + FYI 36, RFC 4949, DOI 10.17487/RFC4949, August 2007, 1264 + <http://www.rfc-editor.org/info/rfc4949>. 1265 + 1266 + [RFC7159] Bray, T., Ed., "The JavaScript Object Notation (JSON) Data 1267 + Interchange Format", RFC 7159, DOI 10.17487/RFC7159, March 1268 + 2014, <http://www.rfc-editor.org/info/rfc7159>. 1269 + 1270 + [UNICODE] The Unicode Consortium, "The Unicode Standard", 1271 + <http://www.unicode.org/versions/latest/>. 1272 + 1273 + 13.2. Informative References 1274 + 1275 + [CanvasApp] 1276 + Facebook, "Canvas Applications", 2010, 1277 + <http://developers.facebook.com/docs/authentication/ 1278 + canvas>. 1279 + 1280 + [JSS] Bradley, J. and N. Sakimura (editor), "JSON Simple Sign", 1281 + September 2010, <http://jsonenc.info/jss/1.0/>. 1282 + 1283 + 1284 + 1285 + 1286 + 1287 + 1288 + 1289 + 1290 + Jones, et al. Standards Track [Page 23] 1291 + 1292 + RFC 7519 JSON Web Token (JWT) May 2015 1293 + 1294 + 1295 + [MagicSignatures] 1296 + Panzer, J., Ed., Laurie, B., and D. Balfanz, "Magic 1297 + Signatures", January 2011, 1298 + <http://salmon-protocol.googlecode.com/svn/ 1299 + trunk/draft-panzer-magicsig-01.html>. 1300 + 1301 + [OASIS.saml-core-2.0-os] 1302 + Cantor, S., Kemp, J., Philpott, R., and E. Maler, 1303 + "Assertions and Protocols for the OASIS Security Assertion 1304 + Markup Language (SAML) V2.0", OASIS Standard 1305 + saml-core-2.0-os, March 2005, 1306 + <http://docs.oasis-open.org/security/saml/v2.0/ 1307 + saml-core-2.0-os.pdf>. 1308 + 1309 + [POSIX.1] IEEE, "The Open Group Base Specifications Issue 7", IEEE 1310 + Std 1003.1, 2013 Edition, 2013, 1311 + <http://pubs.opengroup.org/onlinepubs/9699919799/ 1312 + basedefs/V1_chap04.html#tag_04_15>. 1313 + 1314 + [RFC3275] Eastlake 3rd, D., Reagle, J., and D. Solo, "(Extensible 1315 + Markup Language) XML-Signature Syntax and Processing", 1316 + RFC 3275, DOI 10.17487/RFC3275, March 2002, 1317 + <http://www.rfc-editor.org/info/rfc3275>. 1318 + 1319 + [RFC3339] Klyne, G. and C. Newman, "Date and Time on the Internet: 1320 + Timestamps", RFC 3339, DOI 10.17487/RFC3339, July 2002, 1321 + <http://www.rfc-editor.org/info/rfc3339>. 1322 + 1323 + [RFC4122] Leach, P., Mealling, M., and R. Salz, "A Universally 1324 + Unique IDentifier (UUID) URN Namespace", RFC 4122, 1325 + DOI 10.17487/RFC4122, July 2005, 1326 + <http://www.rfc-editor.org/info/rfc4122>. 1327 + 1328 + [RFC5226] Narten, T. and H. Alvestrand, "Guidelines for Writing an 1329 + IANA Considerations Section in RFCs", BCP 26, RFC 5226, 1330 + DOI 10.17487/RFC5226, May 2008, 1331 + <http://www.rfc-editor.org/info/rfc5226>. 1332 + 1333 + [RFC6755] Campbell, B. and H. Tschofenig, "An IETF URN Sub-Namespace 1334 + for OAuth", RFC 6755, DOI 10.17487/RFC6755, October 2012, 1335 + <http://www.rfc-editor.org/info/rfc6755>. 1336 + 1337 + [RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type 1338 + Specifications and Registration Procedures", BCP 13, 1339 + RFC 6838, DOI 10.17487/RFC6838, January 2013, 1340 + <http://www.rfc-editor.org/info/rfc6838>. 1341 + 1342 + 1343 + 1344 + 1345 + 1346 + Jones, et al. Standards Track [Page 24] 1347 + 1348 + RFC 7519 JSON Web Token (JWT) May 2015 1349 + 1350 + 1351 + [SWT] Hardt, D. and Y. Goland, "Simple Web Token (SWT)", Version 1352 + 0.9.5.1, November 2009, <http://msdn.microsoft.com/en-us/ 1353 + library/windowsazure/hh781551.aspx>. 1354 + 1355 + [W3C.CR-xml11-20060816] 1356 + Cowan, J., "Extensible Markup Language (XML) 1.1 (Second 1357 + Edition)", World Wide Web Consortium Recommendation 1358 + REC-xml11-20060816, August 2006, 1359 + <http://www.w3.org/TR/2006/REC-xml11-20060816>. 1360 + 1361 + [W3C.REC-xml-c14n-20010315] 1362 + Boyer, J., "Canonical XML Version 1.0", World Wide Web 1363 + Consortium Recommendation REC-xml-c14n-20010315, March 1364 + 2001, <http://www.w3.org/TR/2001/REC-xml-c14n-20010315>. 1365 + 1366 + 1367 + 1368 + 1369 + 1370 + 1371 + 1372 + 1373 + 1374 + 1375 + 1376 + 1377 + 1378 + 1379 + 1380 + 1381 + 1382 + 1383 + 1384 + 1385 + 1386 + 1387 + 1388 + 1389 + 1390 + 1391 + 1392 + 1393 + 1394 + 1395 + 1396 + 1397 + 1398 + 1399 + 1400 + 1401 + 1402 + Jones, et al. Standards Track [Page 25] 1403 + 1404 + RFC 7519 JSON Web Token (JWT) May 2015 1405 + 1406 + 1407 + Appendix A. JWT Examples 1408 + 1409 + This section contains examples of JWTs. For other example JWTs, see 1410 + Section 6.1 of this document and Appendices A.1 - A.3 of [JWS]. 1411 + 1412 + A.1. Example Encrypted JWT 1413 + 1414 + This example encrypts the same claims as used in Section 3.1 to the 1415 + recipient using RSAES-PKCS1-v1_5 and AES_128_CBC_HMAC_SHA_256. 1416 + 1417 + The following example JOSE Header declares that: 1418 + 1419 + o The Content Encryption Key is encrypted to the recipient using the 1420 + RSAES-PKCS1-v1_5 algorithm to produce the JWE Encrypted Key. 1421 + o Authenticated encryption is performed on the plaintext using the 1422 + AES_128_CBC_HMAC_SHA_256 algorithm to produce the JWE Ciphertext 1423 + and the JWE Authentication Tag. 1424 + 1425 + {"alg":"RSA1_5","enc":"A128CBC-HS256"} 1426 + 1427 + Other than using the octets of the UTF-8 representation of the JWT 1428 + Claims Set from Section 3.1 as the plaintext value, the computation 1429 + of this JWT is identical to the computation of the JWE in 1430 + Appendix A.2 of [JWE], including the keys used. 1431 + 1432 + The final result in this example (with line breaks for display 1433 + purposes only) is: 1434 + 1435 + eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0. 1436 + QR1Owv2ug2WyPBnbQrRARTeEk9kDO2w8qDcjiHnSJflSdv1iNqhWXaKH4MqAkQtM 1437 + oNfABIPJaZm0HaA415sv3aeuBWnD8J-Ui7Ah6cWafs3ZwwFKDFUUsWHSK-IPKxLG 1438 + TkND09XyjORj_CHAgOPJ-Sd8ONQRnJvWn_hXV1BNMHzUjPyYwEsRhDhzjAD26ima 1439 + sOTsgruobpYGoQcXUwFDn7moXPRfDE8-NoQX7N7ZYMmpUDkR-Cx9obNGwJQ3nM52 1440 + YCitxoQVPzjbl7WBuB7AohdBoZOdZ24WlN1lVIeh8v1K4krB8xgKvRU8kgFrEn_a 1441 + 1rZgN5TiysnmzTROF869lQ. 1442 + AxY8DCtDaGlsbGljb3RoZQ. 1443 + MKOle7UQrG6nSxTLX6Mqwt0orbHvAKeWnDYvpIAeZ72deHxz3roJDXQyhxx0wKaM 1444 + HDjUEOKIwrtkHthpqEanSBNYHZgmNOV7sln1Eu9g3J8. 1445 + fiK51VwhsxJ-siBMR-YFiA 1446 + 1447 + A.2. Example Nested JWT 1448 + 1449 + This example shows how a JWT can be used as the payload of a JWE or 1450 + JWS to create a Nested JWT. In this case, the JWT Claims Set is 1451 + first signed, and then encrypted. 1452 + 1453 + 1454 + 1455 + 1456 + 1457 + 1458 + Jones, et al. Standards Track [Page 26] 1459 + 1460 + RFC 7519 JSON Web Token (JWT) May 2015 1461 + 1462 + 1463 + The inner signed JWT is identical to the example in Appendix A.2 of 1464 + [JWS]. Therefore, its computation is not repeated here. This 1465 + example then encrypts this inner JWT to the recipient using 1466 + RSAES-PKCS1-v1_5 and AES_128_CBC_HMAC_SHA_256. 1467 + 1468 + The following example JOSE Header declares that: 1469 + 1470 + o The Content Encryption Key is encrypted to the recipient using the 1471 + RSAES-PKCS1-v1_5 algorithm to produce the JWE Encrypted Key. 1472 + o Authenticated encryption is performed on the plaintext using the 1473 + AES_128_CBC_HMAC_SHA_256 algorithm to produce the JWE Ciphertext 1474 + and the JWE Authentication Tag. 1475 + o The plaintext is itself a JWT. 1476 + 1477 + {"alg":"RSA1_5","enc":"A128CBC-HS256","cty":"JWT"} 1478 + 1479 + Base64url encoding the octets of the UTF-8 representation of the JOSE 1480 + Header yields this encoded JOSE Header value: 1481 + 1482 + eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5IjoiSldUIn0 1483 + 1484 + The computation of this JWT is identical to the computation of the 1485 + JWE in Appendix A.2 of [JWE], other than that different JOSE Header, 1486 + plaintext, JWE Initialization Vector, and Content Encryption Key 1487 + values are used. (The RSA key used is the same.) 1488 + 1489 + The plaintext used is the octets of the ASCII [RFC20] representation 1490 + of the JWT at the end of Appendix A.2.1 of [JWS] (with all whitespace 1491 + and line breaks removed), which is a sequence of 458 octets. 1492 + 1493 + The JWE Initialization Vector value used (using JSON array notation) 1494 + is: 1495 + 1496 + [82, 101, 100, 109, 111, 110, 100, 32, 87, 65, 32, 57, 56, 48, 53, 1497 + 50] 1498 + 1499 + This example uses the Content Encryption Key represented by the 1500 + base64url-encoded value below: 1501 + 1502 + GawgguFyGrWKav7AX4VKUg 1503 + 1504 + 1505 + 1506 + 1507 + 1508 + 1509 + 1510 + 1511 + 1512 + 1513 + 1514 + Jones, et al. Standards Track [Page 27] 1515 + 1516 + RFC 7519 JSON Web Token (JWT) May 2015 1517 + 1518 + 1519 + The final result for this Nested JWT (with line breaks for display 1520 + purposes only) is: 1521 + 1522 + eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5IjoiSldU 1523 + In0. 1524 + g_hEwksO1Ax8Qn7HoN-BVeBoa8FXe0kpyk_XdcSmxvcM5_P296JXXtoHISr_DD_M 1525 + qewaQSH4dZOQHoUgKLeFly-9RI11TG-_Ge1bZFazBPwKC5lJ6OLANLMd0QSL4fYE 1526 + b9ERe-epKYE3xb2jfY1AltHqBO-PM6j23Guj2yDKnFv6WO72tteVzm_2n17SBFvh 1527 + DuR9a2nHTE67pe0XGBUS_TK7ecA-iVq5COeVdJR4U4VZGGlxRGPLRHvolVLEHx6D 1528 + YyLpw30Ay9R6d68YCLi9FYTq3hIXPK_-dmPlOUlKvPr1GgJzRoeC9G5qCvdcHWsq 1529 + JGTO_z3Wfo5zsqwkxruxwA. 1530 + UmVkbW9uZCBXQSA5ODA1Mg. 1531 + VwHERHPvCNcHHpTjkoigx3_ExK0Qc71RMEParpatm0X_qpg-w8kozSjfNIPPXiTB 1532 + BLXR65CIPkFqz4l1Ae9w_uowKiwyi9acgVztAi-pSL8GQSXnaamh9kX1mdh3M_TT 1533 + -FZGQFQsFhu0Z72gJKGdfGE-OE7hS1zuBD5oEUfk0Dmb0VzWEzpxxiSSBbBAzP10 1534 + l56pPfAtrjEYw-7ygeMkwBl6Z_mLS6w6xUgKlvW6ULmkV-uLC4FUiyKECK4e3WZY 1535 + Kw1bpgIqGYsw2v_grHjszJZ-_I5uM-9RA8ycX9KqPRp9gc6pXmoU_-27ATs9XCvr 1536 + ZXUtK2902AUzqpeEUJYjWWxSNsS-r1TJ1I-FMJ4XyAiGrfmo9hQPcNBYxPz3GQb2 1537 + 8Y5CLSQfNgKSGt0A4isp1hBUXBHAndgtcslt7ZoQJaKe_nNJgNliWtWpJ_ebuOpE 1538 + l8jdhehdccnRMIwAmU1n7SPkmhIl1HlSOpvcvDfhUN5wuqU955vOBvfkBOh5A11U 1539 + zBuo2WlgZ6hYi9-e3w29bR0C2-pp3jbqxEDw3iWaf2dc5b-LnR0FEYXvI_tYk5rd 1540 + _J9N0mg0tQ6RbpxNEMNoA9QWk5lgdPvbh9BaO195abQ. 1541 + AVO9iT5AV4CzvDJCdhSFlQ 1542 + 1543 + Appendix B. Relationship of JWTs to SAML Assertions 1544 + 1545 + Security Assertion Markup Language (SAML) 2.0 1546 + [OASIS.saml-core-2.0-os] provides a standard for creating security 1547 + tokens with greater expressivity and more security options than 1548 + supported by JWTs. However, the cost of this flexibility and 1549 + expressiveness is both size and complexity. SAML's use of XML 1550 + [W3C.CR-xml11-20060816] and XML Digital Signature (DSIG) [RFC3275] 1551 + contributes to the size of SAML Assertions; its use of XML and 1552 + especially XML Canonicalization [W3C.REC-xml-c14n-20010315] 1553 + contributes to their complexity. 1554 + 1555 + JWTs are intended to provide a simple security token format that is 1556 + small enough to fit into HTTP headers and query arguments in URIs. 1557 + It does this by supporting a much simpler token model than SAML and 1558 + using the JSON [RFC7159] object encoding syntax. It also supports 1559 + securing tokens using Message Authentication Codes (MACs) and digital 1560 + signatures using a smaller (and less flexible) format than XML DSIG. 1561 + 1562 + Therefore, while JWTs can do some of the things SAML Assertions do, 1563 + JWTs are not intended as a full replacement for SAML Assertions, but 1564 + rather as a token format to be used when ease of implementation or 1565 + compactness are considerations. 1566 + 1567 + 1568 + 1569 + 1570 + Jones, et al. Standards Track [Page 28] 1571 + 1572 + RFC 7519 JSON Web Token (JWT) May 2015 1573 + 1574 + 1575 + SAML Assertions are always statements made by an entity about a 1576 + subject. JWTs are often used in the same manner, with the entity 1577 + making the statements being represented by the "iss" (issuer) claim, 1578 + and the subject being represented by the "sub" (subject) claim. 1579 + However, with these claims being optional, other uses of the JWT 1580 + format are also permitted. 1581 + 1582 + Appendix C. Relationship of JWTs to Simple Web Tokens (SWTs) 1583 + 1584 + Both JWTs and SWTs [SWT], at their core, enable sets of claims to be 1585 + communicated between applications. For SWTs, both the claim names 1586 + and claim values are strings. For JWTs, while claim names are 1587 + strings, claim values can be any JSON type. Both token types offer 1588 + cryptographic protection of their content: SWTs with HMAC SHA-256 and 1589 + JWTs with a choice of algorithms, including signature, MAC, and 1590 + encryption algorithms. 1591 + 1592 + Acknowledgements 1593 + 1594 + The authors acknowledge that the design of JWTs was intentionally 1595 + influenced by the design and simplicity of SWTs [SWT] and ideas for 1596 + JSON tokens that Dick Hardt discussed within the OpenID community. 1597 + 1598 + Solutions for signing JSON content were previously explored by Magic 1599 + Signatures [MagicSignatures], JSON Simple Sign [JSS], and Canvas 1600 + Applications [CanvasApp], all of which influenced this document. 1601 + 1602 + This specification is the work of the OAuth working group, which 1603 + includes dozens of active and dedicated participants. In particular, 1604 + the following individuals contributed ideas, feedback, and wording 1605 + that influenced this specification: 1606 + 1607 + Dirk Balfanz, Richard Barnes, Brian Campbell, Alissa Cooper, Breno de 1608 + Medeiros, Stephen Farrell, Yaron Y. Goland, Dick Hardt, Joe 1609 + Hildebrand, Jeff Hodges, Edmund Jay, Warren Kumari, Ben Laurie, Barry 1610 + Leiba, Ted Lemon, James Manger, Prateek Mishra, Kathleen Moriarty, 1611 + Tony Nadalin, Axel Nennker, John Panzer, Emmanuel Raviart, David 1612 + Recordon, Eric Rescorla, Jim Schaad, Paul Tarjan, Hannes Tschofenig, 1613 + Sean Turner, and Tom Yu. 1614 + 1615 + Hannes Tschofenig and Derek Atkins chaired the OAuth working group 1616 + and Sean Turner, Stephen Farrell, and Kathleen Moriarty served as 1617 + Security Area Directors during the creation of this specification. 1618 + 1619 + 1620 + 1621 + 1622 + 1623 + 1624 + 1625 + 1626 + Jones, et al. Standards Track [Page 29] 1627 + 1628 + RFC 7519 JSON Web Token (JWT) May 2015 1629 + 1630 + 1631 + Authors' Addresses 1632 + 1633 + Michael B. Jones 1634 + Microsoft 1635 + 1636 + EMail: mbj@microsoft.com 1637 + URI: http://self-issued.info/ 1638 + 1639 + 1640 + John Bradley 1641 + Ping Identity 1642 + 1643 + EMail: ve7jtb@ve7jtb.com 1644 + URI: http://www.thread-safe.com/ 1645 + 1646 + 1647 + Nat Sakimura 1648 + Nomura Research Institute 1649 + 1650 + EMail: n-sakimura@nri.co.jp 1651 + URI: http://nat.sakimura.org/ 1652 + 1653 + 1654 + 1655 + 1656 + 1657 + 1658 + 1659 + 1660 + 1661 + 1662 + 1663 + 1664 + 1665 + 1666 + 1667 + 1668 + 1669 + 1670 + 1671 + 1672 + 1673 + 1674 + 1675 + 1676 + 1677 + 1678 + 1679 + 1680 + 1681 + 1682 + Jones, et al. Standards Track [Page 30] 1683 +
+3
ocaml-jsonwt/test/dune
··· 1 + (test 2 + (name test_jsonwt) 3 + (libraries jsonwt alcotest))
+444
ocaml-jsonwt/test/test_jsonwt.ml
··· 1 + (** JWT Library Tests 2 + 3 + Comprehensive tests derived from RFC 7519 (JSON Web Token) 4 + and RFC 7515 (JSON Web Signature) specifications. *) 5 + 6 + (* RFC 7515 Appendix A.1 symmetric key for HS256 *) 7 + let rfc_hs256_key_b64 = 8 + "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow" 9 + 10 + (* RFC 7519 Section 3.1 example JWT (HS256) *) 11 + let rfc_section3_1_token = 12 + "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.\ 13 + eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.\ 14 + dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 15 + 16 + (* RFC 7519 Section 6.1 unsecured JWT *) 17 + let rfc_section6_1_token = 18 + "eyJhbGciOiJub25lIn0.\ 19 + eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.\ 20 + " 21 + 22 + (* Helper to decode base64url to bytes *) 23 + let b64url_decode s = 24 + (* Pad to multiple of 4 *) 25 + let pad = match String.length s mod 4 with 26 + | 0 -> "" 27 + | 2 -> "==" 28 + | 3 -> "=" 29 + | _ -> failwith "invalid base64url" 30 + in 31 + (* Convert URL-safe chars to standard base64 *) 32 + let s = String.map (function '-' -> '+' | '_' -> '/' | c -> c) s in 33 + Base64.decode_exn (s ^ pad) 34 + 35 + (* ============= Algorithm Tests ============= *) 36 + 37 + let test_algorithm_roundtrip () = 38 + let open Jsonwt.Algorithm in 39 + let algs = [ None; HS256; HS384; HS512; RS256; RS384; RS512; ES256; ES384; ES512; EdDSA ] in 40 + List.iter (fun alg -> 41 + let s = to_string alg in 42 + match of_string s with 43 + | Ok alg' -> 44 + Alcotest.(check string) "roundtrip" s (to_string alg') 45 + | Error e -> 46 + Alcotest.fail (Jsonwt.error_to_string e) 47 + ) algs 48 + 49 + let test_algorithm_unknown () = 50 + match Jsonwt.Algorithm.of_string "UNKNOWN" with 51 + | Error (Jsonwt.Unsupported_algorithm "UNKNOWN") -> () 52 + | Error _ -> Alcotest.fail "Expected Unsupported_algorithm error" 53 + | Ok _ -> Alcotest.fail "Expected error for unknown algorithm" 54 + 55 + (* ============= Header Tests ============= *) 56 + 57 + let test_header_create () = 58 + let h = Jsonwt.Header.make ~typ:"JWT" Jsonwt.Algorithm.HS256 in 59 + Alcotest.(check (option string)) "typ" (Some "JWT") h.typ; 60 + Alcotest.(check bool) "alg" true (h.alg = Jsonwt.Algorithm.HS256) 61 + 62 + let test_header_with_kid () = 63 + let h = Jsonwt.Header.make ~typ:"JWT" ~kid:"key-123" Jsonwt.Algorithm.RS256 in 64 + Alcotest.(check (option string)) "kid" (Some "key-123") h.kid; 65 + Alcotest.(check bool) "alg" true (h.alg = Jsonwt.Algorithm.RS256) 66 + 67 + (* ============= Claims Tests ============= *) 68 + 69 + let test_claims_builder () = 70 + let claims = 71 + Jsonwt.Claims.empty 72 + |> Jsonwt.Claims.set_iss "test-issuer" 73 + |> Jsonwt.Claims.set_sub "test-subject" 74 + |> Jsonwt.Claims.set_string "custom" "value" 75 + |> Jsonwt.Claims.build 76 + in 77 + Alcotest.(check (option string)) "iss" (Some "test-issuer") (Jsonwt.Claims.iss claims); 78 + Alcotest.(check (option string)) "sub" (Some "test-subject") (Jsonwt.Claims.sub claims); 79 + Alcotest.(check (option string)) "custom" (Some "value") (Jsonwt.Claims.get_string "custom" claims) 80 + 81 + let test_claims_with_timestamps () = 82 + let now = Ptime.of_float_s 1609459200. |> Option.get in (* 2021-01-01 00:00:00 UTC *) 83 + let exp = Ptime.of_float_s 1609545600. |> Option.get in (* 2021-01-02 00:00:00 UTC *) 84 + let claims = 85 + Jsonwt.Claims.empty 86 + |> Jsonwt.Claims.set_iat now 87 + |> Jsonwt.Claims.set_exp exp 88 + |> Jsonwt.Claims.set_nbf now 89 + |> Jsonwt.Claims.build 90 + in 91 + Alcotest.(check (option bool)) "has exp" (Some true) (Option.map (fun _ -> true) (Jsonwt.Claims.exp claims)); 92 + Alcotest.(check (option bool)) "has iat" (Some true) (Option.map (fun _ -> true) (Jsonwt.Claims.iat claims)); 93 + Alcotest.(check (option bool)) "has nbf" (Some true) (Option.map (fun _ -> true) (Jsonwt.Claims.nbf claims)) 94 + 95 + let test_claims_audience_single () = 96 + let claims = 97 + Jsonwt.Claims.empty 98 + |> Jsonwt.Claims.set_aud [ "my-app" ] 99 + |> Jsonwt.Claims.build 100 + in 101 + Alcotest.(check (list string)) "aud" [ "my-app" ] (Jsonwt.Claims.aud claims) 102 + 103 + let test_claims_audience_multiple () = 104 + let claims = 105 + Jsonwt.Claims.empty 106 + |> Jsonwt.Claims.set_aud [ "app1"; "app2"; "app3" ] 107 + |> Jsonwt.Claims.build 108 + in 109 + Alcotest.(check (list string)) "aud" [ "app1"; "app2"; "app3" ] (Jsonwt.Claims.aud claims) 110 + 111 + (* ============= Parse Tests ============= *) 112 + 113 + let test_parse_invalid () = 114 + match Jsonwt.parse "not-a-jwt" with 115 + | Error (Jsonwt.Invalid_structure _) -> () 116 + | Error _ -> Alcotest.fail "Expected Invalid_structure error" 117 + | Ok _ -> Alcotest.fail "Expected parse to fail" 118 + 119 + let test_parse_malformed () = 120 + match Jsonwt.parse "a.b" with 121 + | Error (Jsonwt.Invalid_structure _) -> () 122 + | Error _ -> Alcotest.fail "Expected Invalid_structure error" 123 + | Ok _ -> Alcotest.fail "Expected parse to fail with two parts" 124 + 125 + let test_parse_invalid_base64 () = 126 + match Jsonwt.parse "!!!.@@@.###" with 127 + | Error (Jsonwt.Invalid_base64url _) -> () 128 + | Error e -> Alcotest.fail (Printf.sprintf "Expected Invalid_base64url, got %s" (Jsonwt.error_to_string e)) 129 + | Ok _ -> Alcotest.fail "Expected parse to fail with invalid base64" 130 + 131 + (* ============= RFC 7519 Test Vectors ============= *) 132 + 133 + (* RFC 7519 Section 6.1: Unsecured JWT *) 134 + let test_rfc_unsecured_jwt_parse () = 135 + match Jsonwt.parse rfc_section6_1_token with 136 + | Ok jwt -> 137 + Alcotest.(check bool) "alg is none" true (jwt.header.alg = Jsonwt.Algorithm.None); 138 + Alcotest.(check (option string)) "iss is joe" (Some "joe") (Jsonwt.Claims.iss jwt.claims); 139 + Alcotest.(check string) "signature is empty" "" jwt.signature 140 + | Error e -> 141 + Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e)) 142 + 143 + let test_rfc_unsecured_jwt_verify_rejected_by_default () = 144 + match Jsonwt.parse rfc_section6_1_token with 145 + | Ok jwt -> 146 + let key = Jsonwt.Jwk.symmetric "" in (* dummy key *) 147 + begin match Jsonwt.verify ~key jwt with 148 + | Error Jsonwt.Unsecured_not_allowed -> () 149 + | Error e -> Alcotest.fail (Printf.sprintf "Expected Unsecured_not_allowed, got: %s" (Jsonwt.error_to_string e)) 150 + | Ok () -> Alcotest.fail "Unsecured JWT should be rejected by default" 151 + end 152 + | Error e -> 153 + Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e)) 154 + 155 + let test_rfc_unsecured_jwt_verify_allowed_with_opt_in () = 156 + match Jsonwt.parse rfc_section6_1_token with 157 + | Ok jwt -> 158 + let key = Jsonwt.Jwk.symmetric "" in (* dummy key *) 159 + begin match Jsonwt.verify ~key ~allow_none:true jwt with 160 + | Ok () -> () 161 + | Error e -> Alcotest.fail (Printf.sprintf "Verification failed: %s" (Jsonwt.error_to_string e)) 162 + end 163 + | Error e -> 164 + Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e)) 165 + 166 + (* RFC 7519 Section 3.1: HS256 JWT *) 167 + let test_rfc_hs256_jwt_parse () = 168 + match Jsonwt.parse rfc_section3_1_token with 169 + | Ok jwt -> 170 + Alcotest.(check bool) "alg is HS256" true (jwt.header.alg = Jsonwt.Algorithm.HS256); 171 + Alcotest.(check (option string)) "typ is JWT" (Some "JWT") jwt.header.typ; 172 + Alcotest.(check (option string)) "iss is joe" (Some "joe") (Jsonwt.Claims.iss jwt.claims) 173 + | Error e -> 174 + Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e)) 175 + 176 + let test_rfc_hs256_jwt_verify () = 177 + match Jsonwt.parse rfc_section3_1_token with 178 + | Ok jwt -> 179 + let key_bytes = b64url_decode rfc_hs256_key_b64 in 180 + let key = Jsonwt.Jwk.symmetric key_bytes in 181 + begin match Jsonwt.verify ~key jwt with 182 + | Ok () -> () 183 + | Error e -> Alcotest.fail (Printf.sprintf "Verification failed: %s" (Jsonwt.error_to_string e)) 184 + end 185 + | Error e -> 186 + Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e)) 187 + 188 + let test_rfc_hs256_jwt_verify_wrong_key () = 189 + match Jsonwt.parse rfc_section3_1_token with 190 + | Ok jwt -> 191 + let wrong_key = Jsonwt.Jwk.symmetric "wrong-key-material-that-is-long-enough" in 192 + begin match Jsonwt.verify ~key:wrong_key jwt with 193 + | Error Jsonwt.Signature_mismatch -> () 194 + | Error e -> Alcotest.fail (Printf.sprintf "Expected Signature_mismatch, got: %s" (Jsonwt.error_to_string e)) 195 + | Ok () -> Alcotest.fail "Verification should fail with wrong key" 196 + end 197 + | Error e -> 198 + Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e)) 199 + 200 + (* ============= Claims Validation Tests ============= *) 201 + 202 + let test_validate_expired_token () = 203 + let exp = Ptime.of_float_s 1300819380. |> Option.get in (* RFC example exp *) 204 + let now = Ptime.of_float_s 1400000000. |> Option.get in (* After exp *) 205 + let claims = 206 + Jsonwt.Claims.empty 207 + |> Jsonwt.Claims.set_exp exp 208 + |> Jsonwt.Claims.build 209 + in 210 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 211 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 212 + match Jsonwt.validate ~now jwt with 213 + | Error Jsonwt.Token_expired -> () 214 + | Error e -> Alcotest.fail (Printf.sprintf "Expected Token_expired, got: %s" (Jsonwt.error_to_string e)) 215 + | Ok () -> Alcotest.fail "Expected Token_expired error" 216 + 217 + let test_validate_not_yet_valid_token () = 218 + let nbf = Ptime.of_float_s 1500000000. |> Option.get in 219 + let now = Ptime.of_float_s 1400000000. |> Option.get in (* Before nbf *) 220 + let claims = 221 + Jsonwt.Claims.empty 222 + |> Jsonwt.Claims.set_nbf nbf 223 + |> Jsonwt.Claims.build 224 + in 225 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 226 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 227 + match Jsonwt.validate ~now jwt with 228 + | Error Jsonwt.Token_not_yet_valid -> () 229 + | Error e -> Alcotest.fail (Printf.sprintf "Expected Token_not_yet_valid, got: %s" (Jsonwt.error_to_string e)) 230 + | Ok () -> Alcotest.fail "Expected Token_not_yet_valid error" 231 + 232 + let test_validate_with_leeway () = 233 + let exp = Ptime.of_float_s 1300819380. |> Option.get in 234 + let now = Ptime.of_float_s 1300819390. |> Option.get in (* 10 seconds after exp *) 235 + let leeway = Ptime.Span.of_int_s 60 in (* 60 second leeway *) 236 + let claims = 237 + Jsonwt.Claims.empty 238 + |> Jsonwt.Claims.set_exp exp 239 + |> Jsonwt.Claims.build 240 + in 241 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 242 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 243 + match Jsonwt.validate ~now ~leeway jwt with 244 + | Ok () -> () 245 + | Error e -> Alcotest.fail (Printf.sprintf "Expected validation to pass with leeway, got: %s" (Jsonwt.error_to_string e)) 246 + 247 + let test_validate_issuer_match () = 248 + let now = Ptime.of_float_s 1400000000. |> Option.get in 249 + let claims = 250 + Jsonwt.Claims.empty 251 + |> Jsonwt.Claims.set_iss "expected-issuer" 252 + |> Jsonwt.Claims.build 253 + in 254 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 255 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 256 + match Jsonwt.validate ~now ~iss:"expected-issuer" jwt with 257 + | Ok () -> () 258 + | Error e -> Alcotest.fail (Printf.sprintf "Expected validation to pass, got: %s" (Jsonwt.error_to_string e)) 259 + 260 + let test_validate_issuer_mismatch () = 261 + let now = Ptime.of_float_s 1400000000. |> Option.get in 262 + let claims = 263 + Jsonwt.Claims.empty 264 + |> Jsonwt.Claims.set_iss "actual-issuer" 265 + |> Jsonwt.Claims.build 266 + in 267 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 268 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 269 + match Jsonwt.validate ~now ~iss:"expected-issuer" jwt with 270 + | Error Jsonwt.Invalid_issuer -> () 271 + | Error e -> Alcotest.fail (Printf.sprintf "Expected Invalid_issuer, got: %s" (Jsonwt.error_to_string e)) 272 + | Ok () -> Alcotest.fail "Expected Invalid_issuer error" 273 + 274 + let test_validate_audience_match () = 275 + let now = Ptime.of_float_s 1400000000. |> Option.get in 276 + let claims = 277 + Jsonwt.Claims.empty 278 + |> Jsonwt.Claims.set_aud [ "app1"; "app2"; "my-app" ] 279 + |> Jsonwt.Claims.build 280 + in 281 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 282 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 283 + match Jsonwt.validate ~now ~aud:"my-app" jwt with 284 + | Ok () -> () 285 + | Error e -> Alcotest.fail (Printf.sprintf "Expected validation to pass, got: %s" (Jsonwt.error_to_string e)) 286 + 287 + let test_validate_audience_mismatch () = 288 + let now = Ptime.of_float_s 1400000000. |> Option.get in 289 + let claims = 290 + Jsonwt.Claims.empty 291 + |> Jsonwt.Claims.set_aud [ "app1"; "app2" ] 292 + |> Jsonwt.Claims.build 293 + in 294 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 295 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 296 + match Jsonwt.validate ~now ~aud:"my-app" jwt with 297 + | Error Jsonwt.Invalid_audience -> () 298 + | Error e -> Alcotest.fail (Printf.sprintf "Expected Invalid_audience, got: %s" (Jsonwt.error_to_string e)) 299 + | Ok () -> Alcotest.fail "Expected Invalid_audience error" 300 + 301 + (* ============= Algorithm Restriction Tests ============= *) 302 + 303 + let test_algorithm_not_allowed () = 304 + match Jsonwt.parse rfc_section3_1_token with 305 + | Ok jwt -> 306 + let key_bytes = b64url_decode rfc_hs256_key_b64 in 307 + let key = Jsonwt.Jwk.symmetric key_bytes in 308 + (* Only allow HS384 and HS512, not HS256 *) 309 + let allowed_algs = [ Jsonwt.Algorithm.HS384; Jsonwt.Algorithm.HS512 ] in 310 + begin match Jsonwt.verify ~key ~allowed_algs jwt with 311 + | Error (Jsonwt.Algorithm_not_allowed "HS256") -> () 312 + | Error e -> Alcotest.fail (Printf.sprintf "Expected Algorithm_not_allowed, got: %s" (Jsonwt.error_to_string e)) 313 + | Ok () -> Alcotest.fail "Verification should fail when algorithm is not allowed" 314 + end 315 + | Error e -> 316 + Alcotest.fail (Printf.sprintf "Parse failed: %s" (Jsonwt.error_to_string e)) 317 + 318 + (* ============= Helper Function Tests ============= *) 319 + 320 + let test_is_expired () = 321 + let exp = Ptime.of_float_s 1300819380. |> Option.get in 322 + let claims = 323 + Jsonwt.Claims.empty 324 + |> Jsonwt.Claims.set_exp exp 325 + |> Jsonwt.Claims.build 326 + in 327 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 328 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 329 + let now_before = Ptime.of_float_s 1300819370. |> Option.get in 330 + let now_after = Ptime.of_float_s 1300819390. |> Option.get in 331 + Alcotest.(check bool) "not expired before" false (Jsonwt.is_expired ~now:now_before jwt); 332 + Alcotest.(check bool) "expired after" true (Jsonwt.is_expired ~now:now_after jwt) 333 + 334 + let test_time_to_expiry () = 335 + let exp = Ptime.of_float_s 1300819380. |> Option.get in 336 + let claims = 337 + Jsonwt.Claims.empty 338 + |> Jsonwt.Claims.set_exp exp 339 + |> Jsonwt.Claims.build 340 + in 341 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 342 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 343 + let now = Ptime.of_float_s 1300819370. |> Option.get in 344 + match Jsonwt.time_to_expiry ~now jwt with 345 + | Some span -> 346 + let seconds = Ptime.Span.to_float_s span |> int_of_float in 347 + Alcotest.(check int) "time to expiry" 10 seconds 348 + | None -> 349 + Alcotest.fail "Expected Some time to expiry" 350 + 351 + let test_time_to_expiry_already_expired () = 352 + let exp = Ptime.of_float_s 1300819380. |> Option.get in 353 + let claims = 354 + Jsonwt.Claims.empty 355 + |> Jsonwt.Claims.set_exp exp 356 + |> Jsonwt.Claims.build 357 + in 358 + let header = Jsonwt.Header.make Jsonwt.Algorithm.None in 359 + let jwt = { Jsonwt.header; claims; signature = ""; raw = "" } in 360 + let now = Ptime.of_float_s 1300819390. |> Option.get in 361 + match Jsonwt.time_to_expiry ~now jwt with 362 + | None -> () 363 + | Some _ -> Alcotest.fail "Expected None for expired token" 364 + 365 + (* ============= Error Type Tests ============= *) 366 + 367 + let test_error_to_string () = 368 + let errors = [ 369 + (Jsonwt.Invalid_json "test", "Invalid JSON: test"); 370 + (Jsonwt.Invalid_base64url "test", "Invalid base64url: test"); 371 + (Jsonwt.Invalid_structure "test", "Invalid structure: test"); 372 + (Jsonwt.Token_expired, "Token expired"); 373 + (Jsonwt.Token_not_yet_valid, "Token not yet valid"); 374 + (Jsonwt.Signature_mismatch, "Signature mismatch"); 375 + ] in 376 + List.iter (fun (err, expected) -> 377 + let actual = Jsonwt.error_to_string err in 378 + Alcotest.(check string) "error string" expected actual 379 + ) errors 380 + 381 + (* ============= JWK Tests ============= *) 382 + 383 + let test_jwk_symmetric () = 384 + (* Just verify that creating a symmetric key doesn't crash *) 385 + let _key = Jsonwt.Jwk.symmetric "my-secret-key" in 386 + () 387 + 388 + (* ============= Test Runner ============= *) 389 + 390 + let () = 391 + Alcotest.run "Jsonwt" [ 392 + "Algorithm", [ 393 + Alcotest.test_case "roundtrip" `Quick test_algorithm_roundtrip; 394 + Alcotest.test_case "unknown" `Quick test_algorithm_unknown; 395 + ]; 396 + "Header", [ 397 + Alcotest.test_case "create" `Quick test_header_create; 398 + Alcotest.test_case "with_kid" `Quick test_header_with_kid; 399 + ]; 400 + "Claims", [ 401 + Alcotest.test_case "builder" `Quick test_claims_builder; 402 + Alcotest.test_case "timestamps" `Quick test_claims_with_timestamps; 403 + Alcotest.test_case "audience_single" `Quick test_claims_audience_single; 404 + Alcotest.test_case "audience_multiple" `Quick test_claims_audience_multiple; 405 + ]; 406 + "Parse", [ 407 + Alcotest.test_case "invalid" `Quick test_parse_invalid; 408 + Alcotest.test_case "malformed" `Quick test_parse_malformed; 409 + Alcotest.test_case "invalid_base64" `Quick test_parse_invalid_base64; 410 + ]; 411 + "RFC 7519 Section 6.1 - Unsecured JWT", [ 412 + Alcotest.test_case "parse" `Quick test_rfc_unsecured_jwt_parse; 413 + Alcotest.test_case "rejected_by_default" `Quick test_rfc_unsecured_jwt_verify_rejected_by_default; 414 + Alcotest.test_case "allowed_with_opt_in" `Quick test_rfc_unsecured_jwt_verify_allowed_with_opt_in; 415 + ]; 416 + "RFC 7519 Section 3.1 - HS256 JWT", [ 417 + Alcotest.test_case "parse" `Quick test_rfc_hs256_jwt_parse; 418 + Alcotest.test_case "verify" `Quick test_rfc_hs256_jwt_verify; 419 + Alcotest.test_case "verify_wrong_key" `Quick test_rfc_hs256_jwt_verify_wrong_key; 420 + ]; 421 + "Claims Validation", [ 422 + Alcotest.test_case "expired" `Quick test_validate_expired_token; 423 + Alcotest.test_case "not_yet_valid" `Quick test_validate_not_yet_valid_token; 424 + Alcotest.test_case "with_leeway" `Quick test_validate_with_leeway; 425 + Alcotest.test_case "issuer_match" `Quick test_validate_issuer_match; 426 + Alcotest.test_case "issuer_mismatch" `Quick test_validate_issuer_mismatch; 427 + Alcotest.test_case "audience_match" `Quick test_validate_audience_match; 428 + Alcotest.test_case "audience_mismatch" `Quick test_validate_audience_mismatch; 429 + ]; 430 + "Algorithm Restrictions", [ 431 + Alcotest.test_case "not_allowed" `Quick test_algorithm_not_allowed; 432 + ]; 433 + "Helper Functions", [ 434 + Alcotest.test_case "is_expired" `Quick test_is_expired; 435 + Alcotest.test_case "time_to_expiry" `Quick test_time_to_expiry; 436 + Alcotest.test_case "time_to_expiry_expired" `Quick test_time_to_expiry_already_expired; 437 + ]; 438 + "Error Types", [ 439 + Alcotest.test_case "to_string" `Quick test_error_to_string; 440 + ]; 441 + "JWK", [ 442 + Alcotest.test_case "symmetric" `Quick test_jwk_symmetric; 443 + ]; 444 + ]