@zitadel/sdk-core 0.1.0-alpha.9 → 1.0.0-alpha.20
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 +51 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -11
- package/dist/jwt.d.ts +18 -0
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +55 -23
- package/dist/middleware.d.ts +139 -0
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +15 -17
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +186 -179
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -6
- package/package.json +21 -2
package/README.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
1
|
# @zitadel/sdk-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The shared runtime contract behind the framework SDKs: JWT verification,
|
|
4
|
+
middleware primitives, runtime resolution, and the session types every
|
|
5
|
+
`@zitadel/sdk-*` package re-exports. Application code usually consumes a
|
|
6
|
+
framework SDK (`@zitadel/sdk-next`, `@zitadel/sdk-nuxt`, or one of the SPA
|
|
7
|
+
SDKs) instead of this package directly — reach for `sdk-core` when you are
|
|
8
|
+
building your own middleware or need the types without a framework wrapper.
|
|
9
|
+
|
|
10
|
+
## Entry points
|
|
11
|
+
|
|
12
|
+
| Import | What it carries |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `@zitadel/sdk-core` | The middleware, JWT, and runtime-resolution exports in one namespace. The `/types` widget contract is **not** re-exported from the root — import it from `@zitadel/sdk-core/types` |
|
|
15
|
+
| `@zitadel/sdk-core/types` | The SPA widget contract (`ZitadelFlowStepDetail` and the other widget event/config/handler/props types the SPA SDKs re-export) |
|
|
16
|
+
| `@zitadel/sdk-core/jwt` | `verifyJwt`, `decodeJwt`, `isJwtShaped`, `JWKS_TTL_MS` |
|
|
17
|
+
| `@zitadel/sdk-core/middleware` | The middleware layer: `NextgenSession`, `AuthResult`, `NextgenMiddlewareOptions`, route matching (`matchesRoutes`), response-header filtering (`filterResponseHeaders`, `HOP_BY_HOP`) |
|
|
18
|
+
|
|
19
|
+
It also exports `resolveZitadelRuntime` / `resolveZitadelRuntimeEnv` and
|
|
20
|
+
`ZitadelRuntimeError` for resolving the runtime environment
|
|
21
|
+
(`development` / `preview` / `production`).
|
|
22
|
+
|
|
23
|
+
## How JWT verification works
|
|
24
|
+
|
|
25
|
+
This is the canonical description of the verification pipeline every SDK
|
|
26
|
+
middleware runs (`verifyJwt`):
|
|
27
|
+
|
|
28
|
+
1. The bearer token from the `Authorization` header is checked first; the
|
|
29
|
+
`__nextgen_session` cookie is the fallback.
|
|
30
|
+
2. The JWT header is decoded to extract `kid` and `alg`.
|
|
31
|
+
3. Tokens with an `alg` not in `allowedAlgorithms` (`RS256`, `ES256` by
|
|
32
|
+
default) are rejected immediately — no JWKS fetch.
|
|
33
|
+
4. Tokens with a `typ` not in `allowedTokenTypes` are rejected immediately.
|
|
34
|
+
5. The public key is fetched from `{url}/auth/keys` (JWKS) using the Web
|
|
35
|
+
Crypto API, with a timeout (`jwksTimeoutMs`), and cached per `kid`
|
|
36
|
+
(`JWKS_TTL_MS`).
|
|
37
|
+
6. The signature is verified **before** any claim checks.
|
|
38
|
+
7. `iss` must be present and equal `url` — tokens without an issuer are
|
|
39
|
+
rejected.
|
|
40
|
+
8. `exp` must be present and in the future (with `clockSkewMs` tolerance) —
|
|
41
|
+
tokens without an expiry are rejected.
|
|
42
|
+
9. `nbf` and `iat` are validated with `clockSkewMs` tolerance when present.
|
|
43
|
+
|
|
44
|
+
Opaque (non-JWT) session tokens are validated against the backend's
|
|
45
|
+
`GET /sessions/me` instead, bounded by `opaqueTokenTimeoutMs`.
|
|
46
|
+
|
|
47
|
+
Framework-specific steps (header tunnelling, `auth()` re-verification) are
|
|
48
|
+
documented in each framework SDK's README on top of this pipeline.
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
TypeScript ≥ 5.0 — the published type definitions re-export with
|
|
53
|
+
`export type *`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export type { NextgenSession, AuthState, UnauthState, AuthResult, NextgenMiddlewareOptions } from "./
|
|
2
|
-
export { HOP_BY_HOP, INTERNAL_HEADERS, matchesRoutes, filterResponseHeaders } from "./middleware.js";
|
|
3
|
-
export { verifyJwt, decodeJwt, base64UrlDecode, JWKS_TTL_MS } from "./jwt.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
|
+
export { verifyJwt, decodeJwt, isJwtShaped, 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";
|
|
6
6
|
export type ZitadelRuntime = {
|
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,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC3F,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,5 +1,5 @@
|
|
|
1
|
-
export { HOP_BY_HOP, INTERNAL_HEADERS, matchesRoutes, filterResponseHeaders } from "./middleware.js";
|
|
2
|
-
export { verifyJwt, decodeJwt, base64UrlDecode, JWKS_TTL_MS } from "./jwt.js";
|
|
1
|
+
export { HOP_BY_HOP, INTERNAL_HEADERS, matchesRoutes, filterResponseHeaders, } from "./middleware.js";
|
|
2
|
+
export { verifyJwt, decodeJwt, isJwtShaped, base64UrlDecode, JWKS_TTL_MS } from "./jwt.js";
|
|
3
3
|
export class ZitadelRuntimeError extends Error {
|
|
4
4
|
code;
|
|
5
5
|
constructor(code, message) {
|
|
@@ -36,15 +36,6 @@ function parseEnvironment(value) {
|
|
|
36
36
|
return value;
|
|
37
37
|
throw new ZitadelRuntimeError("E_ZITADEL_CONFIG", `Unsupported ZITADEL_ENVIRONMENT "${value}".`);
|
|
38
38
|
}
|
|
39
|
-
// @ts-expect-error will be used later
|
|
40
|
-
// oxlint-disable-next-line no-unused-vars
|
|
41
|
-
function requireEnv(env, key) {
|
|
42
|
-
const value = env[key];
|
|
43
|
-
if (!value) {
|
|
44
|
-
throw new ZitadelRuntimeError("E_ZITADEL_CONFIG", `${key} is required for Zitadel runtime.`);
|
|
45
|
-
}
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
39
|
function currentEnv() {
|
|
49
40
|
const scope = globalThis;
|
|
50
41
|
return scope.process?.env ?? {};
|
package/dist/jwt.d.ts
CHANGED
|
@@ -236,6 +236,24 @@ export declare function base64UrlDecode(input: string): Uint8Array<ArrayBuffer>;
|
|
|
236
236
|
* @throws {TypeError} When `token` has fewer than three dot-separated segments.
|
|
237
237
|
*/
|
|
238
238
|
export declare function decodeJwt(token: string): DecodedJwt;
|
|
239
|
+
/**
|
|
240
|
+
* Returns `true` when `token` looks structurally like a signed JWT (JWS) —
|
|
241
|
+
* NOT an encrypted token (JWE).
|
|
242
|
+
*
|
|
243
|
+
* JWS compact: 3 dot-separated segments; header has `alg` but NOT `enc`.
|
|
244
|
+
* JWE compact: 5 dot-separated segments; header has BOTH `alg` and `enc`.
|
|
245
|
+
*
|
|
246
|
+
* The Zitadel backend issues JWE tokens (AES-256-GCM via go-jose
|
|
247
|
+
* `A256GCMKW/A256GCM`), whose compact form is
|
|
248
|
+
* `header.encrypted_key.iv.ciphertext.tag`. The header decodes to JSON with
|
|
249
|
+
* `"alg":"A256GCMKW","enc":"A256GCM"`. Checking for the absence of `enc`
|
|
250
|
+
* correctly identifies signed JWTs without false-positives on JWE tokens.
|
|
251
|
+
*
|
|
252
|
+
* This is a structural check only — not a security check. Use it to route a
|
|
253
|
+
* token to the correct validation path ({@link verifyJwt} for JWS, a backend
|
|
254
|
+
* session lookup for everything else), never to trust one.
|
|
255
|
+
*/
|
|
256
|
+
export declare function isJwtShaped(token: string): boolean;
|
|
239
257
|
/**
|
|
240
258
|
* Verifies a compact-serialised JWT and returns its validated payload.
|
|
241
259
|
*
|
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;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CASlD;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
|
@@ -143,8 +143,8 @@ const ENCODER = new TextEncoder();
|
|
|
143
143
|
* @returns The decoded bytes.
|
|
144
144
|
*/
|
|
145
145
|
export function base64UrlDecode(input) {
|
|
146
|
-
const base64 = input.replace(/-/g,
|
|
147
|
-
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4),
|
|
146
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
147
|
+
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
|
|
148
148
|
const binary = atob(padded);
|
|
149
149
|
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
150
150
|
}
|
|
@@ -162,7 +162,7 @@ export function base64UrlDecode(input) {
|
|
|
162
162
|
* has fewer than three dot-separated segments.
|
|
163
163
|
*/
|
|
164
164
|
function splitToken(token) {
|
|
165
|
-
const [h, p, s] = token.split(
|
|
165
|
+
const [h, p, s] = token.split(".");
|
|
166
166
|
if (!h || !p || !s) {
|
|
167
167
|
return null;
|
|
168
168
|
}
|
|
@@ -184,7 +184,7 @@ function splitToken(token) {
|
|
|
184
184
|
export function decodeJwt(token) {
|
|
185
185
|
const segments = splitToken(token);
|
|
186
186
|
if (!segments) {
|
|
187
|
-
throw new TypeError(`Invalid JWT: expected at least three dot-separated segments, received ${token.split(
|
|
187
|
+
throw new TypeError(`Invalid JWT: expected at least three dot-separated segments, received ${token.split(".").length}.`);
|
|
188
188
|
}
|
|
189
189
|
const [h, p] = segments;
|
|
190
190
|
return {
|
|
@@ -192,6 +192,36 @@ export function decodeJwt(token) {
|
|
|
192
192
|
payload: JSON.parse(DECODER.decode(base64UrlDecode(p))),
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
+
// ─── JWT shape detection ──────────────────────────────────────────────────────
|
|
196
|
+
/**
|
|
197
|
+
* Returns `true` when `token` looks structurally like a signed JWT (JWS) —
|
|
198
|
+
* NOT an encrypted token (JWE).
|
|
199
|
+
*
|
|
200
|
+
* JWS compact: 3 dot-separated segments; header has `alg` but NOT `enc`.
|
|
201
|
+
* JWE compact: 5 dot-separated segments; header has BOTH `alg` and `enc`.
|
|
202
|
+
*
|
|
203
|
+
* The Zitadel backend issues JWE tokens (AES-256-GCM via go-jose
|
|
204
|
+
* `A256GCMKW/A256GCM`), whose compact form is
|
|
205
|
+
* `header.encrypted_key.iv.ciphertext.tag`. The header decodes to JSON with
|
|
206
|
+
* `"alg":"A256GCMKW","enc":"A256GCM"`. Checking for the absence of `enc`
|
|
207
|
+
* correctly identifies signed JWTs without false-positives on JWE tokens.
|
|
208
|
+
*
|
|
209
|
+
* This is a structural check only — not a security check. Use it to route a
|
|
210
|
+
* token to the correct validation path ({@link verifyJwt} for JWS, a backend
|
|
211
|
+
* session lookup for everything else), never to trust one.
|
|
212
|
+
*/
|
|
213
|
+
export function isJwtShaped(token) {
|
|
214
|
+
const parts = token.split(".");
|
|
215
|
+
if (parts.length < 3 || !parts[0])
|
|
216
|
+
return false;
|
|
217
|
+
try {
|
|
218
|
+
const header = JSON.parse(DECODER.decode(base64UrlDecode(parts[0])));
|
|
219
|
+
return typeof header?.alg === "string" && !("enc" in header);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
195
225
|
// ─── Algorithm helpers ────────────────────────────────────────────────────────
|
|
196
226
|
/**
|
|
197
227
|
* Web Crypto parameters for importing a public key from JWKS, keyed by JWT
|
|
@@ -199,28 +229,28 @@ export function decodeJwt(token) {
|
|
|
199
229
|
* anything not present throws {@link TypeError}.
|
|
200
230
|
*/
|
|
201
231
|
const IMPORT_PARAMS = {
|
|
202
|
-
RS256: { name:
|
|
203
|
-
RS384: { name:
|
|
204
|
-
RS512: { name:
|
|
205
|
-
ES256: { name:
|
|
206
|
-
ES384: { name:
|
|
207
|
-
ES512: { name:
|
|
232
|
+
RS256: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
233
|
+
RS384: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-384" },
|
|
234
|
+
RS512: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-512" },
|
|
235
|
+
ES256: { name: "ECDSA", namedCurve: "P-256" },
|
|
236
|
+
ES384: { name: "ECDSA", namedCurve: "P-384" },
|
|
237
|
+
ES512: { name: "ECDSA", namedCurve: "P-521" },
|
|
208
238
|
};
|
|
209
239
|
/**
|
|
210
240
|
* Web Crypto parameters for verifying a JWT signature, keyed by JWT `alg`
|
|
211
241
|
* header value.
|
|
212
242
|
*/
|
|
213
243
|
const VERIFY_PARAMS = {
|
|
214
|
-
RS256: { name:
|
|
215
|
-
RS384: { name:
|
|
216
|
-
RS512: { name:
|
|
217
|
-
ES256: { name:
|
|
218
|
-
ES384: { name:
|
|
219
|
-
ES512: { name:
|
|
244
|
+
RS256: { name: "RSASSA-PKCS1-v1_5" },
|
|
245
|
+
RS384: { name: "RSASSA-PKCS1-v1_5" },
|
|
246
|
+
RS512: { name: "RSASSA-PKCS1-v1_5" },
|
|
247
|
+
ES256: { name: "ECDSA", hash: "SHA-256" },
|
|
248
|
+
ES384: { name: "ECDSA", hash: "SHA-384" },
|
|
249
|
+
ES512: { name: "ECDSA", hash: "SHA-512" },
|
|
220
250
|
};
|
|
221
251
|
function assertSupportedAlgorithm(alg) {
|
|
222
252
|
if (!Object.hasOwn(IMPORT_PARAMS, alg)) {
|
|
223
|
-
throw new TypeError(`Unsupported JWT algorithm: "${alg}". Supported algorithms are ${Object.keys(IMPORT_PARAMS).join(
|
|
253
|
+
throw new TypeError(`Unsupported JWT algorithm: "${alg}". Supported algorithms are ${Object.keys(IMPORT_PARAMS).join(", ")}.`);
|
|
224
254
|
}
|
|
225
255
|
}
|
|
226
256
|
function resolveImportAlgorithm(alg) {
|
|
@@ -252,7 +282,7 @@ function resolveVerifyAlgorithm(alg) {
|
|
|
252
282
|
* @param kid - The `kid` header value from the JWT, or `undefined`.
|
|
253
283
|
*/
|
|
254
284
|
async function fetchAndCacheJwks(jwksUri, kid, timeoutMs) {
|
|
255
|
-
const cacheKey = `${jwksUri}:${kid ??
|
|
285
|
+
const cacheKey = `${jwksUri}:${kid ?? "__default__"}`;
|
|
256
286
|
const cached = jwksCache.get(cacheKey);
|
|
257
287
|
if (cached && Date.now() - cached.fetchedAt < JWKS_TTL_MS) {
|
|
258
288
|
return cached.key;
|
|
@@ -270,8 +300,10 @@ async function fetchAndCacheJwks(jwksUri, kid, timeoutMs) {
|
|
|
270
300
|
// for RSA; kty/crv/x/y for EC) determines the key type. Fall back to RS256
|
|
271
301
|
// when absent. The actual algorithm enforcement happens in verifyJwt via the
|
|
272
302
|
// JWT header's 'alg' claim, not here.
|
|
273
|
-
const alg = jwk.alg ??
|
|
274
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
303
|
+
const alg = jwk.alg ?? "RS256";
|
|
304
|
+
const cryptoKey = await crypto.subtle.importKey("jwk", jwk, resolveImportAlgorithm(alg), false, [
|
|
305
|
+
"verify",
|
|
306
|
+
]);
|
|
275
307
|
jwksCache.set(cacheKey, { key: cryptoKey, fetchedAt: Date.now() });
|
|
276
308
|
return cryptoKey;
|
|
277
309
|
}
|
|
@@ -320,14 +352,14 @@ export async function verifyJwt(token, opts) {
|
|
|
320
352
|
// 'none' means the token is unsigned; accepting it would allow anyone to
|
|
321
353
|
// forge arbitrary claims without a key. The comparison is case-insensitive
|
|
322
354
|
// so that "None", "NONE", or any other casing variant is also rejected.
|
|
323
|
-
if (alg.toLowerCase() ===
|
|
355
|
+
if (alg.toLowerCase() === "none") {
|
|
324
356
|
return null;
|
|
325
357
|
}
|
|
326
358
|
if (allowedAlgorithms?.length && !allowedAlgorithms.includes(alg)) {
|
|
327
359
|
return null;
|
|
328
360
|
}
|
|
329
361
|
if (allowedTokenTypes.length > 0) {
|
|
330
|
-
const typ = header.typ ??
|
|
362
|
+
const typ = header.typ ?? "";
|
|
331
363
|
if (!allowedTokenTypes.some((t) => t.toLowerCase() === typ.toLowerCase())) {
|
|
332
364
|
return null;
|
|
333
365
|
}
|
|
@@ -377,7 +409,7 @@ export async function verifyJwt(token, opts) {
|
|
|
377
409
|
// Any other unexpected error (network failure, malformed JWKS, Web Crypto
|
|
378
410
|
// exception) is worth logging so operators can distinguish infrastructure
|
|
379
411
|
// failures from genuine bad tokens.
|
|
380
|
-
console.error(
|
|
412
|
+
console.error("[nextgen] verifyJwt unexpected error:", err);
|
|
381
413
|
return null;
|
|
382
414
|
}
|
|
383
415
|
}
|
package/dist/middleware.d.ts
CHANGED
|
@@ -36,4 +36,143 @@ export declare function matchesRoutes(pathname: string, routes: readonly string[
|
|
|
36
36
|
* @returns A new `Headers` object with filtered headers.
|
|
37
37
|
*/
|
|
38
38
|
export declare function filterResponseHeaders(upstream: Headers): Headers;
|
|
39
|
+
/**
|
|
40
|
+
* The authenticated session for a signed-in user.
|
|
41
|
+
*/
|
|
42
|
+
export type NextgenSession = {
|
|
43
|
+
/** The user's unique identifier (`sub` claim). */
|
|
44
|
+
userId: string;
|
|
45
|
+
/** The user's email address, or `null` if not present in the token. */
|
|
46
|
+
email: string | null;
|
|
47
|
+
/** The user's display name, or `null` if not present in the token. */
|
|
48
|
+
name: string | null;
|
|
49
|
+
/** The raw verified JWT. */
|
|
50
|
+
token: string;
|
|
51
|
+
};
|
|
52
|
+
/** Auth state when the user is signed in. */
|
|
53
|
+
export type AuthState = {
|
|
54
|
+
isAuthenticated: true;
|
|
55
|
+
session: NextgenSession;
|
|
56
|
+
};
|
|
57
|
+
/** Auth state when the user is not signed in. */
|
|
58
|
+
export type UnauthState = {
|
|
59
|
+
isAuthenticated: false;
|
|
60
|
+
session: null;
|
|
61
|
+
};
|
|
62
|
+
/** Union of all possible auth states. */
|
|
63
|
+
export type AuthResult = AuthState | UnauthState;
|
|
64
|
+
/**
|
|
65
|
+
* The client-safe session exposed to app UI (headers, account menus).
|
|
66
|
+
* Identical to {@link NextgenSession} but omits `token` — the raw session
|
|
67
|
+
* token must never reach client-side JavaScript, whether through an SSR
|
|
68
|
+
* payload or a client-side fetch result.
|
|
69
|
+
*/
|
|
70
|
+
export type ClientSession = {
|
|
71
|
+
/** The user's unique identifier (`sub` claim). */
|
|
72
|
+
userId: string;
|
|
73
|
+
/** The user's email address, or `null` if not present. */
|
|
74
|
+
email: string | null;
|
|
75
|
+
/** The user's display name, or `null` if not present. */
|
|
76
|
+
name: string | null;
|
|
77
|
+
};
|
|
78
|
+
/** Client-safe auth state when the user is signed in. */
|
|
79
|
+
export type ClientAuthState = {
|
|
80
|
+
isAuthenticated: true;
|
|
81
|
+
session: ClientSession;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Union of all possible client-safe auth states. Returned by the client
|
|
85
|
+
* session reads (`useAuth()` in sdk-nuxt, `getSession()` in sdk-next).
|
|
86
|
+
* Token is intentionally absent — use the server-side helpers when the raw
|
|
87
|
+
* token is needed.
|
|
88
|
+
*/
|
|
89
|
+
export type ClientAuthResult = ClientAuthState | UnauthState;
|
|
90
|
+
/**
|
|
91
|
+
* Options passed to the SDK middleware factory (`nextgenMiddleware` in
|
|
92
|
+
* sdk-next, `createNextgenMiddleware` in sdk-nuxt).
|
|
93
|
+
*/
|
|
94
|
+
export type NextgenMiddlewareOptions = {
|
|
95
|
+
/**
|
|
96
|
+
* Full URL of the Zitadel auth backend.
|
|
97
|
+
* @default process.env.ZITADEL_URL ?? "http://localhost:8080"
|
|
98
|
+
*/
|
|
99
|
+
url?: string;
|
|
100
|
+
/**
|
|
101
|
+
* URL path prefix that is reverse-proxied to the auth backend.
|
|
102
|
+
* @default "/__nextgen"
|
|
103
|
+
*/
|
|
104
|
+
proxyPath?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Pathnames that require a valid session. Requests to these routes without
|
|
107
|
+
* a valid token are redirected to {@link loginPath}.
|
|
108
|
+
* Entries ending with `*` match any sub-path (e.g. `"/admin*"`).
|
|
109
|
+
* @default []
|
|
110
|
+
*/
|
|
111
|
+
protectedRoutes?: string[];
|
|
112
|
+
/**
|
|
113
|
+
* Pathnames that are completely skipped by the middleware — no JWT
|
|
114
|
+
* verification, no protection check, no header tunnelling.
|
|
115
|
+
* Useful for webhooks, health-check endpoints, or any route where
|
|
116
|
+
* running auth logic is undesirable.
|
|
117
|
+
* Entries ending with `*` match any sub-path (e.g. `"/public*"`).
|
|
118
|
+
* @default []
|
|
119
|
+
*/
|
|
120
|
+
ignoredRoutes?: string[];
|
|
121
|
+
/**
|
|
122
|
+
* Pathname to redirect unauthenticated users to.
|
|
123
|
+
* A `next` query parameter is appended with the originally requested path.
|
|
124
|
+
* @default "/login"
|
|
125
|
+
*/
|
|
126
|
+
loginPath?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Reserved for future use. Local PEM public key for offline JWT verification.
|
|
129
|
+
*/
|
|
130
|
+
jwtKey?: string;
|
|
131
|
+
/**
|
|
132
|
+
* Restrict accepted JWT `alg` header values. Tokens whose algorithm is not
|
|
133
|
+
* in this list are rejected before JWKS is fetched.
|
|
134
|
+
* @default ["RS256", "ES256"]
|
|
135
|
+
*/
|
|
136
|
+
allowedAlgorithms?: string[];
|
|
137
|
+
/**
|
|
138
|
+
* Clock skew tolerance in milliseconds applied to `exp`, `nbf`, and `iat`
|
|
139
|
+
* claim validation.
|
|
140
|
+
* @default 5000
|
|
141
|
+
*/
|
|
142
|
+
clockSkewMs?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Expected value(s) of the `aud` claim. When omitted, audience is not
|
|
145
|
+
* validated. Pass a single string or an array when a token may carry
|
|
146
|
+
* multiple audiences.
|
|
147
|
+
*/
|
|
148
|
+
audience?: string | string[];
|
|
149
|
+
/**
|
|
150
|
+
* Accepted values for the JWT header `typ` claim (case-insensitive).
|
|
151
|
+
* Tokens whose `typ` is not in this list are rejected.
|
|
152
|
+
* Set to `[]` to disable token-type checking entirely.
|
|
153
|
+
* @default ["JWT", "at+JWT"]
|
|
154
|
+
*/
|
|
155
|
+
allowedTokenTypes?: string[];
|
|
156
|
+
/**
|
|
157
|
+
* Timeout in milliseconds for JWKS endpoint requests.
|
|
158
|
+
* Requests that do not complete within this window are aborted and the
|
|
159
|
+
* token is rejected, treating the request as unauthenticated.
|
|
160
|
+
* @default 5000
|
|
161
|
+
*/
|
|
162
|
+
jwksTimeoutMs?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Timeout in milliseconds for upstream proxy requests.
|
|
165
|
+
* Requests that do not complete within this window are aborted with a
|
|
166
|
+
* network error. Defaults to 5 000 ms.
|
|
167
|
+
* @default 5000
|
|
168
|
+
*/
|
|
169
|
+
proxyTimeoutMs?: number;
|
|
170
|
+
/**
|
|
171
|
+
* Timeout in milliseconds for opaque (non-JWT) session token validation
|
|
172
|
+
* via the backend's `GET /sessions/me` endpoint. Tokens that cannot be
|
|
173
|
+
* validated within this window are treated as invalid.
|
|
174
|
+
* @default 5000
|
|
175
|
+
*/
|
|
176
|
+
opaqueTokenTimeoutMs?: number;
|
|
177
|
+
};
|
|
39
178
|
//# sourceMappingURL=middleware.d.ts.map
|
package/dist/middleware.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,WAAW,CAAC,MAAM,CAazC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,WAAW,CAAC,MAAM,CAazC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAAqC,CAAC;AAIvF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAUlF;AAID;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAYhE;AAID;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,sEAAsE;IACtE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG;IAAE,eAAe,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,CAAC;AAE3E,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG;IAAE,eAAe,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,CAAC;AAEpE,yCAAyC;AACzC,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,yDAAyD;IACzD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,CAAC;AAEF,yDAAyD;AACzD,MAAM,MAAM,eAAe,GAAG;IAAE,eAAe,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC;AAEhF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,eAAe,GAAG,WAAW,CAAC;AAE7D;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAE3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE7B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE7B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC"}
|
package/dist/middleware.js
CHANGED
|
@@ -11,27 +11,25 @@
|
|
|
11
11
|
* two directly connected peers and become invalid when proxied.
|
|
12
12
|
*/
|
|
13
13
|
export const HOP_BY_HOP = new Set([
|
|
14
|
-
|
|
14
|
+
"connection",
|
|
15
15
|
// host is not a hop-by-hop header per RFC 7230, but it must be stripped so
|
|
16
16
|
// the fetch implementation derives the correct Host from the upstream URL
|
|
17
17
|
// rather than forwarding the client's Host and causing SNI/vhost mismatches.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
"host",
|
|
19
|
+
"keep-alive",
|
|
20
|
+
"proxy-authenticate",
|
|
21
|
+
"proxy-authorization",
|
|
22
|
+
"te",
|
|
23
|
+
"trailer",
|
|
24
|
+
"transfer-encoding",
|
|
25
|
+
"upgrade",
|
|
26
26
|
]);
|
|
27
27
|
/**
|
|
28
28
|
* SDK-internal headers that must never be forwarded to the upstream backend.
|
|
29
29
|
* These headers carry session data between the middleware and server components;
|
|
30
30
|
* forwarding them upstream would expose internal state and allow header injection.
|
|
31
31
|
*/
|
|
32
|
-
export const INTERNAL_HEADERS = new Set([
|
|
33
|
-
'x-nextgen-auth-token',
|
|
34
|
-
]);
|
|
32
|
+
export const INTERNAL_HEADERS = new Set(["x-nextgen-auth-token"]);
|
|
35
33
|
// ─── Route matching ──────────────────────────────────────────────────────────
|
|
36
34
|
/**
|
|
37
35
|
* Returns `true` when `pathname` matches at least one entry in `routes`.
|
|
@@ -48,7 +46,7 @@ export function matchesRoutes(pathname, routes) {
|
|
|
48
46
|
return false;
|
|
49
47
|
}
|
|
50
48
|
return routes.some((pattern) => {
|
|
51
|
-
if (pattern.endsWith(
|
|
49
|
+
if (pattern.endsWith("*")) {
|
|
52
50
|
return pathname.startsWith(pattern.slice(0, -1));
|
|
53
51
|
}
|
|
54
52
|
return pathname === pattern;
|
|
@@ -65,12 +63,12 @@ export function matchesRoutes(pathname, routes) {
|
|
|
65
63
|
*/
|
|
66
64
|
export function filterResponseHeaders(upstream) {
|
|
67
65
|
const filtered = new Headers();
|
|
68
|
-
|
|
66
|
+
upstream.forEach((value, key) => {
|
|
69
67
|
if (!HOP_BY_HOP.has(key.toLowerCase()) &&
|
|
70
|
-
key.toLowerCase() !==
|
|
71
|
-
key.toLowerCase() !==
|
|
68
|
+
key.toLowerCase() !== "set-cookie" &&
|
|
69
|
+
key.toLowerCase() !== "location") {
|
|
72
70
|
filtered.set(key, value);
|
|
73
71
|
}
|
|
74
|
-
}
|
|
72
|
+
});
|
|
75
73
|
return filtered;
|
|
76
74
|
}
|