@zitadel/sdk-core 0.1.0-alpha.8 → 0.1.0
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 +106 -1
- 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} +115 -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 +310 -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 +29 -12
- package/dist/index.d.ts +0 -22
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -51
- package/dist/jwt.d.ts.map +0 -1
- package/dist/jwt.js +0 -383
- package/dist/middleware.d.ts +0 -39
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js +0 -76
- package/dist/tsconfig.lib.tsbuildinfo +0 -1
- package/dist/types.d.ts +0 -206
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -8
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { JwtPayload } from "./jwt.mjs";
|
|
2
|
+
//#region src/middleware.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Headers that must never be forwarded to an upstream service.
|
|
5
|
+
* These are connection-level headers that are meaningful only between
|
|
6
|
+
* two directly connected peers and become invalid when proxied.
|
|
7
|
+
*/
|
|
8
|
+
export declare const HOP_BY_HOP: ReadonlySet<string>;
|
|
9
|
+
/**
|
|
10
|
+
* SDK-internal headers that must never be forwarded to the upstream backend.
|
|
11
|
+
* These headers carry session data between the middleware and server components;
|
|
12
|
+
* forwarding them upstream would expose internal state and allow header injection.
|
|
13
|
+
*/
|
|
14
|
+
export declare const INTERNAL_HEADERS: ReadonlySet<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Returns `true` when `pathname` matches at least one entry in `routes`.
|
|
17
|
+
*
|
|
18
|
+
* An entry ending with `*` matches any path that starts with the prefix
|
|
19
|
+
* before the `*`. All other entries match exactly, ignoring a trailing slash
|
|
20
|
+
* on either side: `/profile` and `/profile/` name the same route, and several
|
|
21
|
+
* frameworks (Qwik Router by default) serve the slashed form as canonical. An
|
|
22
|
+
* exact compare would let `/profile/` past a `protectedRoutes: ["/profile"]`.
|
|
23
|
+
*
|
|
24
|
+
* @param pathname - The URL pathname to test.
|
|
25
|
+
* @param routes - The list of route patterns to match against.
|
|
26
|
+
* @returns `true` if at least one pattern matches.
|
|
27
|
+
*/
|
|
28
|
+
export declare function matchesRoutes(pathname: string, routes: readonly string[]): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Where a signed-out request for a protected route is sent: `loginPath`, with
|
|
31
|
+
* the requested path and query string in `next`, so `<zitadel-login>` returns
|
|
32
|
+
* to the page as it was asked for, filters and tabs included. The fragment
|
|
33
|
+
* never reaches the server, so it cannot be carried.
|
|
34
|
+
*
|
|
35
|
+
* A `next` the request carries itself is left out, so a redirect never nests
|
|
36
|
+
* an earlier one: should the login page be protected by mistake, each pass of
|
|
37
|
+
* the redirect loop would otherwise wrap the last and grow the URL. The rest
|
|
38
|
+
* of the query keeps the encoding it was sent with.
|
|
39
|
+
*
|
|
40
|
+
* Whatever query `loginPath` has of its own is kept, except a `next`, which
|
|
41
|
+
* this replaces.
|
|
42
|
+
*
|
|
43
|
+
* @param loginPath - The login page, a path starting with a single `/`.
|
|
44
|
+
* @param requestUrl - The full URL of the signed-out request.
|
|
45
|
+
* @returns The URL to redirect to, on the request's origin.
|
|
46
|
+
*/
|
|
47
|
+
export declare function loginRedirectUrl(loginPath: string, requestUrl: string | URL): URL;
|
|
48
|
+
/**
|
|
49
|
+
* Whether a proxied call may carry the confidential project secret.
|
|
50
|
+
*
|
|
51
|
+
* ADR 036: the browser flow needs the secret only for the handoff exchange
|
|
52
|
+
* (`POST /sessions/exchange`). Stamping it onto every proxied operation would
|
|
53
|
+
* turn the app's public `/__nextgen/*` boundary into an open relay carrying
|
|
54
|
+
* project-level authority for any anonymous caller.
|
|
55
|
+
*
|
|
56
|
+
* @param method - The proxied request's HTTP method.
|
|
57
|
+
* @param pathname - The path after the proxy prefix, e.g. `/sessions/exchange`.
|
|
58
|
+
* @returns `true` only for the handoff exchange.
|
|
59
|
+
*/
|
|
60
|
+
export declare function requiresProjectSecret(method: string, pathname: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Filters upstream response headers for proxying: strips hop-by-hop headers
|
|
63
|
+
* and `set-cookie` (handled separately via `getSetCookie()`), and reduces
|
|
64
|
+
* `location` to a redirect that can only land on this app.
|
|
65
|
+
*
|
|
66
|
+
* `location` was stripped outright until the identity-provider callback
|
|
67
|
+
* needed it: the provider returns the browser to `/__nextgen/idp/{slug}/callback`,
|
|
68
|
+
* and the engine answers `303` back to the page the sign-in started on. That
|
|
69
|
+
* is a top-level navigation with no JavaScript in the loop, so a dropped
|
|
70
|
+
* `Location` leaves the browser on an empty page. Outbound redirects to a
|
|
71
|
+
* provider do not travel this way -- they arrive as `step.redirect_url` in a
|
|
72
|
+
* JSON body and the widget navigates -- so nothing legitimate points off-origin.
|
|
73
|
+
*
|
|
74
|
+
* @param upstream - The upstream response headers.
|
|
75
|
+
* @param selfUrl - The app's own request URL, which is both the origin a
|
|
76
|
+
* redirect must land on and the base a relative one resolves against.
|
|
77
|
+
* Omitting it keeps the old behaviour of dropping every redirect.
|
|
78
|
+
* @returns A new `Headers` object with filtered headers.
|
|
79
|
+
*/
|
|
80
|
+
export declare function filterResponseHeaders(upstream: Headers, selfUrl?: string): Headers;
|
|
81
|
+
/** The cookie the server sets a session in and reads it from. */
|
|
82
|
+
export declare const SERVER_SESSION_COOKIE = "__nextgen_session";
|
|
83
|
+
/**
|
|
84
|
+
* The cookie an app keeps its project's session in: the server's name with the
|
|
85
|
+
* project id appended (ADR 073). A browser shares one cookie jar across every
|
|
86
|
+
* port of a host, so under the server's name alone the Console of a local
|
|
87
|
+
* server and every app on `localhost` would hold their sessions in one slot and
|
|
88
|
+
* replace each other's.
|
|
89
|
+
*
|
|
90
|
+
* Every byte of the id outside `[A-Za-z0-9_-]` is percent-encoded, so any
|
|
91
|
+
* configured id makes a valid cookie name. Ids the server mints need none.
|
|
92
|
+
*/
|
|
93
|
+
export declare function sessionCookieName(projectId: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* A session cookie a response sets. An empty `value` with `maxAge: 0` clears
|
|
96
|
+
* it.
|
|
97
|
+
*/
|
|
98
|
+
export interface SessionCookie {
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly value: string;
|
|
101
|
+
/** Seconds the browser keeps it, or `undefined` for the browser session. */
|
|
102
|
+
readonly maxAge: number | undefined;
|
|
103
|
+
readonly secure: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The `Set-Cookie` value for `cookie`, with the attributes the server gives its
|
|
107
|
+
* own session cookie: `Path=/`, `HttpOnly`, `SameSite=Lax`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function serializeSessionCookie(cookie: SessionCookie): string;
|
|
110
|
+
/**
|
|
111
|
+
* The `Cache-Control` of a response that sets a session cookie, as the server
|
|
112
|
+
* sends on its session answers: a shared cache must not hand the cookie, the
|
|
113
|
+
* session token it carries or its clearing, to the next visitor.
|
|
114
|
+
*/
|
|
115
|
+
export declare const SESSION_COOKIE_CACHE_CONTROL = "private, no-store";
|
|
116
|
+
/** The browser's request as the app sees it, and as a proxy in front of it forwarded it. */
|
|
117
|
+
export interface BrowserRequest {
|
|
118
|
+
/** The app's own request URL. */
|
|
119
|
+
readonly requestUrl: string | URL;
|
|
120
|
+
/** The request's `X-Forwarded-Proto`, or `null`. */
|
|
121
|
+
readonly forwardedProto?: string | null;
|
|
122
|
+
/** The request's `X-Forwarded-Host`, or `null`. */
|
|
123
|
+
readonly forwardedHost?: string | null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* What a proxied request carries that decides its session cookies. The
|
|
127
|
+
* request URL and the forwarded headers decide whether a cookie needs `Secure`.
|
|
128
|
+
*/
|
|
129
|
+
export interface ProxiedRequest extends BrowserRequest {
|
|
130
|
+
readonly method: string;
|
|
131
|
+
/** The path after the proxy prefix, e.g. `/sessions/me`. */
|
|
132
|
+
readonly path: string;
|
|
133
|
+
/** The query string, with its leading `?`, or `""`. */
|
|
134
|
+
readonly search: string;
|
|
135
|
+
/** The request's `Cookie` header, or `null`. */
|
|
136
|
+
readonly cookie: string | null;
|
|
137
|
+
}
|
|
138
|
+
/** The server a proxy forwards to, for the calls it makes of its own. */
|
|
139
|
+
export interface ProxyUpstream {
|
|
140
|
+
/** Base URL of the auth backend. */
|
|
141
|
+
readonly url: string;
|
|
142
|
+
/** How long to wait for an answer. */
|
|
143
|
+
readonly timeoutMs: number;
|
|
144
|
+
}
|
|
145
|
+
/** What a proxied answer sends the browser. */
|
|
146
|
+
export interface ProxiedAnswer {
|
|
147
|
+
readonly setCookies: string[];
|
|
148
|
+
readonly body: ReadableStream<Uint8Array> | string | null;
|
|
149
|
+
}
|
|
150
|
+
/** A proxied call's session cookies, on the way up and on the way back. */
|
|
151
|
+
export interface ProxiedSessionCookies {
|
|
152
|
+
/** The `Cookie` header to send upstream, or `null` to send none. */
|
|
153
|
+
readonly upstreamCookie: string | null;
|
|
154
|
+
/**
|
|
155
|
+
* The upstream answer's way back to the browser: the `Set-Cookie` values and
|
|
156
|
+
* the body to send. Send this body, never the upstream response's own, which
|
|
157
|
+
* this may have read.
|
|
158
|
+
*/
|
|
159
|
+
toBrowser(upstream: Response): Promise<ProxiedAnswer>;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The session cookies of a call the app proxies to the server (ADR 073). The
|
|
163
|
+
* server knows one session cookie, `__nextgen_session`; the app keeps its
|
|
164
|
+
* project's session in {@link sessionCookieName}, so it neither replaces nor
|
|
165
|
+
* reads the session of another app or of the Console on the same host.
|
|
166
|
+
*
|
|
167
|
+
* - On the way up, the app's own session cookie goes under the server's name,
|
|
168
|
+
* and every other session cookie stays behind. Other cookies pass unchanged.
|
|
169
|
+
* - On the way back, a `Set-Cookie` of the server's session cookie, which sets
|
|
170
|
+
* or clears it, is renamed to the app's.
|
|
171
|
+
* - A session kept under the server's name goes up to one call only, the
|
|
172
|
+
* session probe `GET /sessions/me?project_id=<project>`, which the server
|
|
173
|
+
* refuses for another project's session. When the answer signs a user of
|
|
174
|
+
* this project in, the session is copied to the app's cookie; the server's
|
|
175
|
+
* cookie keeps it, since the Console may share the project.
|
|
176
|
+
* - When the app signs a user in (`POST /sessions/exchange`) or out
|
|
177
|
+
* (`DELETE /sessions/me`), a session still kept under the server's name is
|
|
178
|
+
* retired, or it would sign its user back in once the app's own cookie is
|
|
179
|
+
* gone: the server is asked about it in this project, and a session of this
|
|
180
|
+
* project is cleared (and on sign-out also ended), a dead one cleared, and
|
|
181
|
+
* one of another project, the Console's on `localhost`, left alone.
|
|
182
|
+
* - Without a project the cookies pass as they are: nothing says which session
|
|
183
|
+
* is the app's.
|
|
184
|
+
*
|
|
185
|
+
* @param request - The proxied request.
|
|
186
|
+
* @param projectId - The project this app signs its users in to.
|
|
187
|
+
* @param upstream - The server the proxy forwards to, which it asks about a
|
|
188
|
+
* session kept under the server's name.
|
|
189
|
+
*/
|
|
190
|
+
export declare function proxySessionCookies(request: ProxiedRequest, projectId: string | undefined, upstream: ProxyUpstream): ProxiedSessionCookies;
|
|
191
|
+
/**
|
|
192
|
+
* The authenticated session for a signed-in user. Identity follows the
|
|
193
|
+
* user-ref vocabulary (ADR 058): render `display`, falling back to
|
|
194
|
+
* `identifier`, then `userId`.
|
|
195
|
+
*/
|
|
196
|
+
export type NextgenSession = {
|
|
197
|
+
/** The user's unique identifier (`sub` claim). */
|
|
198
|
+
userId: string;
|
|
199
|
+
/**
|
|
200
|
+
* The user schema's designated identifier value (the login identifier,
|
|
201
|
+
* e.g. an email address or username), or `null` when unresolved. On the
|
|
202
|
+
* JWT path this carries the `email` claim.
|
|
203
|
+
*/
|
|
204
|
+
identifier: string | null;
|
|
205
|
+
/**
|
|
206
|
+
* The schema property `identifier` came from (e.g. `"email"`), or `null`
|
|
207
|
+
* when unknown — JWT-claim identities carry no property attribution.
|
|
208
|
+
*/
|
|
209
|
+
identifierProperty: string | null;
|
|
210
|
+
/** The user's display name rendering, or `null` when the schema designates none. */
|
|
211
|
+
display: string | null;
|
|
212
|
+
/** The raw verified JWT. */
|
|
213
|
+
token: string;
|
|
214
|
+
};
|
|
215
|
+
/** Auth state when the user is signed in. */
|
|
216
|
+
export type AuthState = {
|
|
217
|
+
isAuthenticated: true;
|
|
218
|
+
session: NextgenSession;
|
|
219
|
+
};
|
|
220
|
+
/** Auth state when the user is not signed in. */
|
|
221
|
+
export type UnauthState = {
|
|
222
|
+
isAuthenticated: false;
|
|
223
|
+
session: null;
|
|
224
|
+
};
|
|
225
|
+
/** Union of all possible auth states. */
|
|
226
|
+
export type AuthResult = AuthState | UnauthState;
|
|
227
|
+
/**
|
|
228
|
+
* The client-safe session exposed to app UI (headers, account menus).
|
|
229
|
+
* Identical to {@link NextgenSession} but omits `token` — the raw session
|
|
230
|
+
* token must never reach client-side JavaScript, whether through an SSR
|
|
231
|
+
* payload or a client-side fetch result.
|
|
232
|
+
*/
|
|
233
|
+
export type ClientSession = {
|
|
234
|
+
/** The user's unique identifier (`sub` claim). */
|
|
235
|
+
userId: string;
|
|
236
|
+
/** The designated identifier value (login identifier), or `null`. */
|
|
237
|
+
identifier: string | null;
|
|
238
|
+
/** The schema property the identifier came from, or `null`. */
|
|
239
|
+
identifierProperty: string | null;
|
|
240
|
+
/** The display name rendering, or `null`. */
|
|
241
|
+
display: string | null;
|
|
242
|
+
};
|
|
243
|
+
/** Client-safe auth state when the user is signed in. */
|
|
244
|
+
export type ClientAuthState = {
|
|
245
|
+
isAuthenticated: true;
|
|
246
|
+
session: ClientSession;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* Union of all possible client-safe auth states. Returned by the client
|
|
250
|
+
* session reads (`useAuth()` in sdk-nuxt, `getSession()` in sdk-next).
|
|
251
|
+
* Token is intentionally absent — use the server-side helpers when the raw
|
|
252
|
+
* token is needed.
|
|
253
|
+
*/
|
|
254
|
+
export type ClientAuthResult = ClientAuthState | UnauthState;
|
|
255
|
+
/**
|
|
256
|
+
* Options passed to the SDK middleware factory (`nextgenMiddleware` in
|
|
257
|
+
* sdk-next, `createNextgenMiddleware` in sdk-nuxt).
|
|
258
|
+
*/
|
|
259
|
+
export type NextgenMiddlewareOptions = {
|
|
260
|
+
/**
|
|
261
|
+
* Full URL of the Zitadel auth backend.
|
|
262
|
+
* @default process.env.ZITADEL_URL ?? "http://localhost:8080"
|
|
263
|
+
*/
|
|
264
|
+
url?: string;
|
|
265
|
+
/**
|
|
266
|
+
* The project this app signs its users in to. A session counts as signed in
|
|
267
|
+
* only when it belongs to this project. A session of any other project on the
|
|
268
|
+
* same server (a second app, or the Console, whose cookie the browser sends
|
|
269
|
+
* to every port of a shared host) reads as signed out, and its cookie is left
|
|
270
|
+
* alone.
|
|
271
|
+
*
|
|
272
|
+
* The app keeps its session in a cookie of its own,
|
|
273
|
+
* `__nextgen_session_<projectId>` ({@link sessionCookieName}), so signing in
|
|
274
|
+
* here neither replaces nor reads the session of another app or of the
|
|
275
|
+
* Console on the same host (ADR 073).
|
|
276
|
+
*
|
|
277
|
+
* An opaque session token is checked by the backend
|
|
278
|
+
* (`GET /sessions/me?project_id=…`) and against the project its answer names;
|
|
279
|
+
* a JWT must name the project in its `aud` claim.
|
|
280
|
+
*
|
|
281
|
+
* Required: with no project, no session counts as signed in, and the proxy
|
|
282
|
+
* passes the server's session cookie through unchanged. Each SDK falls back
|
|
283
|
+
* to the `ZITADEL_PROJECT_ID` environment variable, then to its framework's
|
|
284
|
+
* public project id variable, which `zitadel setup` writes to `.env.local`.
|
|
285
|
+
*/
|
|
286
|
+
projectId?: string;
|
|
287
|
+
/**
|
|
288
|
+
* URL path prefix that is reverse-proxied to the auth backend.
|
|
289
|
+
*
|
|
290
|
+
* Sign-in with an external provider currently requires the default: the
|
|
291
|
+
* provider returns to `/__nextgen/idp/{slug}/callback`, which is proxied only under
|
|
292
|
+
* the default prefix.
|
|
293
|
+
* @default "/__nextgen"
|
|
294
|
+
*/
|
|
295
|
+
proxyPath?: string;
|
|
296
|
+
/**
|
|
297
|
+
* Pathnames that require a valid session. Requests to these routes without
|
|
298
|
+
* a valid token are redirected to {@link loginPath}.
|
|
299
|
+
* Entries ending with `*` match any sub-path (e.g. `"/admin*"`).
|
|
300
|
+
*
|
|
301
|
+
* When the backend cannot say whether a session cookie is live (it answers
|
|
302
|
+
* `5xx`, does not answer in time, or cannot be reached), the route answers
|
|
303
|
+
* `503 Service Unavailable` instead: the user may well be signed in, and the
|
|
304
|
+
* login page would tell them they are not.
|
|
305
|
+
* @default []
|
|
306
|
+
*/
|
|
307
|
+
protectedRoutes?: string[];
|
|
308
|
+
/**
|
|
309
|
+
* Pathnames that are completely skipped by the middleware — no JWT
|
|
310
|
+
* verification, no protection check, no header tunnelling.
|
|
311
|
+
* Useful for webhooks, health-check endpoints, or any route where
|
|
312
|
+
* running auth logic is undesirable.
|
|
313
|
+
* Entries ending with `*` match any sub-path (e.g. `"/public*"`).
|
|
314
|
+
* @default []
|
|
315
|
+
*/
|
|
316
|
+
ignoredRoutes?: string[];
|
|
317
|
+
/**
|
|
318
|
+
* Pathname to redirect unauthenticated users to.
|
|
319
|
+
* A `next` query parameter carries the requested path and query string,
|
|
320
|
+
* where `<zitadel-login>` returns after sign-in.
|
|
321
|
+
* @default "/login"
|
|
322
|
+
*/
|
|
323
|
+
loginPath?: string;
|
|
324
|
+
/**
|
|
325
|
+
* Reserved for future use. Local PEM public key for offline JWT verification.
|
|
326
|
+
*/
|
|
327
|
+
jwtKey?: string;
|
|
328
|
+
/**
|
|
329
|
+
* Restrict accepted JWT `alg` header values. Tokens whose algorithm is not
|
|
330
|
+
* in this list are rejected before JWKS is fetched.
|
|
331
|
+
* @default ["RS256", "ES256"]
|
|
332
|
+
*/
|
|
333
|
+
allowedAlgorithms?: string[];
|
|
334
|
+
/**
|
|
335
|
+
* Clock skew tolerance in milliseconds applied to `exp`, `nbf`, and `iat`
|
|
336
|
+
* claim validation.
|
|
337
|
+
* @default 5000
|
|
338
|
+
*/
|
|
339
|
+
clockSkewMs?: number;
|
|
340
|
+
/**
|
|
341
|
+
* Expected value(s) of the `aud` claim. When omitted, audience is not
|
|
342
|
+
* validated. Pass a single string or an array when a token may carry
|
|
343
|
+
* multiple audiences.
|
|
344
|
+
*/
|
|
345
|
+
audience?: string | string[];
|
|
346
|
+
/**
|
|
347
|
+
* Accepted values for the JWT header `typ` claim (case-insensitive).
|
|
348
|
+
* Tokens whose `typ` is not in this list are rejected.
|
|
349
|
+
* Set to `[]` to disable token-type checking entirely.
|
|
350
|
+
* @default ["JWT", "at+JWT"]
|
|
351
|
+
*/
|
|
352
|
+
allowedTokenTypes?: string[];
|
|
353
|
+
/**
|
|
354
|
+
* Timeout in milliseconds for JWKS endpoint requests.
|
|
355
|
+
* Requests that do not complete within this window are aborted and the
|
|
356
|
+
* token is rejected, treating the request as unauthenticated.
|
|
357
|
+
* @default 5000
|
|
358
|
+
*/
|
|
359
|
+
jwksTimeoutMs?: number;
|
|
360
|
+
/**
|
|
361
|
+
* Timeout in milliseconds for upstream proxy requests.
|
|
362
|
+
* Requests that do not complete within this window are aborted with a
|
|
363
|
+
* network error. Defaults to 5 000 ms.
|
|
364
|
+
* @default 5000
|
|
365
|
+
*/
|
|
366
|
+
proxyTimeoutMs?: number;
|
|
367
|
+
/**
|
|
368
|
+
* Timeout in milliseconds for opaque (non-JWT) session token validation
|
|
369
|
+
* via the backend's `GET /sessions/me` endpoint. A token the backend does
|
|
370
|
+
* not answer for within this window is not refused: the request counts as
|
|
371
|
+
* signed out, its session cookie is kept, and a protected route answers
|
|
372
|
+
* `503` (see {@link protectedRoutes}).
|
|
373
|
+
* @default 5000
|
|
374
|
+
*/
|
|
375
|
+
opaqueTokenTimeoutMs?: number;
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* What the backend reports for an opaque (non-JWT) session token.
|
|
379
|
+
*
|
|
380
|
+
* `userId` is `null` when the session names no user of this app's project.
|
|
381
|
+
* That is an anonymous session — `CreateSession` sets the session cookie
|
|
382
|
+
* before any user factor is verified, so an abandoned or in-progress sign-in
|
|
383
|
+
* holds a live session that names no user — or a live session of another
|
|
384
|
+
* project. Such a session is **not** signed in, but it is not dead either, so
|
|
385
|
+
* callers keep its cookie instead of clearing it: an anonymous sign-in may
|
|
386
|
+
* still complete, and the other project's session belongs to the app that
|
|
387
|
+
* signed it in.
|
|
388
|
+
*/
|
|
389
|
+
export interface OpaqueSession {
|
|
390
|
+
readonly userId: string | null;
|
|
391
|
+
readonly identifier: string | null;
|
|
392
|
+
readonly identifierProperty: string | null;
|
|
393
|
+
readonly display: string | null;
|
|
394
|
+
/** When the session expires, as the backend reports it, or `null`. */
|
|
395
|
+
readonly expiresAt: string | null;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The error code the backend answers a session read with when the session
|
|
399
|
+
* belongs to another project than the one the read names.
|
|
400
|
+
*/
|
|
401
|
+
export declare const SESSION_PROJECT_MISMATCH = "sess.project_mismatch";
|
|
402
|
+
/**
|
|
403
|
+
* The backend's verdict on an opaque session token, from
|
|
404
|
+
* {@link checkOpaqueSessionToken}.
|
|
405
|
+
*
|
|
406
|
+
* - `live`: the backend holds a live session for the token. Its `userId` is
|
|
407
|
+
* `null` when the session names no user of this app's project: an anonymous
|
|
408
|
+
* one, or one of another project (see {@link OpaqueSession}).
|
|
409
|
+
* - `refused`: the backend judged the token and refused it (`401`), or found
|
|
410
|
+
* the session it names gone (`404` with the `sess.not_found` code). Only
|
|
411
|
+
* this verdict makes the cookie dead.
|
|
412
|
+
* - `undecided`: the backend gave no verdict: a `5xx` or any other answer, no
|
|
413
|
+
* answer in time, a network error, or a body that cannot be read. The
|
|
414
|
+
* backend answers `500`, never `401`, when it cannot decide (its token or
|
|
415
|
+
* key store failed), so that an outage does not sign its users out. The
|
|
416
|
+
* request counts as signed out, but the cookie is kept.
|
|
417
|
+
*/
|
|
418
|
+
export type OpaqueSessionCheck = {
|
|
419
|
+
readonly verdict: "live";
|
|
420
|
+
readonly session: OpaqueSession;
|
|
421
|
+
} | {
|
|
422
|
+
readonly verdict: "refused";
|
|
423
|
+
} | {
|
|
424
|
+
readonly verdict: "undecided";
|
|
425
|
+
};
|
|
426
|
+
/**
|
|
427
|
+
* Asks the backend (`GET {issuerUrl}/sessions/me?project_id={projectId}`)
|
|
428
|
+
* whether an opaque session token names a live session in this app's project,
|
|
429
|
+
* telling a refusal apart from no verdict.
|
|
430
|
+
*
|
|
431
|
+
* A live session of another project comes back with no user, like an
|
|
432
|
+
* anonymous one: it signs no one in here, and its cookie is not dead. The
|
|
433
|
+
* backend refuses such a session itself (`403 sess.project_mismatch`); the
|
|
434
|
+
* project its answer names is checked as well, because a backend that predates
|
|
435
|
+
* the parameter answers for any project.
|
|
436
|
+
*
|
|
437
|
+
* A `404` is a refusal only with the `sess.not_found` code. Without it, the
|
|
438
|
+
* answer came from something in front of the backend (a proxy, a `url` with
|
|
439
|
+
* the wrong path), which judged nothing.
|
|
440
|
+
*
|
|
441
|
+
* When there is no verdict, it logs why: the request it was asked for then
|
|
442
|
+
* renders signed out, or answers `503` on a protected route, and nothing else
|
|
443
|
+
* would say what went wrong.
|
|
444
|
+
*
|
|
445
|
+
* @param token - The opaque session token from the app's session cookie.
|
|
446
|
+
* @param issuerUrl - Base URL of the auth backend.
|
|
447
|
+
* @param timeoutMs - How long to wait for the backend before giving up.
|
|
448
|
+
* @param projectId - The project this app signs its users in to. Without one,
|
|
449
|
+
* no session can be shown to belong here: the backend is not asked, and the
|
|
450
|
+
* session reads as live with no user, so its cookie is kept.
|
|
451
|
+
* @returns The backend's {@link OpaqueSessionCheck | verdict}.
|
|
452
|
+
*/
|
|
453
|
+
export declare function checkOpaqueSessionToken(token: string, issuerUrl: string, timeoutMs: number, projectId: string): Promise<OpaqueSessionCheck>;
|
|
454
|
+
/**
|
|
455
|
+
* Forgets which session tokens the backend placed in another project
|
|
456
|
+
* ({@link checkOpaqueSessionToken} remembers them for a while). For tests that
|
|
457
|
+
* answer for the same token differently.
|
|
458
|
+
*/
|
|
459
|
+
export declare function clearSessionCheckCache(): void;
|
|
460
|
+
/**
|
|
461
|
+
* Validates an opaque session token by asking the backend
|
|
462
|
+
* (`GET {issuerUrl}/sessions/me?project_id={projectId}`) whether the session is
|
|
463
|
+
* live in this app's project.
|
|
464
|
+
*
|
|
465
|
+
* `null` folds a refusal and no verdict together, so it says whether the
|
|
466
|
+
* request is signed in, not whether the cookie is dead: clearing the cookie on
|
|
467
|
+
* `null` signs users out whenever the backend fails. To decide that, use
|
|
468
|
+
* {@link checkOpaqueSessionToken}.
|
|
469
|
+
*
|
|
470
|
+
* @param token - The opaque session token from the app's session cookie.
|
|
471
|
+
* @param issuerUrl - Base URL of the auth backend.
|
|
472
|
+
* @param timeoutMs - How long to wait for the backend before giving up.
|
|
473
|
+
* @param projectId - The project this app signs its users in to.
|
|
474
|
+
* @returns The live session — with a `null` `userId` when it names no user of
|
|
475
|
+
* this project — or `null` when the backend did not confirm one: it refused
|
|
476
|
+
* the token, or gave no verdict.
|
|
477
|
+
*/
|
|
478
|
+
export declare function validateOpaqueSessionToken(token: string, issuerUrl: string, timeoutMs: number, projectId: string): Promise<OpaqueSession | null>;
|
|
479
|
+
/**
|
|
480
|
+
* Whether a verified JWT names `projectId` in its `aud` claim, the one place a
|
|
481
|
+
* self-contained token can say which project it was issued for.
|
|
482
|
+
*/
|
|
483
|
+
export declare function jwtNamesProject(payload: JwtPayload, projectId: string): boolean;
|
|
484
|
+
/**
|
|
485
|
+
* What a request carries that decides its session. The request URL and the
|
|
486
|
+
* forwarded headers decide whether a cookie needs `Secure`.
|
|
487
|
+
*/
|
|
488
|
+
export interface SessionRequest extends BrowserRequest {
|
|
489
|
+
/**
|
|
490
|
+
* The `Authorization: Bearer` token, or `null`. It takes precedence: when it
|
|
491
|
+
* signs a user of this project in, the cookie is not consulted.
|
|
492
|
+
*/
|
|
493
|
+
readonly bearer: string | null;
|
|
494
|
+
/** Reads one of the request's cookies by name. */
|
|
495
|
+
readonly cookie: (name: string) => string | null | undefined;
|
|
496
|
+
}
|
|
497
|
+
/** The settings {@link resolveSession} judges a request's session by. */
|
|
498
|
+
export interface ResolveSessionOptions {
|
|
499
|
+
readonly url: string;
|
|
500
|
+
/** The project this app signs its users in to; see {@link NextgenMiddlewareOptions.projectId}. */
|
|
501
|
+
readonly projectId: string | undefined;
|
|
502
|
+
readonly allowedAlgorithms: readonly string[] | undefined;
|
|
503
|
+
readonly clockSkewMs: number;
|
|
504
|
+
readonly audience: string | readonly string[] | undefined;
|
|
505
|
+
readonly allowedTokenTypes: readonly string[];
|
|
506
|
+
readonly jwksTimeoutMs: number | undefined;
|
|
507
|
+
readonly opaqueTokenTimeoutMs: number;
|
|
508
|
+
}
|
|
509
|
+
/** A request's session, as {@link resolveSession} judged it. */
|
|
510
|
+
export interface ResolvedSession {
|
|
511
|
+
readonly auth: AuthResult;
|
|
512
|
+
/**
|
|
513
|
+
* Whether the backend gave no verdict on the session cookie
|
|
514
|
+
* ({@link OpaqueSessionCheck}). The request counts as signed out and the
|
|
515
|
+
* cookie is kept; a protected route answers `503` rather than send a user
|
|
516
|
+
* who may well be signed in to the login page.
|
|
517
|
+
*/
|
|
518
|
+
readonly undecided: boolean;
|
|
519
|
+
/**
|
|
520
|
+
* The session cookies the response sets, with {@link SESSION_COOKIE_CACHE_CONTROL}
|
|
521
|
+
* when there are any. A cookie the backend refused is cleared so the browser
|
|
522
|
+
* stops sending it; a session of this project found in the server's cookie is
|
|
523
|
+
* copied to the project's own, and the server's cookie keeps it, since the
|
|
524
|
+
* Console may share the project. No other cookie is
|
|
525
|
+
* touched: a session that is live but signs no one in here (an anonymous
|
|
526
|
+
* one, or one of another project) keeps its cookie, and so does one the
|
|
527
|
+
* backend gave no verdict on, and one behind a Bearer token that signed the
|
|
528
|
+
* request in, which was never judged.
|
|
529
|
+
*/
|
|
530
|
+
readonly cookies: readonly SessionCookie[];
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Judges which user, if any, a request is signed in as in this app's project.
|
|
534
|
+
* The one implementation behind every server SDK's middleware, so they cannot
|
|
535
|
+
* drift on who counts as signed in.
|
|
536
|
+
*
|
|
537
|
+
* - A Bearer token is judged first, as a JWT. When it signs a user of this
|
|
538
|
+
* project in, that is the answer and the cookie is not judged; otherwise the
|
|
539
|
+
* cookie speaks for itself.
|
|
540
|
+
* - The session cookie is the project's own, else the server's. A session of
|
|
541
|
+
* this project found in the server's cookie is copied to the project's own.
|
|
542
|
+
* - A JWT-shaped session cookie signs its `sub` in when it verifies and its
|
|
543
|
+
* `aud` names the project. It never reaches the backend, which does not
|
|
544
|
+
* re-check JWT claims.
|
|
545
|
+
* - Any other session cookie is checked by the backend
|
|
546
|
+
* ({@link checkOpaqueSessionToken}) and signs its user in when the session
|
|
547
|
+
* is live in this project.
|
|
548
|
+
* - Only a cookie that held a dead session is cleared: one the backend refused,
|
|
549
|
+
* or a JWT that fails verification or names no user of this project. When
|
|
550
|
+
* the backend gives no verdict, the result says so
|
|
551
|
+
* ({@link ResolvedSession.undecided}) and the cookie stays.
|
|
552
|
+
* - Without a project nothing signs in, and no cookie is touched.
|
|
553
|
+
*/
|
|
554
|
+
export declare function resolveSession(request: SessionRequest, options: ResolveSessionOptions): Promise<ResolvedSession>;
|
|
555
|
+
//#endregion
|
|
556
|
+
//# sourceMappingURL=middleware.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.mts","names":[],"sources":["../src/middleware.ts"],"mappings":";;;;;;;qBAoBa,YAAY;;;;;;qBA2BZ,kBAAkB;;;;;;;;;;;;;;wBAiBf,cAAc,kBAAkB;;;;;;;;;;;;;;;;;;;wBA8ChC,iBAAiB,mBAAmB,qBAAqB,MAAM;;;;;;;;;;;;;wBA+B/D,sBAAsB,gBAAgB;;;;;;;;;;;;;;;;;;;;wBAgEtC,sBAAsB,UAAU,SAAS,mBAAmB;;qBAsB/D;;;;;;;;;;;wBAeG,kBAAkB;;;;;iBAoBjB;WACN;WACA;;WAEA;WACA;;;;;;wBAOK,uBAAuB,QAAQ;;;;;;qBAiBlC;;iBAGI;;WAEN,qBAAqB;;WAErB;;WAEA;;;;;;iBAiGM,uBAAuB;WAC7B;;WAEA;;WAEA;;WAEA;;;iBAIM;;WAEN;;WAEA;;;iBAIM;WACN;WACA,MAAM,eAAe;;;iBAIf;;WAEN;;;;;;EAMT,UAAU,UAAU,WAAW,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgCzB,oBACd,SAAS,gBACT,+BACA,UAAU,gBACT;;;;;;YAmJS;;EAEV;;;;;;EAMA;;;;;EAKA;;EAEA;;EAEA;;;YAIU;EAAc;EAAuB,SAAS;;;YAG9C;EAAgB;EAAwB;;;YAGxC,aAAa,YAAY;;;;;;;YAQzB;;EAEV;;EAEA;;EAEA;;EAEA;;;YAIU;EAAoB;EAAuB,SAAS;;;;;;;;YAQpD,mBAAmB,kBAAkB;;;;;YAMrC;;;;;EAKV;;;;;;;;;;;;;;;;;;;;;;EAuBA;;;;;;;;;EAUA;;;;;;;;;;;;EAaA;;;;;;;;;EAUA;;;;;;;EAQA;;;;EAKA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;EAQA;;;;;;;;;EAUA;;;;;;;;;;;;;;iBAiBe;WACN;WACA;WACA;WACA;;WAEA;;;;;;qBAOE;;;;;;;;;;;;;;;;;YAuDD;WACG;WAA0B,SAAS;;WACnC;;WACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAgCO,wBACpB,eACA,mBACA,mBACA,oBACC,QAAQ;;;;;;wBA2FK;;;;;;;;;;;;;;;;;;;wBAsBM,2BACpB,eACA,mBACA,mBACA,oBACC,QAAQ;;;;;wBAsCK,gBAAgB,SAAS,YAAY;;;;;iBAWpC,uBAAuB;;;;;WAK7B;;WAEA,SAAS;;;iBAIH;WACN;;WAEA;WACA;WACA;WACA;WACA;WACA;WACA;;;iBAIM;WACN,MAAM;;;;;;;WAON;;;;;;;;;;;;WAYA,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;wBAsHP,eACpB,SAAS,gBACT,SAAS,wBACR,QAAQ"}
|