@zitadel/sdk-core 1.0.0-alpha.24 → 1.0.0-alpha.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -3
- package/dist/index.d.mts +23 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +40 -0
- package/dist/index.mjs.map +1 -0
- package/dist/{jwt.d.ts → jwt.d.mts} +97 -83
- package/dist/jwt.d.mts.map +1 -0
- package/dist/jwt.mjs +465 -0
- package/dist/jwt.mjs.map +1 -0
- package/dist/middleware.d.mts +556 -0
- package/dist/middleware.d.mts.map +1 -0
- package/dist/middleware.mjs +764 -0
- package/dist/middleware.mjs.map +1 -0
- package/dist/types.d.mts +301 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +78 -0
- package/dist/types.mjs.map +1 -0
- package/package.json +23 -27
- package/dist/index.d.ts +0 -22
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -42
- package/dist/jwt.d.ts.map +0 -1
- package/dist/jwt.js +0 -415
- package/dist/middleware.d.ts +0 -202
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js +0 -131
- package/dist/tsconfig.lib.tsbuildinfo +0 -1
- package/dist/types.d.ts +0 -215
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -26
package/dist/jwt.mjs
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { joinURL, withoutTrailingSlash } from "ufo";
|
|
2
|
+
//#region src/jwt.ts
|
|
3
|
+
/**
|
|
4
|
+
* @file lib/jwt.ts
|
|
5
|
+
*
|
|
6
|
+
* # JWT Verification
|
|
7
|
+
*
|
|
8
|
+
* Stateless JWT verification via JWKS, with in-process key caching.
|
|
9
|
+
*
|
|
10
|
+
* ## Anatomy of a JWT
|
|
11
|
+
*
|
|
12
|
+
* A compact JWT (JWS compact serialisation) is three Base64URL-encoded parts
|
|
13
|
+
* joined by dots:
|
|
14
|
+
*
|
|
15
|
+
* ```
|
|
16
|
+
* <header>.<payload>.<signature>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* - **Header** — metadata about the token: signing algorithm, key ID, token type.
|
|
20
|
+
* - **Payload** — the claims: user identity, expiry, issuer, audience, etc.
|
|
21
|
+
* - **Signature** — `SIGN(privateKey, "<header>.<payload>")`. Any party with the
|
|
22
|
+
* matching public key can verify it without contacting the auth server.
|
|
23
|
+
*
|
|
24
|
+
* A valid signature proves two things:
|
|
25
|
+
*
|
|
26
|
+
* 1. **Authenticity** — the token was produced by the holder of the private key.
|
|
27
|
+
* 2. **Integrity** — neither the header nor the payload has been altered since signing.
|
|
28
|
+
*
|
|
29
|
+
* Signature verification alone is not sufficient. The claims inside the payload
|
|
30
|
+
* must also be checked (expiry, issuer, audience, …). Both steps are performed
|
|
31
|
+
* by {@link verifyJwt}.
|
|
32
|
+
*
|
|
33
|
+
* ---
|
|
34
|
+
*
|
|
35
|
+
* ## JWT Header fields
|
|
36
|
+
*
|
|
37
|
+
* The header is a JSON object Base64URL-encoded into the first segment.
|
|
38
|
+
*
|
|
39
|
+
* | Field | Purpose | Verified |
|
|
40
|
+
* |-------|---------|---------|
|
|
41
|
+
* | `alg` | Signing algorithm (`RS256`, `ES256`, …) | Checked against {@link VerifyJwtOptions.allowedAlgorithms} before JWKS is fetched |
|
|
42
|
+
* | `kid` | Key ID — selects the matching public key from the JWKS | Used for key lookup; falls back to the first JWKS key when absent |
|
|
43
|
+
* | `typ` | Token type (`JWT`, `at+JWT`, …) | Checked against {@link VerifyJwtOptions.allowedTokenTypes} (case-insensitive) |
|
|
44
|
+
* | ~~`enc`~~ | ~~Encryption algorithm — present in JWE (encrypted) tokens only~~ | ~~Not applicable — we verify JWS (signed) tokens, not JWE (encrypted)~~ |
|
|
45
|
+
* | ~~`cty`~~ | ~~Content type — describes the secured content media type~~ | ~~Not applicable — not set by standard OIDC/OAuth auth servers~~ |
|
|
46
|
+
*
|
|
47
|
+
* ---
|
|
48
|
+
*
|
|
49
|
+
* ## JWT Payload claims
|
|
50
|
+
*
|
|
51
|
+
* The payload is a JSON object Base64URL-encoded into the second segment.
|
|
52
|
+
*
|
|
53
|
+
* ### Validated claims
|
|
54
|
+
*
|
|
55
|
+
* | Claim | Purpose | How |
|
|
56
|
+
* |-------|---------|-----|
|
|
57
|
+
* | `iss` | Issuer — the URL of the auth server that issued the token | Must be present and must equal {@link VerifyJwtOptions.issuerUrl} |
|
|
58
|
+
* | `exp` | Expiration time — Unix timestamp after which the token is invalid | Must be present and in the future, allowing {@link VerifyJwtOptions.clockSkewMs} tolerance |
|
|
59
|
+
* | `nbf` | Not before — Unix timestamp before which the token must not be accepted | Must be in the past, allowing {@link VerifyJwtOptions.clockSkewMs} tolerance |
|
|
60
|
+
* | `iat` | Issued at — Unix timestamp when the token was created | Must not be in the future beyond {@link VerifyJwtOptions.clockSkewMs} |
|
|
61
|
+
* | `aud` | Audience — the intended recipient(s) of the token | Checked against {@link VerifyJwtOptions.audience} when that option is provided |
|
|
62
|
+
*
|
|
63
|
+
* ### Read-only claims (extracted into the session, not further validated)
|
|
64
|
+
*
|
|
65
|
+
* | Claim | Purpose |
|
|
66
|
+
* |-------|---------|
|
|
67
|
+
* | `sub` | Subject — the user's unique identifier |
|
|
68
|
+
* | `email` | User's email address |
|
|
69
|
+
* | `name` | User's display name |
|
|
70
|
+
*
|
|
71
|
+
* ### Not validated
|
|
72
|
+
*
|
|
73
|
+
* | Claim | Purpose | Why skipped |
|
|
74
|
+
* |-------|---------|-------------|
|
|
75
|
+
* | ~~`jti`~~ | ~~JWT ID — a unique identifier for this specific token instance~~ | ~~Revocation requires a server-side blocklist; not feasible in stateless edge middleware~~ |
|
|
76
|
+
* | ~~`azp`~~ | ~~Authorized party — the OAuth 2.0 client that requested the token~~ | ~~The proxy architecture means the browser never contacts the auth server directly, making cross-origin token replay a non-threat~~ |
|
|
77
|
+
* | ~~`nonce`~~ | ~~OIDC nonce — binds a token to a specific authorisation request~~ | ~~Not applicable in our password/cookie flow (no OIDC redirect round-trip)~~ |
|
|
78
|
+
* | ~~`acr`~~ | ~~Authentication class reference — describes the strength of the authentication~~ | ~~Not validated (future: require MFA for sensitive routes)~~ |
|
|
79
|
+
* | ~~`amr`~~ | ~~Authentication methods reference — list of methods used to authenticate~~ | ~~Not validated~~ |
|
|
80
|
+
* | ~~`scope`~~ | ~~OAuth 2.0 scopes granted to the token~~ | ~~Not validated (future: per-route scope enforcement)~~ |
|
|
81
|
+
*
|
|
82
|
+
* ---
|
|
83
|
+
*
|
|
84
|
+
* ## JWKS caching
|
|
85
|
+
*
|
|
86
|
+
* Each issuer's whole JWKS document is cached in-process, keyed by its JWKS URI,
|
|
87
|
+
* for {@link JWKS_TTL_MS} milliseconds (default 5 minutes); keys are imported
|
|
88
|
+
* once and reused. Keying by the URI keeps two issuers that publish the same
|
|
89
|
+
* `kid` apart.
|
|
90
|
+
*
|
|
91
|
+
* The document is fetched from `{issuerUrl}/auth/keys` on first use and after
|
|
92
|
+
* the TTL. A token naming a `kid` the cached document lacks refreshes it — how
|
|
93
|
+
* a rotated key is picked up — but no sooner than {@link JWKS_MIN_REFRESH_MS}
|
|
94
|
+
* after the last fetch. The `kid` header is attacker-controlled, so a per-`kid`
|
|
95
|
+
* cache would let any request with a made-up `kid` cost a JWKS fetch.
|
|
96
|
+
*
|
|
97
|
+
* > **Note (serverless):** the cache is module-level. In serverless environments
|
|
98
|
+
* > each cold start gets a fresh cache; warm instances share it across requests.
|
|
99
|
+
*
|
|
100
|
+
* ---
|
|
101
|
+
*
|
|
102
|
+
* ## Runtime note
|
|
103
|
+
*
|
|
104
|
+
* This file is the **shared implementation** used by both `sdk-next` (Edge
|
|
105
|
+
* runtime) and `sdk-nuxt` (Node.js/Nitro). `base64UrlDecode` uses `atob`,
|
|
106
|
+
* which is available in both environments (Edge natively; Node.js since v16).
|
|
107
|
+
* The previous per-SDK copies (one using `atob`, one using `Buffer`) have
|
|
108
|
+
* been consolidated here.
|
|
109
|
+
*/
|
|
110
|
+
/** How long a cached JWKS document is considered fresh before re-fetching. */
|
|
111
|
+
const JWKS_TTL_MS = 3e5;
|
|
112
|
+
/**
|
|
113
|
+
* The least time between two JWKS fetches for one issuer. A token naming a
|
|
114
|
+
* `kid` the cached document lacks triggers a refresh — that is how a rotated
|
|
115
|
+
* key is picked up — but at most this often, so a stream of tokens with random
|
|
116
|
+
* `kid` values cannot turn every request into a JWKS fetch.
|
|
117
|
+
*/
|
|
118
|
+
const JWKS_MIN_REFRESH_MS = 3e4;
|
|
119
|
+
/**
|
|
120
|
+
* In-memory JWKS cache: one document per JWKS URI. Keying by the full URI keeps
|
|
121
|
+
* two issuers that publish the same `kid` apart. It holds one entry per issuer
|
|
122
|
+
* the process verifies against, each with the keys that issuer published, so it
|
|
123
|
+
* cannot be grown by the `kid` values tokens claim.
|
|
124
|
+
*/
|
|
125
|
+
const jwksCache = /* @__PURE__ */ new Map();
|
|
126
|
+
/** In-flight JWKS fetches by URI, so concurrent misses share one request. */
|
|
127
|
+
const jwksInFlight = /* @__PURE__ */ new Map();
|
|
128
|
+
/**
|
|
129
|
+
* When each URI was last fetched, successfully or not. The refresh throttle
|
|
130
|
+
* counts from here, not from the last document, so a failing endpoint is not
|
|
131
|
+
* retried on every request.
|
|
132
|
+
*/
|
|
133
|
+
const jwksAttemptedAt = /* @__PURE__ */ new Map();
|
|
134
|
+
/**
|
|
135
|
+
* Bumped by {@link clearJwksCache}. A fetch that started before a clear must not
|
|
136
|
+
* write its answer into the cache or the in-flight table afterwards.
|
|
137
|
+
*/
|
|
138
|
+
let jwksGeneration = 0;
|
|
139
|
+
/**
|
|
140
|
+
* Forgets every cached JWKS document, so the next verification fetches the key
|
|
141
|
+
* set anew. Use it to drop a compromised key at once instead of waiting out
|
|
142
|
+
* {@link JWKS_TTL_MS}, and in tests that publish different keys per case.
|
|
143
|
+
*/
|
|
144
|
+
function clearJwksCache() {
|
|
145
|
+
jwksGeneration++;
|
|
146
|
+
jwksCache.clear();
|
|
147
|
+
jwksInFlight.clear();
|
|
148
|
+
jwksAttemptedAt.clear();
|
|
149
|
+
}
|
|
150
|
+
const DECODER = new TextDecoder();
|
|
151
|
+
const ENCODER = new TextEncoder();
|
|
152
|
+
/**
|
|
153
|
+
* Decodes a Base64URL-encoded string into a `Uint8Array`.
|
|
154
|
+
*
|
|
155
|
+
* The Edge runtime has no `Buffer`, so this uses `atob` after translating the
|
|
156
|
+
* Base64URL alphabet (`-` → `+`, `_` → `/`) and padding to a multiple of four.
|
|
157
|
+
*
|
|
158
|
+
* @param input - A Base64URL-encoded string (padding is optional).
|
|
159
|
+
* @returns The decoded bytes.
|
|
160
|
+
*/
|
|
161
|
+
function base64UrlDecode(input) {
|
|
162
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
163
|
+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
164
|
+
const binary = atob(padded);
|
|
165
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Splits a compact JWT string into its three raw Base64URL segments without
|
|
169
|
+
* decoding or verifying any of them.
|
|
170
|
+
*
|
|
171
|
+
* Tokens with more than three segments (e.g. nested JWTs) are accepted; only
|
|
172
|
+
* the first three parts are returned. Tokens with fewer than three segments are
|
|
173
|
+
* structurally invalid compact JWTs and cannot be verified.
|
|
174
|
+
*
|
|
175
|
+
* @param token - The compact JWT string.
|
|
176
|
+
* @returns A `[header, payload, signature]` tuple, or `null` when the token
|
|
177
|
+
* has fewer than three dot-separated segments.
|
|
178
|
+
*/
|
|
179
|
+
function splitToken(token) {
|
|
180
|
+
const [h, p, s] = token.split(".");
|
|
181
|
+
if (!h || !p || !s) return null;
|
|
182
|
+
return [
|
|
183
|
+
h,
|
|
184
|
+
p,
|
|
185
|
+
s
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Decodes a compact-serialised JWT into its header and payload **without**
|
|
190
|
+
* verifying the signature.
|
|
191
|
+
*
|
|
192
|
+
* **Do not trust the result of this function alone.** Use it only as the first
|
|
193
|
+
* step of a full verification pipeline — the signature check in {@link verifyJwt}
|
|
194
|
+
* must follow before any claim can be trusted.
|
|
195
|
+
*
|
|
196
|
+
* @param token - A compact JWT string (`header.payload.signature`).
|
|
197
|
+
* @returns The decoded {@link DecodedJwt}.
|
|
198
|
+
* @throws {TypeError} When `token` has fewer than three dot-separated segments.
|
|
199
|
+
*/
|
|
200
|
+
function decodeJwt(token) {
|
|
201
|
+
const segments = splitToken(token);
|
|
202
|
+
if (!segments) throw new TypeError(`Invalid JWT: expected at least three dot-separated segments, received ${token.split(".").length}.`);
|
|
203
|
+
const [h, p] = segments;
|
|
204
|
+
return {
|
|
205
|
+
header: JSON.parse(DECODER.decode(base64UrlDecode(h))),
|
|
206
|
+
payload: JSON.parse(DECODER.decode(base64UrlDecode(p)))
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Returns `true` when `token` looks structurally like a signed JWT (JWS) —
|
|
211
|
+
* NOT an encrypted token (JWE).
|
|
212
|
+
*
|
|
213
|
+
* JWS compact: 3 dot-separated segments; header has `alg` but NOT `enc`.
|
|
214
|
+
* JWE compact: 5 dot-separated segments; header has BOTH `alg` and `enc`.
|
|
215
|
+
*
|
|
216
|
+
* The Zitadel backend issues JWE tokens (AES-256-GCM via go-jose
|
|
217
|
+
* `A256GCMKW/A256GCM`), whose compact form is
|
|
218
|
+
* `header.encrypted_key.iv.ciphertext.tag`. The header decodes to JSON with
|
|
219
|
+
* `"alg":"A256GCMKW","enc":"A256GCM"`. Checking for the absence of `enc`
|
|
220
|
+
* correctly identifies signed JWTs without false-positives on JWE tokens.
|
|
221
|
+
*
|
|
222
|
+
* This is a structural check only — not a security check. Use it to route a
|
|
223
|
+
* token to the correct validation path ({@link verifyJwt} for JWS, a backend
|
|
224
|
+
* session lookup for everything else), never to trust one.
|
|
225
|
+
*/
|
|
226
|
+
function isJwtShaped(token) {
|
|
227
|
+
const parts = token.split(".");
|
|
228
|
+
if (parts.length < 3 || !parts[0]) return false;
|
|
229
|
+
try {
|
|
230
|
+
const header = JSON.parse(DECODER.decode(base64UrlDecode(parts[0])));
|
|
231
|
+
return typeof header?.alg === "string" && !("enc" in header);
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Web Crypto parameters for importing a public key from JWKS, keyed by JWT
|
|
238
|
+
* `alg` header value. The keys of this object are the only supported algorithms;
|
|
239
|
+
* anything not present throws {@link TypeError}.
|
|
240
|
+
*/
|
|
241
|
+
const IMPORT_PARAMS = {
|
|
242
|
+
RS256: {
|
|
243
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
244
|
+
hash: "SHA-256"
|
|
245
|
+
},
|
|
246
|
+
RS384: {
|
|
247
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
248
|
+
hash: "SHA-384"
|
|
249
|
+
},
|
|
250
|
+
RS512: {
|
|
251
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
252
|
+
hash: "SHA-512"
|
|
253
|
+
},
|
|
254
|
+
ES256: {
|
|
255
|
+
name: "ECDSA",
|
|
256
|
+
namedCurve: "P-256"
|
|
257
|
+
},
|
|
258
|
+
ES384: {
|
|
259
|
+
name: "ECDSA",
|
|
260
|
+
namedCurve: "P-384"
|
|
261
|
+
},
|
|
262
|
+
ES512: {
|
|
263
|
+
name: "ECDSA",
|
|
264
|
+
namedCurve: "P-521"
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* Web Crypto parameters for verifying a JWT signature, keyed by JWT `alg`
|
|
269
|
+
* header value.
|
|
270
|
+
*/
|
|
271
|
+
const VERIFY_PARAMS = {
|
|
272
|
+
RS256: { name: "RSASSA-PKCS1-v1_5" },
|
|
273
|
+
RS384: { name: "RSASSA-PKCS1-v1_5" },
|
|
274
|
+
RS512: { name: "RSASSA-PKCS1-v1_5" },
|
|
275
|
+
ES256: {
|
|
276
|
+
name: "ECDSA",
|
|
277
|
+
hash: "SHA-256"
|
|
278
|
+
},
|
|
279
|
+
ES384: {
|
|
280
|
+
name: "ECDSA",
|
|
281
|
+
hash: "SHA-384"
|
|
282
|
+
},
|
|
283
|
+
ES512: {
|
|
284
|
+
name: "ECDSA",
|
|
285
|
+
hash: "SHA-512"
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
function assertSupportedAlgorithm(alg) {
|
|
289
|
+
if (!Object.hasOwn(IMPORT_PARAMS, alg)) throw new TypeError(`Unsupported JWT algorithm: "${alg}". Supported algorithms are ${Object.keys(IMPORT_PARAMS).join(", ")}.`);
|
|
290
|
+
}
|
|
291
|
+
/** The JWT `alg` an EC key's curve signs with (RFC 7518 §3.4). */
|
|
292
|
+
const CURVE_ALGORITHMS = {
|
|
293
|
+
"P-256": "ES256",
|
|
294
|
+
"P-384": "ES384",
|
|
295
|
+
"P-521": "ES512"
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* The Web Crypto import parameters for verifying a token signed with `alg`
|
|
299
|
+
* under `jwk`, or `null` when the key cannot have produced it.
|
|
300
|
+
*
|
|
301
|
+
* RFC 7517 §4.4 makes a JWK's `alg` optional, so the key type decides: an EC
|
|
302
|
+
* key's curve fixes its algorithm, and an RSA key fits any `RS*` algorithm.
|
|
303
|
+
* When the JWK names an `alg`, the token must use that one. A mismatch is a
|
|
304
|
+
* token that cannot verify against this key, not an error worth logging.
|
|
305
|
+
*/
|
|
306
|
+
function importParams(jwk, alg) {
|
|
307
|
+
if (!Object.hasOwn(IMPORT_PARAMS, alg)) return null;
|
|
308
|
+
if (jwk.alg !== void 0 && jwk.alg !== alg) return null;
|
|
309
|
+
return (jwk.kty === "RSA" ? alg.startsWith("RS") : jwk.kty === "EC" && CURVE_ALGORITHMS[jwk.crv ?? ""] === alg) ? IMPORT_PARAMS[alg] : null;
|
|
310
|
+
}
|
|
311
|
+
function resolveVerifyAlgorithm(alg) {
|
|
312
|
+
assertSupportedAlgorithm(alg);
|
|
313
|
+
return VERIFY_PARAMS[alg];
|
|
314
|
+
}
|
|
315
|
+
/** Resolves to `null` if `promise` has not settled within `timeoutMs`. */
|
|
316
|
+
function withTimeout(promise, timeoutMs) {
|
|
317
|
+
let timer;
|
|
318
|
+
const expired = new Promise((resolve) => {
|
|
319
|
+
timer = setTimeout(() => {
|
|
320
|
+
resolve(null);
|
|
321
|
+
}, timeoutMs);
|
|
322
|
+
});
|
|
323
|
+
return Promise.race([promise, expired]).finally(() => {
|
|
324
|
+
clearTimeout(timer);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Fetches the JWKS document at `jwksUri`, coalescing concurrent fetches, and
|
|
329
|
+
* caches it. Returns `null` when the endpoint fails or answers without a key
|
|
330
|
+
* set, and also, without fetching, when the URI was fetched less than
|
|
331
|
+
* {@link JWKS_MIN_REFRESH_MS} ago: a failed fetch counts, so a failing endpoint
|
|
332
|
+
* is not retried on every request. A failure leaves any previously cached
|
|
333
|
+
* document in place, but {@link resolveJwk} does not use it once it has expired.
|
|
334
|
+
*/
|
|
335
|
+
function refreshJwks(jwksUri, timeoutMs) {
|
|
336
|
+
const pending = jwksInFlight.get(jwksUri);
|
|
337
|
+
if (pending) return withTimeout(pending, timeoutMs);
|
|
338
|
+
const attemptedAt = jwksAttemptedAt.get(jwksUri);
|
|
339
|
+
if (attemptedAt !== void 0 && Date.now() - attemptedAt < 3e4) return Promise.resolve(null);
|
|
340
|
+
const generation = jwksGeneration;
|
|
341
|
+
jwksAttemptedAt.set(jwksUri, Date.now());
|
|
342
|
+
const request = (async () => {
|
|
343
|
+
const res = await fetch(jwksUri, { signal: AbortSignal.timeout(timeoutMs) });
|
|
344
|
+
if (!res.ok) return null;
|
|
345
|
+
const json = await res.json();
|
|
346
|
+
if (!Array.isArray(json.keys)) return null;
|
|
347
|
+
const entry = {
|
|
348
|
+
keys: json.keys,
|
|
349
|
+
fetchedAt: Date.now(),
|
|
350
|
+
imported: /* @__PURE__ */ new Map()
|
|
351
|
+
};
|
|
352
|
+
if (generation === jwksGeneration) jwksCache.set(jwksUri, entry);
|
|
353
|
+
return entry;
|
|
354
|
+
})().finally(() => {
|
|
355
|
+
if (jwksInFlight.get(jwksUri) === request) jwksInFlight.delete(jwksUri);
|
|
356
|
+
});
|
|
357
|
+
jwksInFlight.set(jwksUri, request);
|
|
358
|
+
return request;
|
|
359
|
+
}
|
|
360
|
+
/** The key a token names: the one with its `kid`, or the first without one. */
|
|
361
|
+
function findKey(entry, kid) {
|
|
362
|
+
return kid === void 0 ? entry.keys[0] : entry.keys.find((key) => key.kid === kid);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Resolves the public key for a token from the issuer's JWKS, imported for
|
|
366
|
+
* `alg`.
|
|
367
|
+
*
|
|
368
|
+
* The JWKS document is cached for {@link JWKS_TTL_MS}. A `kid` the cached
|
|
369
|
+
* document lacks refreshes it — a rotated key — but no sooner than
|
|
370
|
+
* {@link JWKS_MIN_REFRESH_MS} after the last fetch, so tokens with made-up
|
|
371
|
+
* `kid` values are answered from the cache. An expired document is never used
|
|
372
|
+
* when its refresh fails. Returns `null` when no key fits: the endpoint failed,
|
|
373
|
+
* no key has that `kid`, or the key cannot verify `alg`.
|
|
374
|
+
*
|
|
375
|
+
* @param jwksUri - Full URL of the JWKS endpoint.
|
|
376
|
+
* @param kid - The `kid` header value from the JWT, or `undefined`.
|
|
377
|
+
* @param alg - The token's (already allowlisted) `alg` header value.
|
|
378
|
+
*/
|
|
379
|
+
async function resolveJwk(jwksUri, kid, alg, timeoutMs) {
|
|
380
|
+
const now = Date.now();
|
|
381
|
+
let entry = jwksCache.get(jwksUri);
|
|
382
|
+
if (!entry || now - entry.fetchedAt >= 3e5) entry = await refreshJwks(jwksUri, timeoutMs) ?? void 0;
|
|
383
|
+
if (!entry) return null;
|
|
384
|
+
let jwk = findKey(entry, kid);
|
|
385
|
+
if (!jwk && now - entry.fetchedAt >= 3e4) {
|
|
386
|
+
const refreshed = await refreshJwks(jwksUri, timeoutMs);
|
|
387
|
+
if (refreshed) {
|
|
388
|
+
entry = refreshed;
|
|
389
|
+
jwk = findKey(entry, kid);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!jwk) return null;
|
|
393
|
+
const params = importParams(jwk, alg);
|
|
394
|
+
if (!params) return null;
|
|
395
|
+
const importedKey = `${kid ?? "__default__"}:${alg}`;
|
|
396
|
+
const cached = entry.imported.get(importedKey);
|
|
397
|
+
if (cached) return cached;
|
|
398
|
+
const cryptoKey = await crypto.subtle.importKey("jwk", jwk, params, false, ["verify"]);
|
|
399
|
+
entry.imported.set(importedKey, cryptoKey);
|
|
400
|
+
return cryptoKey;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Verifies a compact-serialised JWT and returns its validated payload.
|
|
404
|
+
*
|
|
405
|
+
* ## Verification steps (in order)
|
|
406
|
+
*
|
|
407
|
+
* 1. Split the token — returns `null` immediately when fewer than three
|
|
408
|
+
* dot-separated segments are present.
|
|
409
|
+
* 2. Reject if `alg` is not in {@link VerifyJwtOptions.allowedAlgorithms} (when set).
|
|
410
|
+
* 3. Reject if `typ` is not in {@link VerifyJwtOptions.allowedTokenTypes} (when non-empty).
|
|
411
|
+
* 4. Fetch (or retrieve from cache) the matching public key from the JWKS endpoint.
|
|
412
|
+
* 5. Verify the cryptographic signature over `header.payload`.
|
|
413
|
+
* 6. Reject if `iss` is absent or does not equal {@link VerifyJwtOptions.issuerUrl}.
|
|
414
|
+
* 7. Reject if {@link VerifyJwtOptions.audience} is set and `aud` does not contain it.
|
|
415
|
+
* 8. Reject if `exp` is absent or is in the past (minus clock-skew tolerance).
|
|
416
|
+
* 9. Reject if `nbf` is in the future (plus clock-skew tolerance).
|
|
417
|
+
* 10. Reject if `iat` is in the future (plus clock-skew tolerance).
|
|
418
|
+
*
|
|
419
|
+
* Returns `null` on any failure rather than throwing, so callers can treat a
|
|
420
|
+
* bad token identically to a missing one without try/catch at the call site.
|
|
421
|
+
*
|
|
422
|
+
* @param token - The raw compact-serialised JWT string.
|
|
423
|
+
* @param opts - Verification options; see {@link VerifyJwtOptions}.
|
|
424
|
+
* @returns The verified {@link JwtPayload}, or `null` if verification fails.
|
|
425
|
+
*/
|
|
426
|
+
async function verifyJwt(token, opts) {
|
|
427
|
+
try {
|
|
428
|
+
const { issuerUrl, allowedAlgorithms, clockSkewMs, audience, allowedTokenTypes, jwksTimeoutMs = 5e3 } = opts;
|
|
429
|
+
const segments = splitToken(token);
|
|
430
|
+
if (!segments) return null;
|
|
431
|
+
const [rawHeader, rawPayload, rawSig] = segments;
|
|
432
|
+
const header = JSON.parse(DECODER.decode(base64UrlDecode(rawHeader)));
|
|
433
|
+
const payload = JSON.parse(DECODER.decode(base64UrlDecode(rawPayload)));
|
|
434
|
+
const alg = header.alg;
|
|
435
|
+
if (!alg) return null;
|
|
436
|
+
if (alg.toLowerCase() === "none") return null;
|
|
437
|
+
if (allowedAlgorithms?.length && !allowedAlgorithms.includes(alg)) return null;
|
|
438
|
+
if (allowedTokenTypes.length > 0) {
|
|
439
|
+
const typ = header.typ ?? "";
|
|
440
|
+
if (!allowedTokenTypes.some((t) => t.toLowerCase() === typ.toLowerCase())) return null;
|
|
441
|
+
}
|
|
442
|
+
const kid = header.kid;
|
|
443
|
+
const cryptoKey = await resolveJwk(joinURL(issuerUrl, "/auth/keys"), kid, alg, jwksTimeoutMs);
|
|
444
|
+
if (!cryptoKey) return null;
|
|
445
|
+
if (!await crypto.subtle.verify(resolveVerifyAlgorithm(alg), cryptoKey, base64UrlDecode(rawSig), ENCODER.encode(`${rawHeader}.${rawPayload}`))) return null;
|
|
446
|
+
if (payload.iss !== withoutTrailingSlash(issuerUrl)) return null;
|
|
447
|
+
if (audience !== void 0) {
|
|
448
|
+
const audList = payload.aud !== void 0 ? [payload.aud].flat() : [];
|
|
449
|
+
if (![audience].flat().some((a) => audList.includes(a))) return null;
|
|
450
|
+
}
|
|
451
|
+
const now = Date.now();
|
|
452
|
+
if (payload.exp === void 0 || payload.exp * 1e3 < now - clockSkewMs) return null;
|
|
453
|
+
if (payload.nbf !== void 0 && payload.nbf * 1e3 > now + clockSkewMs) return null;
|
|
454
|
+
if (payload.iat !== void 0 && payload.iat * 1e3 > now + clockSkewMs) return null;
|
|
455
|
+
return payload;
|
|
456
|
+
} catch (err) {
|
|
457
|
+
if (err instanceof SyntaxError) return null;
|
|
458
|
+
console.error("[nextgen] verifyJwt unexpected error:", err);
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
463
|
+
export { JWKS_MIN_REFRESH_MS, JWKS_TTL_MS, base64UrlDecode, clearJwksCache, decodeJwt, isJwtShaped, verifyJwt };
|
|
464
|
+
|
|
465
|
+
//# sourceMappingURL=jwt.mjs.map
|
package/dist/jwt.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.mjs","names":[],"sources":["../src/jwt.ts"],"sourcesContent":["/**\n * @file lib/jwt.ts\n *\n * # JWT Verification\n *\n * Stateless JWT verification via JWKS, with in-process key caching.\n *\n * ## Anatomy of a JWT\n *\n * A compact JWT (JWS compact serialisation) is three Base64URL-encoded parts\n * joined by dots:\n *\n * ```\n * <header>.<payload>.<signature>\n * ```\n *\n * - **Header** — metadata about the token: signing algorithm, key ID, token type.\n * - **Payload** — the claims: user identity, expiry, issuer, audience, etc.\n * - **Signature** — `SIGN(privateKey, \"<header>.<payload>\")`. Any party with the\n * matching public key can verify it without contacting the auth server.\n *\n * A valid signature proves two things:\n *\n * 1. **Authenticity** — the token was produced by the holder of the private key.\n * 2. **Integrity** — neither the header nor the payload has been altered since signing.\n *\n * Signature verification alone is not sufficient. The claims inside the payload\n * must also be checked (expiry, issuer, audience, …). Both steps are performed\n * by {@link verifyJwt}.\n *\n * ---\n *\n * ## JWT Header fields\n *\n * The header is a JSON object Base64URL-encoded into the first segment.\n *\n * | Field | Purpose | Verified |\n * |-------|---------|---------|\n * | `alg` | Signing algorithm (`RS256`, `ES256`, …) | Checked against {@link VerifyJwtOptions.allowedAlgorithms} before JWKS is fetched |\n * | `kid` | Key ID — selects the matching public key from the JWKS | Used for key lookup; falls back to the first JWKS key when absent |\n * | `typ` | Token type (`JWT`, `at+JWT`, …) | Checked against {@link VerifyJwtOptions.allowedTokenTypes} (case-insensitive) |\n * | ~~`enc`~~ | ~~Encryption algorithm — present in JWE (encrypted) tokens only~~ | ~~Not applicable — we verify JWS (signed) tokens, not JWE (encrypted)~~ |\n * | ~~`cty`~~ | ~~Content type — describes the secured content media type~~ | ~~Not applicable — not set by standard OIDC/OAuth auth servers~~ |\n *\n * ---\n *\n * ## JWT Payload claims\n *\n * The payload is a JSON object Base64URL-encoded into the second segment.\n *\n * ### Validated claims\n *\n * | Claim | Purpose | How |\n * |-------|---------|-----|\n * | `iss` | Issuer — the URL of the auth server that issued the token | Must be present and must equal {@link VerifyJwtOptions.issuerUrl} |\n * | `exp` | Expiration time — Unix timestamp after which the token is invalid | Must be present and in the future, allowing {@link VerifyJwtOptions.clockSkewMs} tolerance |\n * | `nbf` | Not before — Unix timestamp before which the token must not be accepted | Must be in the past, allowing {@link VerifyJwtOptions.clockSkewMs} tolerance |\n * | `iat` | Issued at — Unix timestamp when the token was created | Must not be in the future beyond {@link VerifyJwtOptions.clockSkewMs} |\n * | `aud` | Audience — the intended recipient(s) of the token | Checked against {@link VerifyJwtOptions.audience} when that option is provided |\n *\n * ### Read-only claims (extracted into the session, not further validated)\n *\n * | Claim | Purpose |\n * |-------|---------|\n * | `sub` | Subject — the user's unique identifier |\n * | `email` | User's email address |\n * | `name` | User's display name |\n *\n * ### Not validated\n *\n * | Claim | Purpose | Why skipped |\n * |-------|---------|-------------|\n * | ~~`jti`~~ | ~~JWT ID — a unique identifier for this specific token instance~~ | ~~Revocation requires a server-side blocklist; not feasible in stateless edge middleware~~ |\n * | ~~`azp`~~ | ~~Authorized party — the OAuth 2.0 client that requested the token~~ | ~~The proxy architecture means the browser never contacts the auth server directly, making cross-origin token replay a non-threat~~ |\n * | ~~`nonce`~~ | ~~OIDC nonce — binds a token to a specific authorisation request~~ | ~~Not applicable in our password/cookie flow (no OIDC redirect round-trip)~~ |\n * | ~~`acr`~~ | ~~Authentication class reference — describes the strength of the authentication~~ | ~~Not validated (future: require MFA for sensitive routes)~~ |\n * | ~~`amr`~~ | ~~Authentication methods reference — list of methods used to authenticate~~ | ~~Not validated~~ |\n * | ~~`scope`~~ | ~~OAuth 2.0 scopes granted to the token~~ | ~~Not validated (future: per-route scope enforcement)~~ |\n *\n * ---\n *\n * ## JWKS caching\n *\n * Each issuer's whole JWKS document is cached in-process, keyed by its JWKS URI,\n * for {@link JWKS_TTL_MS} milliseconds (default 5 minutes); keys are imported\n * once and reused. Keying by the URI keeps two issuers that publish the same\n * `kid` apart.\n *\n * The document is fetched from `{issuerUrl}/auth/keys` on first use and after\n * the TTL. A token naming a `kid` the cached document lacks refreshes it — how\n * a rotated key is picked up — but no sooner than {@link JWKS_MIN_REFRESH_MS}\n * after the last fetch. The `kid` header is attacker-controlled, so a per-`kid`\n * cache would let any request with a made-up `kid` cost a JWKS fetch.\n *\n * > **Note (serverless):** the cache is module-level. In serverless environments\n * > each cold start gets a fresh cache; warm instances share it across requests.\n *\n * ---\n *\n * ## Runtime note\n *\n * This file is the **shared implementation** used by both `sdk-next` (Edge\n * runtime) and `sdk-nuxt` (Node.js/Nitro). `base64UrlDecode` uses `atob`,\n * which is available in both environments (Edge natively; Node.js since v16).\n * The previous per-SDK copies (one using `atob`, one using `Buffer`) have\n * been consolidated here.\n */\n\nimport { joinURL, withoutTrailingSlash } from \"ufo\";\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\n/** How long a cached JWKS document is considered fresh before re-fetching. */\nexport const JWKS_TTL_MS = 5 * 60 * 1_000;\n\n/**\n * The least time between two JWKS fetches for one issuer. A token naming a\n * `kid` the cached document lacks triggers a refresh — that is how a rotated\n * key is picked up — but at most this often, so a stream of tokens with random\n * `kid` values cannot turn every request into a JWKS fetch.\n */\nexport const JWKS_MIN_REFRESH_MS = 30 * 1_000;\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * The subset of standard JWT payload claims this module reads or validates.\n *\n * All fields are `readonly`. Additional custom claims are preserved under\n * the index signature and accessible via `payload[\"my-claim\"]`.\n *\n * Refer to the module-level documentation for the full verification matrix.\n */\nexport interface JwtPayload {\n /** Issuer (`iss`) — identifies the auth server that created the token. */\n readonly iss?: string;\n /** Subject (`sub`) — the user's unique identifier. */\n readonly sub?: string;\n /**\n * Audience (`aud`) — the intended recipient(s).\n * A single string or an array of strings per RFC 7519 §4.1.3.\n */\n readonly aud?: string | readonly string[];\n /** Expiration time (`exp`) — seconds since Unix epoch. */\n readonly exp?: number;\n /** Not before (`nbf`) — seconds since Unix epoch. */\n readonly nbf?: number;\n /** Issued at (`iat`) — seconds since Unix epoch. */\n readonly iat?: number;\n /** User's email address (custom claim). */\n readonly email?: string;\n /** User's display name (custom claim). */\n readonly name?: string;\n readonly [key: string]: unknown;\n}\n\n/**\n * The typed representation of a JWT header.\n *\n * The three fields below are the only ones this library reads. Additional\n * header parameters are preserved under the index signature.\n */\nexport interface JwtHeader {\n /** Signing algorithm (`alg`) — e.g. `\"RS256\"`, `\"ES256\"`. */\n readonly alg?: string;\n /** Token type (`typ`) — e.g. `\"JWT\"`, `\"at+JWT\"`. */\n readonly typ?: string;\n /** Key ID (`kid`) — selects the matching public key from the JWKS. */\n readonly kid?: string;\n readonly [key: string]: unknown;\n}\n\n/**\n * The decoded, unverified parts of a compact-serialised JWT.\n *\n * **Do not act on these values before signature verification.**\n * This type is returned by {@link decodeJwt} purely to carry the\n * raw data into the next step.\n */\nexport interface DecodedJwt {\n readonly header: JwtHeader;\n readonly payload: JwtPayload;\n}\n\n/** A public key as published in a JWKS document. */\ntype Jwk = JsonWebKey & { readonly kid?: string; readonly alg?: string };\n\n/**\n * The cached JWKS document of one issuer. The whole key set is cached rather\n * than one key per `kid`, so a token naming an unknown `kid` is answered from\n * the cache instead of costing a JWKS fetch.\n */\ninterface JwksCacheEntry {\n readonly keys: readonly Jwk[];\n readonly fetchedAt: number;\n /** Imported keys, by `kid` and algorithm, so each is imported once. */\n readonly imported: Map<string, CryptoKey>;\n}\n\n/**\n * Options for {@link verifyJwt}.\n *\n * All fields are `readonly` — this object is never mutated after construction.\n */\nexport interface VerifyJwtOptions {\n /**\n * Base URL of the auth backend.\n *\n * Serves two purposes:\n * 1. Determines the JWKS endpoint: `{issuerUrl}/auth/keys`\n * 2. Is compared against the `iss` claim when present in the token.\n */\n readonly issuerUrl: string;\n\n /**\n * Restrict accepted `alg` header values. Tokens whose algorithm is not in\n * this list are rejected before the JWKS is fetched.\n * When `undefined`, all algorithms are accepted (the middleware layer\n * defaults to `[\"RS256\", \"ES256\"]` before passing options here).\n */\n readonly allowedAlgorithms?: readonly string[];\n\n /**\n * Clock-skew tolerance in milliseconds, applied symmetrically to `exp`,\n * `nbf`, and `iat` checks to accommodate minor differences between clocks.\n */\n readonly clockSkewMs: number;\n\n /**\n * Expected `aud` claim value(s). When `undefined`, audience is not\n * validated. Provide a string or array when the auth server sets `aud`\n * and you want to ensure the token was issued specifically for your app.\n */\n readonly audience?: string | readonly string[];\n\n /**\n * Accepted `typ` header values (case-insensitive).\n *\n * Guards against using a refresh token or ID token where an access token\n * is expected. Set to an empty array (`[]`) to disable this check.\n *\n * @default [\"JWT\", \"at+JWT\"]\n */\n readonly allowedTokenTypes: readonly string[];\n\n /**\n * Timeout in milliseconds for JWKS endpoint requests.\n * Requests that do not complete within this window are aborted and the\n * token is rejected (function returns `null`).\n * @default 5000\n */\n readonly jwksTimeoutMs?: number;\n}\n\n// ─── JWKS cache ───────────────────────────────────────────────────────────────\n\n/**\n * In-memory JWKS cache: one document per JWKS URI. Keying by the full URI keeps\n * two issuers that publish the same `kid` apart. It holds one entry per issuer\n * the process verifies against, each with the keys that issuer published, so it\n * cannot be grown by the `kid` values tokens claim.\n */\nconst jwksCache = new Map<string, JwksCacheEntry>();\n\n/** In-flight JWKS fetches by URI, so concurrent misses share one request. */\nconst jwksInFlight = new Map<string, Promise<JwksCacheEntry | null>>();\n\n/**\n * When each URI was last fetched, successfully or not. The refresh throttle\n * counts from here, not from the last document, so a failing endpoint is not\n * retried on every request.\n */\nconst jwksAttemptedAt = new Map<string, number>();\n\n/**\n * Bumped by {@link clearJwksCache}. A fetch that started before a clear must not\n * write its answer into the cache or the in-flight table afterwards.\n */\nlet jwksGeneration = 0;\n\n/**\n * Forgets every cached JWKS document, so the next verification fetches the key\n * set anew. Use it to drop a compromised key at once instead of waiting out\n * {@link JWKS_TTL_MS}, and in tests that publish different keys per case.\n */\nexport function clearJwksCache(): void {\n jwksGeneration++;\n jwksCache.clear();\n jwksInFlight.clear();\n jwksAttemptedAt.clear();\n}\n\nconst DECODER = new TextDecoder();\nconst ENCODER = new TextEncoder();\n\n// ─── Base64URL ────────────────────────────────────────────────────────────────\n\n/**\n * Decodes a Base64URL-encoded string into a `Uint8Array`.\n *\n * The Edge runtime has no `Buffer`, so this uses `atob` after translating the\n * Base64URL alphabet (`-` → `+`, `_` → `/`) and padding to a multiple of four.\n *\n * @param input - A Base64URL-encoded string (padding is optional).\n * @returns The decoded bytes.\n */\nexport function base64UrlDecode(input: string): Uint8Array<ArrayBuffer> {\n const base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n return Uint8Array.from(binary, (c) => c.charCodeAt(0));\n}\n\n// ─── JWT splitting ────────────────────────────────────────────────────────────\n\n/**\n * Splits a compact JWT string into its three raw Base64URL segments without\n * decoding or verifying any of them.\n *\n * Tokens with more than three segments (e.g. nested JWTs) are accepted; only\n * the first three parts are returned. Tokens with fewer than three segments are\n * structurally invalid compact JWTs and cannot be verified.\n *\n * @param token - The compact JWT string.\n * @returns A `[header, payload, signature]` tuple, or `null` when the token\n * has fewer than three dot-separated segments.\n */\nfunction splitToken(token: string): readonly [string, string, string] | null {\n const [h, p, s] = token.split(\".\");\n if (!h || !p || !s) {\n return null;\n }\n return [h, p, s] as const;\n}\n\n// ─── JWT decode ───────────────────────────────────────────────────────────────\n\n/**\n * Decodes a compact-serialised JWT into its header and payload **without**\n * verifying the signature.\n *\n * **Do not trust the result of this function alone.** Use it only as the first\n * step of a full verification pipeline — the signature check in {@link verifyJwt}\n * must follow before any claim can be trusted.\n *\n * @param token - A compact JWT string (`header.payload.signature`).\n * @returns The decoded {@link DecodedJwt}.\n * @throws {TypeError} When `token` has fewer than three dot-separated segments.\n */\nexport function decodeJwt(token: string): DecodedJwt {\n const segments = splitToken(token);\n if (!segments) {\n throw new TypeError(\n `Invalid JWT: expected at least three dot-separated segments, received ${token.split(\".\").length}.`,\n );\n }\n const [h, p] = segments;\n return {\n header: JSON.parse(DECODER.decode(base64UrlDecode(h))) as JwtHeader,\n payload: JSON.parse(DECODER.decode(base64UrlDecode(p))) as JwtPayload,\n };\n}\n\n// ─── JWT shape detection ──────────────────────────────────────────────────────\n\n/**\n * Returns `true` when `token` looks structurally like a signed JWT (JWS) —\n * NOT an encrypted token (JWE).\n *\n * JWS compact: 3 dot-separated segments; header has `alg` but NOT `enc`.\n * JWE compact: 5 dot-separated segments; header has BOTH `alg` and `enc`.\n *\n * The Zitadel backend issues JWE tokens (AES-256-GCM via go-jose\n * `A256GCMKW/A256GCM`), whose compact form is\n * `header.encrypted_key.iv.ciphertext.tag`. The header decodes to JSON with\n * `\"alg\":\"A256GCMKW\",\"enc\":\"A256GCM\"`. Checking for the absence of `enc`\n * correctly identifies signed JWTs without false-positives on JWE tokens.\n *\n * This is a structural check only — not a security check. Use it to route a\n * token to the correct validation path ({@link verifyJwt} for JWS, a backend\n * session lookup for everything else), never to trust one.\n */\nexport function isJwtShaped(token: string): boolean {\n const parts = token.split(\".\");\n if (parts.length < 3 || !parts[0]) return false;\n try {\n const header = JSON.parse(DECODER.decode(base64UrlDecode(parts[0]))) as Record<string, unknown>;\n return typeof header?.alg === \"string\" && !(\"enc\" in header);\n } catch {\n return false;\n }\n}\n\n// ─── Algorithm helpers ────────────────────────────────────────────────────────\n\n/**\n * Web Crypto parameters for importing a public key from JWKS, keyed by JWT\n * `alg` header value. The keys of this object are the only supported algorithms;\n * anything not present throws {@link TypeError}.\n */\nconst IMPORT_PARAMS = {\n RS256: { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-256\" },\n RS384: { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-384\" },\n RS512: { name: \"RSASSA-PKCS1-v1_5\", hash: \"SHA-512\" },\n ES256: { name: \"ECDSA\", namedCurve: \"P-256\" },\n ES384: { name: \"ECDSA\", namedCurve: \"P-384\" },\n ES512: { name: \"ECDSA\", namedCurve: \"P-521\" },\n} satisfies Record<string, RsaHashedImportParams | EcKeyImportParams>;\n\n/**\n * Web Crypto parameters for verifying a JWT signature, keyed by JWT `alg`\n * header value.\n */\nconst VERIFY_PARAMS = {\n RS256: { name: \"RSASSA-PKCS1-v1_5\" },\n RS384: { name: \"RSASSA-PKCS1-v1_5\" },\n RS512: { name: \"RSASSA-PKCS1-v1_5\" },\n ES256: { name: \"ECDSA\", hash: \"SHA-256\" },\n ES384: { name: \"ECDSA\", hash: \"SHA-384\" },\n ES512: { name: \"ECDSA\", hash: \"SHA-512\" },\n} satisfies Record<string, EcdsaParams | Algorithm>;\n\nfunction assertSupportedAlgorithm(alg: string): asserts alg is keyof typeof IMPORT_PARAMS {\n if (!Object.hasOwn(IMPORT_PARAMS, alg)) {\n throw new TypeError(\n `Unsupported JWT algorithm: \"${alg}\". Supported algorithms are ${Object.keys(IMPORT_PARAMS).join(\", \")}.`,\n );\n }\n}\n\n/** The JWT `alg` an EC key's curve signs with (RFC 7518 §3.4). */\nconst CURVE_ALGORITHMS: Readonly<Record<string, string>> = {\n \"P-256\": \"ES256\",\n \"P-384\": \"ES384\",\n \"P-521\": \"ES512\",\n};\n\n/**\n * The Web Crypto import parameters for verifying a token signed with `alg`\n * under `jwk`, or `null` when the key cannot have produced it.\n *\n * RFC 7517 §4.4 makes a JWK's `alg` optional, so the key type decides: an EC\n * key's curve fixes its algorithm, and an RSA key fits any `RS*` algorithm.\n * When the JWK names an `alg`, the token must use that one. A mismatch is a\n * token that cannot verify against this key, not an error worth logging.\n */\nfunction importParams(jwk: Jwk, alg: string): RsaHashedImportParams | EcKeyImportParams | null {\n if (!Object.hasOwn(IMPORT_PARAMS, alg)) {\n return null;\n }\n if (jwk.alg !== undefined && jwk.alg !== alg) {\n return null;\n }\n const fits =\n jwk.kty === \"RSA\"\n ? alg.startsWith(\"RS\")\n : jwk.kty === \"EC\" && CURVE_ALGORITHMS[jwk.crv ?? \"\"] === alg;\n return fits ? IMPORT_PARAMS[alg as keyof typeof IMPORT_PARAMS] : null;\n}\n\nfunction resolveVerifyAlgorithm(alg: string): EcdsaParams | Algorithm {\n assertSupportedAlgorithm(alg);\n return VERIFY_PARAMS[alg];\n}\n\n// ─── JWKS fetch ───────────────────────────────────────────────────────────────\n\n/** Resolves to `null` if `promise` has not settled within `timeoutMs`. */\nfunction withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | null> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const expired = new Promise<null>((resolve) => {\n timer = setTimeout(() => {\n resolve(null);\n }, timeoutMs);\n });\n return Promise.race([promise, expired]).finally(() => {\n clearTimeout(timer);\n });\n}\n\n/**\n * Fetches the JWKS document at `jwksUri`, coalescing concurrent fetches, and\n * caches it. Returns `null` when the endpoint fails or answers without a key\n * set, and also, without fetching, when the URI was fetched less than\n * {@link JWKS_MIN_REFRESH_MS} ago: a failed fetch counts, so a failing endpoint\n * is not retried on every request. A failure leaves any previously cached\n * document in place, but {@link resolveJwk} does not use it once it has expired.\n */\nfunction refreshJwks(jwksUri: string, timeoutMs: number): Promise<JwksCacheEntry | null> {\n const pending = jwksInFlight.get(jwksUri);\n if (pending) {\n // The fetch in flight runs on the timeout of whoever started it. A caller\n // that joins it waits no longer than its own.\n return withTimeout(pending, timeoutMs);\n }\n const attemptedAt = jwksAttemptedAt.get(jwksUri);\n if (attemptedAt !== undefined && Date.now() - attemptedAt < JWKS_MIN_REFRESH_MS) {\n return Promise.resolve(null);\n }\n const generation = jwksGeneration;\n jwksAttemptedAt.set(jwksUri, Date.now());\n const request: Promise<JwksCacheEntry | null> = (async () => {\n const res = await fetch(jwksUri, { signal: AbortSignal.timeout(timeoutMs) });\n if (!res.ok) {\n return null;\n }\n const json = (await res.json()) as { keys?: unknown };\n if (!Array.isArray(json.keys)) {\n return null;\n }\n const entry: JwksCacheEntry = {\n keys: json.keys as Jwk[],\n fetchedAt: Date.now(),\n imported: new Map(),\n };\n if (generation === jwksGeneration) {\n jwksCache.set(jwksUri, entry);\n }\n return entry;\n })().finally(() => {\n // Only the request that is still the registered one is removed: a clear can\n // have replaced it with a newer fetch that must stay shared.\n if (jwksInFlight.get(jwksUri) === request) {\n jwksInFlight.delete(jwksUri);\n }\n });\n jwksInFlight.set(jwksUri, request);\n return request;\n}\n\n/** The key a token names: the one with its `kid`, or the first without one. */\nfunction findKey(entry: JwksCacheEntry, kid: string | undefined): Jwk | undefined {\n return kid === undefined ? entry.keys[0] : entry.keys.find((key) => key.kid === kid);\n}\n\n/**\n * Resolves the public key for a token from the issuer's JWKS, imported for\n * `alg`.\n *\n * The JWKS document is cached for {@link JWKS_TTL_MS}. A `kid` the cached\n * document lacks refreshes it — a rotated key — but no sooner than\n * {@link JWKS_MIN_REFRESH_MS} after the last fetch, so tokens with made-up\n * `kid` values are answered from the cache. An expired document is never used\n * when its refresh fails. Returns `null` when no key fits: the endpoint failed,\n * no key has that `kid`, or the key cannot verify `alg`.\n *\n * @param jwksUri - Full URL of the JWKS endpoint.\n * @param kid - The `kid` header value from the JWT, or `undefined`.\n * @param alg - The token's (already allowlisted) `alg` header value.\n */\nasync function resolveJwk(\n jwksUri: string,\n kid: string | undefined,\n alg: string,\n timeoutMs: number,\n): Promise<CryptoKey | null> {\n const now = Date.now();\n let entry = jwksCache.get(jwksUri);\n if (!entry || now - entry.fetchedAt >= JWKS_TTL_MS) {\n // An expired document is never used: a key the issuer has removed, or one\n // that was compromised, must stop verifying once the TTL has passed even if\n // the endpoint is down.\n entry = (await refreshJwks(jwksUri, timeoutMs)) ?? undefined;\n }\n if (!entry) {\n return null;\n }\n\n let jwk = findKey(entry, kid);\n if (!jwk && now - entry.fetchedAt >= JWKS_MIN_REFRESH_MS) {\n const refreshed = await refreshJwks(jwksUri, timeoutMs);\n if (refreshed) {\n entry = refreshed;\n jwk = findKey(entry, kid);\n }\n }\n if (!jwk) {\n return null;\n }\n\n const params = importParams(jwk, alg);\n if (!params) {\n return null;\n }\n const importedKey = `${kid ?? \"__default__\"}:${alg}`;\n const cached = entry.imported.get(importedKey);\n if (cached) {\n return cached;\n }\n const cryptoKey = await crypto.subtle.importKey(\"jwk\", jwk, params, false, [\"verify\"]);\n entry.imported.set(importedKey, cryptoKey);\n return cryptoKey;\n}\n\n// ─── Verification ─────────────────────────────────────────────────────────────\n\n/**\n * Verifies a compact-serialised JWT and returns its validated payload.\n *\n * ## Verification steps (in order)\n *\n * 1. Split the token — returns `null` immediately when fewer than three\n * dot-separated segments are present.\n * 2. Reject if `alg` is not in {@link VerifyJwtOptions.allowedAlgorithms} (when set).\n * 3. Reject if `typ` is not in {@link VerifyJwtOptions.allowedTokenTypes} (when non-empty).\n * 4. Fetch (or retrieve from cache) the matching public key from the JWKS endpoint.\n * 5. Verify the cryptographic signature over `header.payload`.\n * 6. Reject if `iss` is absent or does not equal {@link VerifyJwtOptions.issuerUrl}.\n * 7. Reject if {@link VerifyJwtOptions.audience} is set and `aud` does not contain it.\n * 8. Reject if `exp` is absent or is in the past (minus clock-skew tolerance).\n * 9. Reject if `nbf` is in the future (plus clock-skew tolerance).\n * 10. Reject if `iat` is in the future (plus clock-skew tolerance).\n *\n * Returns `null` on any failure rather than throwing, so callers can treat a\n * bad token identically to a missing one without try/catch at the call site.\n *\n * @param token - The raw compact-serialised JWT string.\n * @param opts - Verification options; see {@link VerifyJwtOptions}.\n * @returns The verified {@link JwtPayload}, or `null` if verification fails.\n */\nexport async function verifyJwt(token: string, opts: VerifyJwtOptions): Promise<JwtPayload | null> {\n try {\n const {\n issuerUrl,\n allowedAlgorithms,\n clockSkewMs,\n audience,\n allowedTokenTypes,\n jwksTimeoutMs = 5000,\n } = opts;\n\n const segments = splitToken(token);\n if (!segments) {\n return null;\n }\n const [rawHeader, rawPayload, rawSig] = segments;\n\n const header = JSON.parse(DECODER.decode(base64UrlDecode(rawHeader))) as JwtHeader;\n const payload = JSON.parse(DECODER.decode(base64UrlDecode(rawPayload))) as JwtPayload;\n\n // RFC 7515 §4.1.1 requires 'alg' in the JWT header. A token that omits it\n // is structurally invalid — we reject it rather than guessing an algorithm.\n const alg = header.alg;\n if (!alg) {\n return null;\n }\n // Unconditionally reject the 'none' algorithm regardless of the allowlist.\n // 'none' means the token is unsigned; accepting it would allow anyone to\n // forge arbitrary claims without a key. The comparison is case-insensitive\n // so that \"None\", \"NONE\", or any other casing variant is also rejected.\n if (alg.toLowerCase() === \"none\") {\n return null;\n }\n if (allowedAlgorithms?.length && !allowedAlgorithms.includes(alg)) {\n return null;\n }\n\n if (allowedTokenTypes.length > 0) {\n const typ = header.typ ?? \"\";\n if (!allowedTokenTypes.some((t) => t.toLowerCase() === typ.toLowerCase())) {\n return null;\n }\n }\n\n const kid = header.kid;\n const cryptoKey = await resolveJwk(joinURL(issuerUrl, \"/auth/keys\"), kid, alg, jwksTimeoutMs);\n if (!cryptoKey) {\n return null;\n }\n\n const valid = await crypto.subtle.verify(\n resolveVerifyAlgorithm(alg),\n cryptoKey,\n base64UrlDecode(rawSig),\n ENCODER.encode(`${rawHeader}.${rawPayload}`),\n );\n if (!valid) {\n return null;\n }\n\n // Strict equality is intentional: the issuer URL must match exactly as\n // configured, apart from a trailing slash on the configured address, which\n // is not part of an issuer. We deliberately do not normalise case or\n // surrounding whitespace — any difference is either a misconfiguration or\n // a token issued by a different server and must be rejected.\n if (payload.iss !== withoutTrailingSlash(issuerUrl)) {\n return null;\n }\n\n if (audience !== undefined) {\n const audList = payload.aud !== undefined ? [payload.aud].flat() : [];\n const expectedList = [audience].flat();\n if (!expectedList.some((a) => audList.includes(a))) {\n return null;\n }\n }\n\n const now = Date.now();\n if (payload.exp === undefined || payload.exp * 1000 < now - clockSkewMs) {\n return null;\n }\n if (payload.nbf !== undefined && payload.nbf * 1000 > now + clockSkewMs) {\n return null;\n }\n if (payload.iat !== undefined && payload.iat * 1000 > now + clockSkewMs) {\n return null;\n }\n\n return payload;\n } catch (err) {\n // SyntaxError from JSON.parse means the token segments are not JSON —\n // this is an opaque (non-JWT) token. Return null silently; the middleware\n // will fall back to opaque token validation via the backend.\n if (err instanceof SyntaxError) {\n return null;\n }\n // Any other unexpected error (network failure, malformed JWKS, Web Crypto\n // exception) is worth logging so operators can distinguish infrastructure\n // failures from genuine bad tokens.\n console.error(\"[nextgen] verifyJwt unexpected error:\", err);\n return null;\n }\n}\n"],"mappings":"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"}
|