@zitadel/sdk-core 0.1.0-alpha.0 → 0.1.0-alpha.11
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/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -32
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +376 -102
- package/dist/middleware.d.ts +113 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +72 -20
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +99 -196
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +20 -0
- package/package.json +26 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { NextgenSession, AuthState, UnauthState, AuthResult, NextgenMiddlewareOptions } from "./
|
|
2
|
-
export { HOP_BY_HOP, INTERNAL_HEADERS, matchesRoutes, filterResponseHeaders } from "./middleware.js";
|
|
1
|
+
export type { NextgenSession, AuthState, UnauthState, AuthResult, NextgenMiddlewareOptions, } from "./middleware.js";
|
|
2
|
+
export { HOP_BY_HOP, INTERNAL_HEADERS, matchesRoutes, filterResponseHeaders, } from "./middleware.js";
|
|
3
3
|
export { verifyJwt, decodeJwt, base64UrlDecode, JWKS_TTL_MS } from "./jwt.js";
|
|
4
4
|
export type { JwtPayload, JwtHeader, DecodedJwt, VerifyJwtOptions } from "./jwt.js";
|
|
5
5
|
export type ZitadelEnvironment = "development" | "preview" | "production";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,cAAc,EACd,SAAS,EACT,WAAW,EACX,UAAU,EACV,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC9E,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEpF,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG,SAAS,GAAG,YAAY,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,kBAAkB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK1C;AAED,wBAAgB,wBAAwB,CACtC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAgB,GACrD,cAAc,CAMhB;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,mBAAmB,GAAG,cAAc,CAgBhF"}
|
package/dist/index.js
CHANGED
|
@@ -1,36 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
function l(e = f()) {
|
|
11
|
-
return u({
|
|
12
|
-
projectId: e.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? e.ZITADEL_PROJECT_ID,
|
|
13
|
-
environment: d(e.NEXT_PUBLIC_ZITADEL_ENVIRONMENT ?? e.ZITADEL_ENVIRONMENT),
|
|
14
|
-
issuer: e.NEXT_PUBLIC_ZITADEL_ISSUER ?? e.ZITADEL_ISSUER
|
|
15
|
-
});
|
|
1
|
+
export { HOP_BY_HOP, INTERNAL_HEADERS, matchesRoutes, filterResponseHeaders, } from "./middleware.js";
|
|
2
|
+
export { verifyJwt, decodeJwt, base64UrlDecode, JWKS_TTL_MS } from "./jwt.js";
|
|
3
|
+
export class ZitadelRuntimeError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ZitadelRuntimeError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
16
10
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
environment: t,
|
|
24
|
-
issuer: e.issuer
|
|
25
|
-
};
|
|
11
|
+
export function resolveZitadelRuntimeEnv(env = currentEnv()) {
|
|
12
|
+
return resolveZitadelRuntime({
|
|
13
|
+
projectId: env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? env.ZITADEL_PROJECT_ID,
|
|
14
|
+
environment: parseEnvironment(env.NEXT_PUBLIC_ZITADEL_ENVIRONMENT ?? env.ZITADEL_ENVIRONMENT),
|
|
15
|
+
issuer: env.NEXT_PUBLIC_ZITADEL_ISSUER ?? env.ZITADEL_ISSUER,
|
|
16
|
+
});
|
|
26
17
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
export function resolveZitadelRuntime(input) {
|
|
19
|
+
const environment = input.environment ?? "development";
|
|
20
|
+
if (!input.projectId) {
|
|
21
|
+
throw new ZitadelRuntimeError("E_ZITADEL_CONFIG", "ZITADEL_PROJECT_ID is required for Zitadel runtime.");
|
|
22
|
+
}
|
|
23
|
+
if (environment === "production" && !input.issuer) {
|
|
24
|
+
throw new ZitadelRuntimeError("E_ZITADEL_CONFIG", "ZITADEL_ISSUER is required in production.");
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
projectId: input.projectId,
|
|
28
|
+
environment,
|
|
29
|
+
issuer: input.issuer,
|
|
30
|
+
};
|
|
31
31
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
32
|
+
function parseEnvironment(value) {
|
|
33
|
+
if (!value || value === "development")
|
|
34
|
+
return "development";
|
|
35
|
+
if (value === "preview" || value === "production")
|
|
36
|
+
return value;
|
|
37
|
+
throw new ZitadelRuntimeError("E_ZITADEL_CONFIG", `Unsupported ZITADEL_ENVIRONMENT "${value}".`);
|
|
38
|
+
}
|
|
39
|
+
function currentEnv() {
|
|
40
|
+
const scope = globalThis;
|
|
41
|
+
return scope.process?.env ?? {};
|
|
34
42
|
}
|
|
35
|
-
//#endregion
|
|
36
|
-
export { e as HOP_BY_HOP, t as INTERNAL_HEADERS, i as JWKS_TTL_MS, c as ZitadelRuntimeError, a as base64UrlDecode, o as decodeJwt, n as filterResponseHeaders, r as matchesRoutes, u as resolveZitadelRuntime, l as resolveZitadelRuntimeEnv, s as verifyJwt };
|
package/dist/jwt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2GG;AAIH,yEAAyE;AACzE,eAAO,MAAM,WAAW,QAAiB,CAAC;AAI1C;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC1C,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;CAC9B;AAYD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE/C;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAE/C;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAE9C;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AA8BD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2GG;AAIH,yEAAyE;AACzE,eAAO,MAAM,WAAW,QAAiB,CAAC;AAI1C;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAC1C,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,oDAAoD;IACpD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;CAC9B;AAYD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAE3B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE/C;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAE/C;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAE9C;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AA8BD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,CAKtE;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAYnD;AA8GD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqGjG"}
|
package/dist/jwt.js
CHANGED
|
@@ -1,111 +1,385 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file lib/jwt.ts
|
|
3
|
+
*
|
|
4
|
+
* # JWT Verification
|
|
5
|
+
*
|
|
6
|
+
* Stateless JWT verification via JWKS, with in-process key caching.
|
|
7
|
+
*
|
|
8
|
+
* ## Anatomy of a JWT
|
|
9
|
+
*
|
|
10
|
+
* A compact JWT (JWS compact serialisation) is three Base64URL-encoded parts
|
|
11
|
+
* joined by dots:
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* <header>.<payload>.<signature>
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* - **Header** — metadata about the token: signing algorithm, key ID, token type.
|
|
18
|
+
* - **Payload** — the claims: user identity, expiry, issuer, audience, etc.
|
|
19
|
+
* - **Signature** — `SIGN(privateKey, "<header>.<payload>")`. Any party with the
|
|
20
|
+
* matching public key can verify it without contacting the auth server.
|
|
21
|
+
*
|
|
22
|
+
* A valid signature proves two things:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Authenticity** — the token was produced by the holder of the private key.
|
|
25
|
+
* 2. **Integrity** — neither the header nor the payload has been altered since signing.
|
|
26
|
+
*
|
|
27
|
+
* Signature verification alone is not sufficient. The claims inside the payload
|
|
28
|
+
* must also be checked (expiry, issuer, audience, …). Both steps are performed
|
|
29
|
+
* by {@link verifyJwt}.
|
|
30
|
+
*
|
|
31
|
+
* ---
|
|
32
|
+
*
|
|
33
|
+
* ## JWT Header fields
|
|
34
|
+
*
|
|
35
|
+
* The header is a JSON object Base64URL-encoded into the first segment.
|
|
36
|
+
*
|
|
37
|
+
* | Field | Purpose | Verified |
|
|
38
|
+
* |-------|---------|---------|
|
|
39
|
+
* | `alg` | Signing algorithm (`RS256`, `ES256`, …) | Checked against {@link VerifyJwtOptions.allowedAlgorithms} before JWKS is fetched |
|
|
40
|
+
* | `kid` | Key ID — selects the matching public key from the JWKS | Used for key lookup; falls back to the first JWKS key when absent |
|
|
41
|
+
* | `typ` | Token type (`JWT`, `at+JWT`, …) | Checked against {@link VerifyJwtOptions.allowedTokenTypes} (case-insensitive) |
|
|
42
|
+
* | ~~`enc`~~ | ~~Encryption algorithm — present in JWE (encrypted) tokens only~~ | ~~Not applicable — we verify JWS (signed) tokens, not JWE (encrypted)~~ |
|
|
43
|
+
* | ~~`cty`~~ | ~~Content type — describes the secured content media type~~ | ~~Not applicable — not set by standard OIDC/OAuth auth servers~~ |
|
|
44
|
+
*
|
|
45
|
+
* ---
|
|
46
|
+
*
|
|
47
|
+
* ## JWT Payload claims
|
|
48
|
+
*
|
|
49
|
+
* The payload is a JSON object Base64URL-encoded into the second segment.
|
|
50
|
+
*
|
|
51
|
+
* ### Validated claims
|
|
52
|
+
*
|
|
53
|
+
* | Claim | Purpose | How |
|
|
54
|
+
* |-------|---------|-----|
|
|
55
|
+
* | `iss` | Issuer — the URL of the auth server that issued the token | Must be present and must equal {@link VerifyJwtOptions.issuerUrl} |
|
|
56
|
+
* | `exp` | Expiration time — Unix timestamp after which the token is invalid | Must be present and in the future, allowing {@link VerifyJwtOptions.clockSkewMs} tolerance |
|
|
57
|
+
* | `nbf` | Not before — Unix timestamp before which the token must not be accepted | Must be in the past, allowing {@link VerifyJwtOptions.clockSkewMs} tolerance |
|
|
58
|
+
* | `iat` | Issued at — Unix timestamp when the token was created | Must not be in the future beyond {@link VerifyJwtOptions.clockSkewMs} |
|
|
59
|
+
* | `aud` | Audience — the intended recipient(s) of the token | Checked against {@link VerifyJwtOptions.audience} when that option is provided |
|
|
60
|
+
*
|
|
61
|
+
* ### Read-only claims (extracted into the session, not further validated)
|
|
62
|
+
*
|
|
63
|
+
* | Claim | Purpose |
|
|
64
|
+
* |-------|---------|
|
|
65
|
+
* | `sub` | Subject — the user's unique identifier |
|
|
66
|
+
* | `email` | User's email address |
|
|
67
|
+
* | `name` | User's display name |
|
|
68
|
+
*
|
|
69
|
+
* ### Not validated
|
|
70
|
+
*
|
|
71
|
+
* | Claim | Purpose | Why skipped |
|
|
72
|
+
* |-------|---------|-------------|
|
|
73
|
+
* | ~~`jti`~~ | ~~JWT ID — a unique identifier for this specific token instance~~ | ~~Revocation requires a server-side blocklist; not feasible in stateless edge middleware~~ |
|
|
74
|
+
* | ~~`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~~ |
|
|
75
|
+
* | ~~`nonce`~~ | ~~OIDC nonce — binds a token to a specific authorisation request~~ | ~~Not applicable in our password/cookie flow (no OIDC redirect round-trip)~~ |
|
|
76
|
+
* | ~~`acr`~~ | ~~Authentication class reference — describes the strength of the authentication~~ | ~~Not validated (future: require MFA for sensitive routes)~~ |
|
|
77
|
+
* | ~~`amr`~~ | ~~Authentication methods reference — list of methods used to authenticate~~ | ~~Not validated~~ |
|
|
78
|
+
* | ~~`scope`~~ | ~~OAuth 2.0 scopes granted to the token~~ | ~~Not validated (future: per-route scope enforcement)~~ |
|
|
79
|
+
*
|
|
80
|
+
* ---
|
|
81
|
+
*
|
|
82
|
+
* ## JWKS caching
|
|
83
|
+
*
|
|
84
|
+
* Public keys are expensive to fetch and import. They are cached in-process,
|
|
85
|
+
* keyed by `"${jwksUri}:${kid}"` (or `"${jwksUri}:__default__"` when `kid` is
|
|
86
|
+
* absent), for {@link JWKS_TTL_MS} milliseconds (default 5 minutes).
|
|
87
|
+
*
|
|
88
|
+
* Including the full JWKS URI in the cache key prevents a key imported for one
|
|
89
|
+
* issuer from being mistakenly served when a different issuer happens to use the
|
|
90
|
+
* same `kid` string — a scenario that can arise in multi-tenant deployments or
|
|
91
|
+
* during key-rotation incidents where two servers briefly share a key identifier.
|
|
92
|
+
*
|
|
93
|
+
* A cache miss — on first use or after TTL expiry — triggers a fresh fetch
|
|
94
|
+
* from `{issuerUrl}/auth/keys`.
|
|
95
|
+
*
|
|
96
|
+
* > **Note (serverless):** the cache is module-level. In serverless environments
|
|
97
|
+
* > each cold start gets a fresh cache; warm instances share it across requests.
|
|
98
|
+
*
|
|
99
|
+
* ---
|
|
100
|
+
*
|
|
101
|
+
* ## Runtime note
|
|
102
|
+
*
|
|
103
|
+
* This file is the **shared implementation** used by both `sdk-next` (Edge
|
|
104
|
+
* runtime) and `sdk-nuxt` (Node.js/Nitro). `base64UrlDecode` uses `atob`,
|
|
105
|
+
* which is available in both environments (Edge natively; Node.js since v16).
|
|
106
|
+
* The previous per-SDK copies (one using `atob`, one using `Buffer`) have
|
|
107
|
+
* been consolidated here.
|
|
108
|
+
*/
|
|
109
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
110
|
+
/** How long a cached JWKS key is considered fresh before re-fetching. */
|
|
111
|
+
export const JWKS_TTL_MS = 5 * 60 * 1_000;
|
|
112
|
+
// ─── JWKS cache ───────────────────────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* In-memory JWKS key cache.
|
|
115
|
+
*
|
|
116
|
+
* Keys are of the form `"${jwksUri}:${kid}"`, or `"${jwksUri}:__default__"`
|
|
117
|
+
* when the JWT carries no `kid`. Including the full JWKS URI prevents a cached
|
|
118
|
+
* key from one issuer being returned for a request targeting a different issuer
|
|
119
|
+
* that happens to share the same `kid` string — a scenario that can arise in
|
|
120
|
+
* multi-tenant deployments or during key-rotation incidents.
|
|
121
|
+
*
|
|
122
|
+
* Cache entries are immutable once written; the cache itself grows up to one
|
|
123
|
+
* entry per distinct `(jwksUri, kid)` pair seen in the lifetime of the process.
|
|
124
|
+
*
|
|
125
|
+
* The cache is intentionally unbounded. In practice the number of distinct
|
|
126
|
+
* (jwksUri, kid) pairs is bounded by the number of key rotations during the
|
|
127
|
+
* process lifetime — typically a handful. A bounded LRU eviction policy could
|
|
128
|
+
* be added if memory growth becomes a concern for very long-lived processes
|
|
129
|
+
* with frequent key rotations, but is omitted here to keep the implementation
|
|
130
|
+
* dependency-free.
|
|
131
|
+
*/
|
|
132
|
+
const jwksCache = new Map();
|
|
133
|
+
const DECODER = new TextDecoder();
|
|
134
|
+
const ENCODER = new TextEncoder();
|
|
135
|
+
// ─── Base64URL ────────────────────────────────────────────────────────────────
|
|
136
|
+
/**
|
|
137
|
+
* Decodes a Base64URL-encoded string into a `Uint8Array`.
|
|
138
|
+
*
|
|
139
|
+
* The Edge runtime has no `Buffer`, so this uses `atob` after translating the
|
|
140
|
+
* Base64URL alphabet (`-` → `+`, `_` → `/`) and padding to a multiple of four.
|
|
141
|
+
*
|
|
142
|
+
* @param input - A Base64URL-encoded string (padding is optional).
|
|
143
|
+
* @returns The decoded bytes.
|
|
144
|
+
*/
|
|
145
|
+
export function base64UrlDecode(input) {
|
|
146
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
147
|
+
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
|
|
148
|
+
const binary = atob(padded);
|
|
149
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
6
150
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
151
|
+
// ─── JWT splitting ────────────────────────────────────────────────────────────
|
|
152
|
+
/**
|
|
153
|
+
* Splits a compact JWT string into its three raw Base64URL segments without
|
|
154
|
+
* decoding or verifying any of them.
|
|
155
|
+
*
|
|
156
|
+
* Tokens with more than three segments (e.g. nested JWTs) are accepted; only
|
|
157
|
+
* the first three parts are returned. Tokens with fewer than three segments are
|
|
158
|
+
* structurally invalid compact JWTs and cannot be verified.
|
|
159
|
+
*
|
|
160
|
+
* @param token - The compact JWT string.
|
|
161
|
+
* @returns A `[header, payload, signature]` tuple, or `null` when the token
|
|
162
|
+
* has fewer than three dot-separated segments.
|
|
163
|
+
*/
|
|
164
|
+
function splitToken(token) {
|
|
165
|
+
const [h, p, s] = token.split(".");
|
|
166
|
+
if (!h || !p || !s) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return [h, p, s];
|
|
14
170
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
171
|
+
// ─── JWT decode ───────────────────────────────────────────────────────────────
|
|
172
|
+
/**
|
|
173
|
+
* Decodes a compact-serialised JWT into its header and payload **without**
|
|
174
|
+
* verifying the signature.
|
|
175
|
+
*
|
|
176
|
+
* **Do not trust the result of this function alone.** Use it only as the first
|
|
177
|
+
* step of a full verification pipeline — the signature check in {@link verifyJwt}
|
|
178
|
+
* must follow before any claim can be trusted.
|
|
179
|
+
*
|
|
180
|
+
* @param token - A compact JWT string (`header.payload.signature`).
|
|
181
|
+
* @returns The decoded {@link DecodedJwt}.
|
|
182
|
+
* @throws {TypeError} When `token` has fewer than three dot-separated segments.
|
|
183
|
+
*/
|
|
184
|
+
export function decodeJwt(token) {
|
|
185
|
+
const segments = splitToken(token);
|
|
186
|
+
if (!segments) {
|
|
187
|
+
throw new TypeError(`Invalid JWT: expected at least three dot-separated segments, received ${token.split(".").length}.`);
|
|
188
|
+
}
|
|
189
|
+
const [h, p] = segments;
|
|
190
|
+
return {
|
|
191
|
+
header: JSON.parse(DECODER.decode(base64UrlDecode(h))),
|
|
192
|
+
payload: JSON.parse(DECODER.decode(base64UrlDecode(p))),
|
|
193
|
+
};
|
|
23
194
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ES256: {
|
|
38
|
-
name: "ECDSA",
|
|
39
|
-
namedCurve: "P-256"
|
|
40
|
-
},
|
|
41
|
-
ES384: {
|
|
42
|
-
name: "ECDSA",
|
|
43
|
-
namedCurve: "P-384"
|
|
44
|
-
},
|
|
45
|
-
ES512: {
|
|
46
|
-
name: "ECDSA",
|
|
47
|
-
namedCurve: "P-521"
|
|
48
|
-
}
|
|
49
|
-
}, c = {
|
|
50
|
-
RS256: { name: "RSASSA-PKCS1-v1_5" },
|
|
51
|
-
RS384: { name: "RSASSA-PKCS1-v1_5" },
|
|
52
|
-
RS512: { name: "RSASSA-PKCS1-v1_5" },
|
|
53
|
-
ES256: {
|
|
54
|
-
name: "ECDSA",
|
|
55
|
-
hash: "SHA-256"
|
|
56
|
-
},
|
|
57
|
-
ES384: {
|
|
58
|
-
name: "ECDSA",
|
|
59
|
-
hash: "SHA-384"
|
|
60
|
-
},
|
|
61
|
-
ES512: {
|
|
62
|
-
name: "ECDSA",
|
|
63
|
-
hash: "SHA-512"
|
|
64
|
-
}
|
|
195
|
+
// ─── Algorithm helpers ────────────────────────────────────────────────────────
|
|
196
|
+
/**
|
|
197
|
+
* Web Crypto parameters for importing a public key from JWKS, keyed by JWT
|
|
198
|
+
* `alg` header value. The keys of this object are the only supported algorithms;
|
|
199
|
+
* anything not present throws {@link TypeError}.
|
|
200
|
+
*/
|
|
201
|
+
const IMPORT_PARAMS = {
|
|
202
|
+
RS256: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
203
|
+
RS384: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" },
|
|
204
|
+
RS512: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
|
|
205
|
+
ES256: { name: "ECDSA", namedCurve: "P-256" },
|
|
206
|
+
ES384: { name: "ECDSA", namedCurve: "P-384" },
|
|
207
|
+
ES512: { name: "ECDSA", namedCurve: "P-521" },
|
|
65
208
|
};
|
|
66
|
-
|
|
67
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Web Crypto parameters for verifying a JWT signature, keyed by JWT `alg`
|
|
211
|
+
* header value.
|
|
212
|
+
*/
|
|
213
|
+
const VERIFY_PARAMS = {
|
|
214
|
+
RS256: { name: "RSASSA-PKCS1-v1_5" },
|
|
215
|
+
RS384: { name: "RSASSA-PKCS1-v1_5" },
|
|
216
|
+
RS512: { name: "RSASSA-PKCS1-v1_5" },
|
|
217
|
+
ES256: { name: "ECDSA", hash: "SHA-256" },
|
|
218
|
+
ES384: { name: "ECDSA", hash: "SHA-384" },
|
|
219
|
+
ES512: { name: "ECDSA", hash: "SHA-512" },
|
|
220
|
+
};
|
|
221
|
+
function assertSupportedAlgorithm(alg) {
|
|
222
|
+
if (!Object.hasOwn(IMPORT_PARAMS, alg)) {
|
|
223
|
+
throw new TypeError(`Unsupported JWT algorithm: "${alg}". Supported algorithms are ${Object.keys(IMPORT_PARAMS).join(", ")}.`);
|
|
224
|
+
}
|
|
68
225
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
226
|
+
function resolveImportAlgorithm(alg) {
|
|
227
|
+
assertSupportedAlgorithm(alg);
|
|
228
|
+
return IMPORT_PARAMS[alg];
|
|
71
229
|
}
|
|
72
|
-
function
|
|
73
|
-
|
|
230
|
+
function resolveVerifyAlgorithm(alg) {
|
|
231
|
+
assertSupportedAlgorithm(alg);
|
|
232
|
+
return VERIFY_PARAMS[alg];
|
|
74
233
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
234
|
+
// ─── JWKS fetch ───────────────────────────────────────────────────────────────
|
|
235
|
+
/**
|
|
236
|
+
* Fetches the JWKS from `jwksUri`, imports the key matching `kid`, and caches
|
|
237
|
+
* it for {@link JWKS_TTL_MS} milliseconds.
|
|
238
|
+
*
|
|
239
|
+
* The cache key is `"${jwksUri}:${kid ?? '__default__'}"`. Scoping the key to
|
|
240
|
+
* the full endpoint URL ensures that two issuers that happen to publish keys
|
|
241
|
+
* under the same `kid` string are never confused with one another.
|
|
242
|
+
*
|
|
243
|
+
* Behaviour:
|
|
244
|
+
* - When the cached entry for the `(jwksUri, kid)` pair is still within TTL,
|
|
245
|
+
* it is returned immediately with no network request.
|
|
246
|
+
* - When `kid` is absent, the first key in the JWKS is used and cached under
|
|
247
|
+
* the `"__default__"` sentinel combined with the endpoint URI.
|
|
248
|
+
* - Returns `null` when the JWKS endpoint returns a non-OK status, no matching
|
|
249
|
+
* key is found, or key import fails.
|
|
250
|
+
*
|
|
251
|
+
* @param jwksUri - Full URL of the JWKS endpoint.
|
|
252
|
+
* @param kid - The `kid` header value from the JWT, or `undefined`.
|
|
253
|
+
*/
|
|
254
|
+
async function fetchAndCacheJwks(jwksUri, kid, timeoutMs) {
|
|
255
|
+
const cacheKey = `${jwksUri}:${kid ?? "__default__"}`;
|
|
256
|
+
const cached = jwksCache.get(cacheKey);
|
|
257
|
+
if (cached && Date.now() - cached.fetchedAt < JWKS_TTL_MS) {
|
|
258
|
+
return cached.key;
|
|
259
|
+
}
|
|
260
|
+
const res = await fetch(jwksUri, { signal: AbortSignal.timeout(timeoutMs) });
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const json = (await res.json());
|
|
265
|
+
const jwk = kid ? json.keys.find((k) => k.kid === kid) : json.keys[0];
|
|
266
|
+
if (!jwk) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
// RFC 7517 §4.4 makes 'alg' optional in a JWK — the key material (kty, n/e
|
|
270
|
+
// for RSA; kty/crv/x/y for EC) determines the key type. Fall back to RS256
|
|
271
|
+
// when absent. The actual algorithm enforcement happens in verifyJwt via the
|
|
272
|
+
// JWT header's 'alg' claim, not here.
|
|
273
|
+
const alg = jwk.alg ?? "RS256";
|
|
274
|
+
const cryptoKey = await crypto.subtle.importKey("jwk", jwk, resolveImportAlgorithm(alg), false, [
|
|
275
|
+
"verify",
|
|
276
|
+
]);
|
|
277
|
+
jwksCache.set(cacheKey, { key: cryptoKey, fetchedAt: Date.now() });
|
|
278
|
+
return cryptoKey;
|
|
87
279
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
280
|
+
// ─── Verification ─────────────────────────────────────────────────────────────
|
|
281
|
+
/**
|
|
282
|
+
* Verifies a compact-serialised JWT and returns its validated payload.
|
|
283
|
+
*
|
|
284
|
+
* ## Verification steps (in order)
|
|
285
|
+
*
|
|
286
|
+
* 1. Split the token — returns `null` immediately when fewer than three
|
|
287
|
+
* dot-separated segments are present.
|
|
288
|
+
* 2. Reject if `alg` is not in {@link VerifyJwtOptions.allowedAlgorithms} (when set).
|
|
289
|
+
* 3. Reject if `typ` is not in {@link VerifyJwtOptions.allowedTokenTypes} (when non-empty).
|
|
290
|
+
* 4. Fetch (or retrieve from cache) the matching public key from the JWKS endpoint.
|
|
291
|
+
* 5. Verify the cryptographic signature over `header.payload`.
|
|
292
|
+
* 6. Reject if `iss` is absent or does not equal {@link VerifyJwtOptions.issuerUrl}.
|
|
293
|
+
* 7. Reject if {@link VerifyJwtOptions.audience} is set and `aud` does not contain it.
|
|
294
|
+
* 8. Reject if `exp` is absent or is in the past (minus clock-skew tolerance).
|
|
295
|
+
* 9. Reject if `nbf` is in the future (plus clock-skew tolerance).
|
|
296
|
+
* 10. Reject if `iat` is in the future (plus clock-skew tolerance).
|
|
297
|
+
*
|
|
298
|
+
* Returns `null` on any failure rather than throwing, so callers can treat a
|
|
299
|
+
* bad token identically to a missing one without try/catch at the call site.
|
|
300
|
+
*
|
|
301
|
+
* @param token - The raw compact-serialised JWT string.
|
|
302
|
+
* @param opts - Verification options; see {@link VerifyJwtOptions}.
|
|
303
|
+
* @returns The verified {@link JwtPayload}, or `null` if verification fails.
|
|
304
|
+
*/
|
|
305
|
+
export async function verifyJwt(token, opts) {
|
|
306
|
+
try {
|
|
307
|
+
const { issuerUrl, allowedAlgorithms, clockSkewMs, audience, allowedTokenTypes, jwksTimeoutMs = 5000, } = opts;
|
|
308
|
+
const segments = splitToken(token);
|
|
309
|
+
if (!segments) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
const [rawHeader, rawPayload, rawSig] = segments;
|
|
313
|
+
const header = JSON.parse(DECODER.decode(base64UrlDecode(rawHeader)));
|
|
314
|
+
const payload = JSON.parse(DECODER.decode(base64UrlDecode(rawPayload)));
|
|
315
|
+
// RFC 7515 §4.1.1 requires 'alg' in the JWT header. A token that omits it
|
|
316
|
+
// is structurally invalid — we reject it rather than guessing an algorithm.
|
|
317
|
+
const alg = header.alg;
|
|
318
|
+
if (!alg) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
// Unconditionally reject the 'none' algorithm regardless of the allowlist.
|
|
322
|
+
// 'none' means the token is unsigned; accepting it would allow anyone to
|
|
323
|
+
// forge arbitrary claims without a key. The comparison is case-insensitive
|
|
324
|
+
// so that "None", "NONE", or any other casing variant is also rejected.
|
|
325
|
+
if (alg.toLowerCase() === "none") {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
if (allowedAlgorithms?.length && !allowedAlgorithms.includes(alg)) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
if (allowedTokenTypes.length > 0) {
|
|
332
|
+
const typ = header.typ ?? "";
|
|
333
|
+
if (!allowedTokenTypes.some((t) => t.toLowerCase() === typ.toLowerCase())) {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const kid = header.kid;
|
|
338
|
+
const cryptoKey = await fetchAndCacheJwks(`${issuerUrl}/auth/keys`, kid, jwksTimeoutMs);
|
|
339
|
+
if (!cryptoKey) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const valid = await crypto.subtle.verify(resolveVerifyAlgorithm(alg), cryptoKey, base64UrlDecode(rawSig), ENCODER.encode(`${rawHeader}.${rawPayload}`));
|
|
343
|
+
if (!valid) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
// Strict equality is intentional: the issuer URL must match exactly as
|
|
347
|
+
// configured. We deliberately do not normalise trailing slashes, case, or
|
|
348
|
+
// surrounding whitespace — any difference is either a misconfiguration or
|
|
349
|
+
// a token issued by a different server and must be rejected.
|
|
350
|
+
if (payload.iss !== issuerUrl) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
if (audience !== undefined) {
|
|
354
|
+
const audList = payload.aud !== undefined ? [payload.aud].flat() : [];
|
|
355
|
+
const expectedList = [audience].flat();
|
|
356
|
+
if (!expectedList.some((a) => audList.includes(a))) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const now = Date.now();
|
|
361
|
+
if (payload.exp === undefined || payload.exp * 1000 < now - clockSkewMs) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
if (payload.nbf !== undefined && payload.nbf * 1000 > now + clockSkewMs) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
if (payload.iat !== undefined && payload.iat * 1000 > now + clockSkewMs) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
return payload;
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
// SyntaxError from JSON.parse means the token segments are not JSON —
|
|
374
|
+
// this is an opaque (non-JWT) token. Return null silently; the middleware
|
|
375
|
+
// will fall back to opaque token validation via the backend.
|
|
376
|
+
if (err instanceof SyntaxError) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
// Any other unexpected error (network failure, malformed JWKS, Web Crypto
|
|
380
|
+
// exception) is worth logging so operators can distinguish infrastructure
|
|
381
|
+
// failures from genuine bad tokens.
|
|
382
|
+
console.error("[nextgen] verifyJwt unexpected error:", err);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
109
385
|
}
|
|
110
|
-
//#endregion
|
|
111
|
-
export { e as JWKS_TTL_MS, i as base64UrlDecode, o as decodeJwt, p as verifyJwt };
|