@web-ts-toolkit/express-oidc-vault 0.32.0 → 0.34.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.
Files changed (6) hide show
  1. package/README.md +95 -23
  2. package/index.d.mts +65 -14
  3. package/index.d.ts +65 -14
  4. package/index.js +993 -460
  5. package/index.mjs +991 -459
  6. package/package.json +8 -4
package/index.js CHANGED
@@ -34,8 +34,9 @@ __export(index_exports, {
34
34
  DEFAULT_EXCHANGE_CODE_TTL_MS: () => DEFAULT_EXCHANGE_CODE_TTL_MS,
35
35
  DEFAULT_OIDC_SCOPES: () => DEFAULT_OIDC_SCOPES,
36
36
  DEFAULT_OIDC_VAULT_BASE_PATH: () => DEFAULT_OIDC_VAULT_BASE_PATH,
37
- DEFAULT_SESSION_COOKIE_NAME: () => DEFAULT_SESSION_COOKIE_NAME,
37
+ DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT: () => DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT,
38
38
  OIDC_VAULT_ROUTE_PATHS: () => OIDC_VAULT_ROUTE_PATHS,
39
+ OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT: () => OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT,
39
40
  OidcVaultStoreConflictError: () => OidcVaultStoreConflictError,
40
41
  createOidcVaultAccessTokenMiddleware: () => createOidcVaultAccessTokenMiddleware,
41
42
  createOidcVaultJwtAccessTokenValidator: () => createOidcVaultJwtAccessTokenValidator,
@@ -47,29 +48,46 @@ __export(index_exports, {
47
48
  module.exports = __toCommonJS(index_exports);
48
49
  var import_node_crypto = require("crypto");
49
50
  var import_express = __toESM(require("express"));
50
- var import_jose = require("jose");
51
51
 
52
52
  // src/config.ts
53
53
  var DEFAULT_OIDC_SCOPES = "openid email profile";
54
- var MANUAL_CONFIG_REQUIREMENTS = ["authorizationEndpoint", "tokenEndpoint", "jwksUri"];
54
+ var MANUAL_CONFIG_REQUIREMENTS = ["issuer", "authorizationEndpoint", "tokenEndpoint", "jwksUri"];
55
55
  var normalizeOptionalString = (value) => {
56
56
  const trimmed = value?.trim();
57
57
  return trimmed ? trimmed : void 0;
58
58
  };
59
+ var normalizeHttpUrl = (value, optionName) => {
60
+ if (value === void 0) {
61
+ return void 0;
62
+ }
63
+ let url;
64
+ try {
65
+ url = new URL(value);
66
+ } catch {
67
+ throw new Error(`${optionName} must be an absolute HTTP(S) URL.`);
68
+ }
69
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
70
+ throw new Error(`${optionName} must use http or https.`);
71
+ }
72
+ return url.toString();
73
+ };
59
74
  var withOptionalValue = (target, key, value) => {
60
75
  if (value !== void 0) {
61
76
  target[key] = value;
62
77
  }
63
78
  };
64
79
  function resolveOidcVaultConfig(config = {}) {
65
- const issuer = normalizeOptionalString(config.issuer);
80
+ const issuer = normalizeHttpUrl(normalizeOptionalString(config.issuer), "config.issuer");
66
81
  const clientId = normalizeOptionalString(config.clientId);
67
82
  const clientSecret = normalizeOptionalString(config.clientSecret);
68
83
  const scopes = normalizeOptionalString(config.scopes) ?? DEFAULT_OIDC_SCOPES;
84
+ const hasManualEndpoints = MANUAL_CONFIG_REQUIREMENTS.some(
85
+ (key) => key !== "issuer" && normalizeOptionalString(config[key])
86
+ );
69
87
  if (!clientId) {
70
88
  throw new Error("OIDC clientId is required.");
71
89
  }
72
- if (issuer) {
90
+ if (issuer && !hasManualEndpoints) {
73
91
  const resolved2 = {
74
92
  mode: "issuer",
75
93
  issuer,
@@ -83,11 +101,28 @@ function resolveOidcVaultConfig(config = {}) {
83
101
  mode: "manual",
84
102
  scopes
85
103
  };
86
- withOptionalValue(resolved, "authorizationEndpoint", normalizeOptionalString(config.authorizationEndpoint));
87
- withOptionalValue(resolved, "tokenEndpoint", normalizeOptionalString(config.tokenEndpoint));
88
- withOptionalValue(resolved, "userInfoEndpoint", normalizeOptionalString(config.userInfoEndpoint));
89
- withOptionalValue(resolved, "jwksUri", normalizeOptionalString(config.jwksUri));
90
- withOptionalValue(resolved, "endSessionEndpoint", normalizeOptionalString(config.endSessionEndpoint));
104
+ withOptionalValue(resolved, "issuer", issuer);
105
+ withOptionalValue(
106
+ resolved,
107
+ "authorizationEndpoint",
108
+ normalizeHttpUrl(normalizeOptionalString(config.authorizationEndpoint), "config.authorizationEndpoint")
109
+ );
110
+ withOptionalValue(
111
+ resolved,
112
+ "tokenEndpoint",
113
+ normalizeHttpUrl(normalizeOptionalString(config.tokenEndpoint), "config.tokenEndpoint")
114
+ );
115
+ withOptionalValue(
116
+ resolved,
117
+ "userInfoEndpoint",
118
+ normalizeHttpUrl(normalizeOptionalString(config.userInfoEndpoint), "config.userInfoEndpoint")
119
+ );
120
+ withOptionalValue(resolved, "jwksUri", normalizeHttpUrl(normalizeOptionalString(config.jwksUri), "config.jwksUri"));
121
+ withOptionalValue(
122
+ resolved,
123
+ "endSessionEndpoint",
124
+ normalizeHttpUrl(normalizeOptionalString(config.endSessionEndpoint), "config.endSessionEndpoint")
125
+ );
91
126
  withOptionalValue(resolved, "clientId", clientId);
92
127
  withOptionalValue(resolved, "clientSecret", clientSecret);
93
128
  const missing = MANUAL_CONFIG_REQUIREMENTS.filter((key) => !resolved[key]);
@@ -110,19 +145,13 @@ function resolveOidcVaultConfigFromEnv(env = process.env) {
110
145
  });
111
146
  }
112
147
 
113
- // src/types.ts
114
- var OidcVaultStoreConflictError = class extends Error {
115
- constructor(message = "OIDC vault store operation conflicted with concurrent state changes.") {
116
- super(message);
117
- this.name = "OidcVaultStoreConflictError";
118
- }
119
- };
120
-
121
- // src/index.ts
148
+ // src/constants.ts
122
149
  var DEFAULT_OIDC_VAULT_BASE_PATH = "/auth/oidc";
123
150
  var DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
124
151
  var DEFAULT_EXCHANGE_CODE_TTL_MS = 30 * 1e3;
125
152
  var DEFAULT_SESSION_COOKIE_NAME = "oidc_vault_session";
153
+ var DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT = "16kb";
154
+ var OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT = 16;
126
155
  var OIDC_VAULT_ROUTE_PATHS = {
127
156
  login: "/login",
128
157
  callback: "/callback",
@@ -131,51 +160,89 @@ var OIDC_VAULT_ROUTE_PATHS = {
131
160
  logout: "/logout",
132
161
  "backchannel-logout": "/backchannel-logout"
133
162
  };
134
- var discoveryCache = /* @__PURE__ */ new Map();
135
- var jwksCache = /* @__PURE__ */ new Map();
163
+
164
+ // src/errors.ts
136
165
  var OidcVaultHttpError = class extends Error {
137
- constructor(status, code, message) {
166
+ constructor(status, code, message, clientMessage = message) {
138
167
  super(message);
139
168
  this.status = status;
140
169
  this.code = code;
170
+ this.clientMessage = clientMessage;
141
171
  }
142
172
  };
143
- function normalizeOidcVaultBasePath(value) {
144
- if (!value || value === "/") {
145
- return DEFAULT_OIDC_VAULT_BASE_PATH;
146
- }
147
- return `/${value.replace(/^\/+|\/+$/g, "")}`;
148
- }
149
- var isRecord = (value) => typeof value === "object" && value !== null;
150
- var isString = (value) => typeof value === "string" && value.length > 0;
151
- var getNow = (options) => (options.now ?? Date.now)();
152
- var getSessionTransport = (options) => options.sessionTransport ?? "body";
153
- var usesCookieTransport = (options) => getSessionTransport(options) === "cookie";
154
- var createOpaqueId = (prefix) => `${prefix}_${(0, import_node_crypto.randomBytes)(16).toString("base64url")}`;
155
- var createPkceVerifier = () => (0, import_node_crypto.randomBytes)(32).toString("base64url");
156
- var createPkceChallenge = (verifier) => (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url");
157
173
  var toErrorPayload = (error) => {
158
174
  if (error instanceof OidcVaultHttpError) {
159
175
  return {
160
176
  status: error.status,
161
177
  code: error.code,
162
- message: error.message
178
+ message: error.clientMessage
163
179
  };
164
180
  }
165
181
  return {
166
182
  status: 500,
167
183
  code: "OIDC_VAULT_INTERNAL_ERROR",
168
- message: error instanceof Error ? error.message : "Unexpected OIDC vault error."
184
+ message: "Unexpected OIDC vault error."
185
+ };
186
+ };
187
+ var isBodyParserError = (error) => {
188
+ if (!(error instanceof Error)) {
189
+ return false;
190
+ }
191
+ const maybeParserError = error;
192
+ return typeof maybeParserError.type === "string" || typeof maybeParserError.status === "number";
193
+ };
194
+ var toBodyParserErrorPayload = (error) => {
195
+ if (error.type === "entity.too.large") {
196
+ return {
197
+ status: 413,
198
+ code: "OIDC_VAULT_REQUEST_BODY_TOO_LARGE",
199
+ message: "OIDC vault request body exceeds the configured size limit."
200
+ };
201
+ }
202
+ if (error.type === "parameters.too.many") {
203
+ return {
204
+ status: 413,
205
+ code: "OIDC_VAULT_REQUEST_BODY_PARAMETER_LIMIT_EXCEEDED",
206
+ message: "OIDC vault form request contains too many parameters."
207
+ };
208
+ }
209
+ if (error.type === "encoding.unsupported" || error.type === "charset.unsupported") {
210
+ return {
211
+ status: 415,
212
+ code: "OIDC_VAULT_UNSUPPORTED_REQUEST_BODY_ENCODING",
213
+ message: "OIDC vault request body encoding is not supported."
214
+ };
215
+ }
216
+ if (error.type === "entity.parse.failed") {
217
+ return {
218
+ status: 400,
219
+ code: "OIDC_VAULT_MALFORMED_REQUEST_BODY",
220
+ message: "OIDC vault request body is malformed."
221
+ };
222
+ }
223
+ const status = error.status ?? error.statusCode ?? 400;
224
+ return {
225
+ status: status >= 400 && status < 500 ? status : 400,
226
+ code: "OIDC_VAULT_INVALID_REQUEST_BODY",
227
+ message: "OIDC vault request body could not be parsed."
169
228
  };
170
229
  };
171
- var getRequestOrigin = (req) => `${req.protocol}://${req.get("host") ?? "localhost"}`;
172
- var getCallbackUri = (req, basePath) => `${getRequestOrigin(req)}${basePath}${OIDC_VAULT_ROUTE_PATHS.callback}`;
173
230
  var getRequiredString = (value, message, code, status = 400) => {
174
- if (!isString(value)) {
231
+ if (typeof value !== "string" || value.length === 0) {
232
+ throw new OidcVaultHttpError(status, code, message);
233
+ }
234
+ return value;
235
+ };
236
+ var getRequiredFiniteNonNegativeInteger = (value, message, code, status = 502) => {
237
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
175
238
  throw new OidcVaultHttpError(status, code, message);
176
239
  }
177
240
  return value;
178
241
  };
242
+
243
+ // src/cookies.ts
244
+ var getSessionTransport = (options) => options.sessionTransport ?? "body";
245
+ var usesCookieTransport = (options) => getSessionTransport(options) === "cookie";
179
246
  var parseCookieHeader = (headerValue) => {
180
247
  if (!headerValue) {
181
248
  return {};
@@ -190,7 +257,15 @@ var parseCookieHeader = (headerValue) => {
190
257
  if (!name) {
191
258
  return cookies;
192
259
  }
193
- cookies[name] = decodeURIComponent(value);
260
+ try {
261
+ cookies[name] = decodeURIComponent(value);
262
+ } catch {
263
+ throw new OidcVaultHttpError(
264
+ 400,
265
+ "OIDC_VAULT_MALFORMED_SESSION_COOKIE",
266
+ "OIDC vault session cookie is malformed."
267
+ );
268
+ }
194
269
  return cookies;
195
270
  }, {});
196
271
  };
@@ -208,44 +283,36 @@ var resolveCookieOptions = (options) => {
208
283
  httpOnly: cookieOptions.httpOnly ?? true
209
284
  };
210
285
  };
211
- var usesCrossSiteCookieTransport = (options) => usesCookieTransport(options) && resolveCookieOptions(options).sameSite === "none";
212
- var resolveTrustedOrigins = (options) => new Set((options.trustedOrigins ?? []).map((value) => new URL(value).origin));
213
- var getRequestSourceOrigin = (req) => {
214
- const origin = req.get("origin");
215
- if (isString(origin)) {
216
- return origin;
217
- }
218
- const referer = req.get("referer");
219
- if (!isString(referer)) {
220
- return void 0;
221
- }
222
- try {
223
- return new URL(referer).origin;
224
- } catch {
225
- return void 0;
286
+ var COOKIE_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
287
+ var COOKIE_DOMAIN_PATTERN = /^\.?[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
288
+ var hasUnsafeCookieValueCharacters = (value) => {
289
+ for (let index = 0; index < value.length; index += 1) {
290
+ const code = value.charCodeAt(index);
291
+ if (code <= 31 || code === 127 || value[index] === ";") {
292
+ return true;
293
+ }
226
294
  }
295
+ return false;
227
296
  };
228
- var assertTrustedOrigin = (req, options, trustedOrigins, action) => {
229
- if (!usesCrossSiteCookieTransport(options)) {
297
+ var validateCookieOptions = (options) => {
298
+ if (!usesCookieTransport(options)) {
230
299
  return;
231
300
  }
232
- const origin = getRequestSourceOrigin(req);
233
- if (!origin || !trustedOrigins.has(origin)) {
234
- throw new OidcVaultHttpError(
235
- 403,
236
- "OIDC_VAULT_UNTRUSTED_ORIGIN",
237
- `${action === "refresh" ? "Refresh" : "Logout"} request origin is not trusted.`
238
- );
301
+ const cookieOptions = resolveCookieOptions(options);
302
+ if (!COOKIE_NAME_PATTERN.test(cookieOptions.name)) {
303
+ throw new Error("cookie.name must be a valid HTTP cookie name.");
239
304
  }
240
- };
241
- var validateOidcVaultOptions = (options) => {
242
- const config = resolveOidcVaultConfig(options.config);
243
- const trustedOrigins = resolveTrustedOrigins(options);
244
- if (usesCrossSiteCookieTransport(options) && trustedOrigins.size === 0) {
245
- throw new Error("trustedOrigins is required when using cross-site cookie transport.");
305
+ if (hasUnsafeCookieValueCharacters(cookieOptions.path) || !cookieOptions.path.startsWith("/")) {
306
+ throw new Error("cookie.path must start with / and cannot contain control characters or semicolons.");
307
+ }
308
+ if (cookieOptions.domain !== void 0 && !COOKIE_DOMAIN_PATTERN.test(cookieOptions.domain)) {
309
+ throw new Error("cookie.domain must be a valid cookie domain.");
310
+ }
311
+ if (!cookieOptions.httpOnly) {
312
+ throw new Error("cookie.httpOnly must be true for cookie session transport.");
246
313
  }
247
- return { config, trustedOrigins };
248
314
  };
315
+ var usesCrossSiteCookieTransport = (options) => usesCookieTransport(options) && resolveCookieOptions(options).sameSite === "none";
249
316
  var serializeCookie = (name, value, options, overrides) => {
250
317
  const sameSite = options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1);
251
318
  const parts = [`${name}=${encodeURIComponent(value)}`, `Path=${options.path}`, `SameSite=${sameSite}`];
@@ -284,113 +351,727 @@ var getSessionIdFromCookie = (req, options) => {
284
351
  const cookies = parseCookieHeader(req.headers.cookie);
285
352
  return cookies[resolveCookieOptions(options).name];
286
353
  };
287
- var getSessionIdFromRequest = (req, options, action) => {
288
- const body = getBody(req);
289
- if (usesCookieTransport(options)) {
290
- const cookieSessionId = getSessionIdFromCookie(req, options);
291
- if (isString(cookieSessionId)) {
292
- return cookieSessionId;
293
- }
294
- if (isString(body.sessionId)) {
295
- return body.sessionId;
354
+
355
+ // src/utils.ts
356
+ var isRecord = (value) => typeof value === "object" && value !== null;
357
+ var isString = (value) => typeof value === "string" && value.length > 0;
358
+ var getBody = (req) => isRecord(req.body) ? req.body : {};
359
+
360
+ // src/origins.ts
361
+ var parseHttpUrl = (value, optionName, example) => {
362
+ let url;
363
+ try {
364
+ url = new URL(value);
365
+ } catch {
366
+ throw new Error(`${optionName} must be an absolute HTTP(S) URL, for example ${example}.`);
367
+ }
368
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
369
+ throw new Error(`${optionName} must use http or https, for example ${example}.`);
370
+ }
371
+ return url;
372
+ };
373
+ var resolveTrustedOrigins = (options) => {
374
+ const origins = /* @__PURE__ */ new Set();
375
+ for (const value of options.trustedOrigins ?? []) {
376
+ if (!isString(value)) {
377
+ throw new Error(
378
+ "trustedOrigins entries must be absolute HTTP(S) URLs, for example https://frontend.example.com."
379
+ );
296
380
  }
297
- throw new OidcVaultHttpError(
298
- 400,
299
- "OIDC_VAULT_MISSING_SESSION_ID",
300
- `${action === "refresh" ? "Refresh" : "Logout"} request is missing the session cookie.`
381
+ const url = parseHttpUrl(value, "trustedOrigins entries", "https://frontend.example.com");
382
+ origins.add(url.origin);
383
+ }
384
+ return origins;
385
+ };
386
+ var resolveBackendOrigin = (options) => {
387
+ if (!isString(options.backendOrigin)) {
388
+ throw new Error(
389
+ "backendOrigin is required and must be the public backend origin, for example https://api.example.com."
301
390
  );
302
391
  }
303
- return getRequiredString(
304
- body.sessionId,
305
- `${action === "refresh" ? "Refresh" : "Logout"} request is missing sessionId.`,
306
- "OIDC_VAULT_MISSING_SESSION_ID"
307
- );
392
+ const url = parseHttpUrl(options.backendOrigin, "backendOrigin", "https://api.example.com");
393
+ return url.origin;
308
394
  };
309
- var createExchangeResponse = (options, session, issuedToken) => ({
310
- sessionId: usesCookieTransport(options) ? void 0 : session.sessionId,
311
- user: session.user,
312
- ...issuedToken
313
- });
314
- var extractBearerToken = (authorizationHeader) => {
315
- if (!authorizationHeader) {
316
- throw new OidcVaultHttpError(401, "OIDC_VAULT_MISSING_BEARER_TOKEN", "Missing bearer token.");
395
+ var resolveFrontendRedirectUri = (options) => {
396
+ if (options.frontendRedirectUri === void 0) {
397
+ return void 0;
317
398
  }
318
- const [scheme, token, extra] = authorizationHeader.trim().split(/\s+/);
319
- if (scheme?.toLowerCase() !== "bearer" || !token || extra) {
399
+ if (!isString(options.frontendRedirectUri)) {
400
+ throw new Error(
401
+ "frontendRedirectUri must be an absolute HTTP(S) URL, for example https://frontend.example.com/callback."
402
+ );
403
+ }
404
+ return parseHttpUrl(
405
+ options.frontendRedirectUri,
406
+ "frontendRedirectUri",
407
+ "https://frontend.example.com/callback"
408
+ ).toString();
409
+ };
410
+ var validatePostLogoutRedirectUri = (options) => {
411
+ if (options.postLogoutRedirectUri === void 0) {
412
+ return;
413
+ }
414
+ if (!isString(options.postLogoutRedirectUri)) {
415
+ throw new Error(
416
+ "postLogoutRedirectUri must be an absolute HTTP(S) URL registered with the OIDC provider, for example https://frontend.example.com/logged-out."
417
+ );
418
+ }
419
+ parseHttpUrl(options.postLogoutRedirectUri, "postLogoutRedirectUri", "https://frontend.example.com/logged-out");
420
+ };
421
+ var getRequestSourceOrigin = (req) => {
422
+ const origin = req.get("origin");
423
+ if (isString(origin)) {
424
+ return origin;
425
+ }
426
+ const referer = req.get("referer");
427
+ if (!isString(referer)) {
428
+ return void 0;
429
+ }
430
+ try {
431
+ return new URL(referer).origin;
432
+ } catch {
433
+ return void 0;
434
+ }
435
+ };
436
+ var assertTrustedOrigin = (req, options, trustedOrigins, action) => {
437
+ if (!usesCookieTransport(options)) {
438
+ return;
439
+ }
440
+ const origin = getRequestSourceOrigin(req);
441
+ if (!origin || !trustedOrigins.has(origin)) {
320
442
  throw new OidcVaultHttpError(
321
- 401,
322
- "OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
323
- "Authorization header must use the Bearer scheme."
443
+ 403,
444
+ "OIDC_VAULT_UNTRUSTED_ORIGIN",
445
+ `${action === "refresh" ? "Refresh" : "Logout"} request origin is not trusted.`
324
446
  );
325
447
  }
326
- return token;
327
448
  };
328
- var setBearerChallengeHeader = (res) => {
329
- res.setHeader("WWW-Authenticate", "Bearer");
449
+
450
+ // src/provider-client.ts
451
+ var import_jose = require("jose");
452
+ var DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 5e3;
453
+ var DEFAULT_DISCOVERY_CACHE_TTL_MS = 10 * 6e4;
454
+ var DEFAULT_DISCOVERY_CACHE_CAPACITY = 32;
455
+ var DEFAULT_JWKS_CACHE_CAPACITY = 32;
456
+ var DEFAULT_PROVIDER_ERROR_BODY_LIMIT = 1024;
457
+ var DEFAULT_PROVIDER_JSON_BODY_LIMIT = 1024 * 1024;
458
+ var discoveryCache = /* @__PURE__ */ new Map();
459
+ var jwksCache = /* @__PURE__ */ new Map();
460
+ var normalizeIssuerForDiscoveryComparison = (issuer) => issuer.replace(/\/+$/g, "");
461
+ var assertDiscoveredIssuerMatchesConfig = (configuredIssuer, discoveredIssuer) => {
462
+ if (normalizeIssuerForDiscoveryComparison(configuredIssuer) !== normalizeIssuerForDiscoveryComparison(discoveredIssuer)) {
463
+ throw new OidcVaultHttpError(
464
+ 502,
465
+ "OIDC_VAULT_DISCOVERY_INVALID",
466
+ "OIDC discovery response issuer does not match the configured issuer."
467
+ );
468
+ }
330
469
  };
331
- var BACKCHANNEL_LOGOUT_EVENT_CLAIM = "http://schemas.openid.net/event/backchannel-logout";
332
- var defaultJwtClaimsMapper = (claims) => {
333
- const subject = getRequiredString(
334
- claims.sub,
335
- "JWT access token is missing sub.",
336
- "OIDC_VAULT_INVALID_ACCESS_TOKEN",
337
- 401
338
- );
339
- return {
340
- subject,
341
- sessionId: typeof claims.sid === "string" ? claims.sid : void 0,
342
- scope: typeof claims.scope === "string" ? claims.scope : void 0,
343
- claims
344
- };
470
+ var validateDiscoveredHttpUrl = (value, fieldName) => {
471
+ let url;
472
+ try {
473
+ url = new URL(value);
474
+ } catch {
475
+ throw new OidcVaultHttpError(
476
+ 502,
477
+ "OIDC_VAULT_DISCOVERY_INVALID",
478
+ `OIDC discovery response ${fieldName} must be an absolute HTTP(S) URL.`
479
+ );
480
+ }
481
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
482
+ throw new OidcVaultHttpError(
483
+ 502,
484
+ "OIDC_VAULT_DISCOVERY_INVALID",
485
+ `OIDC discovery response ${fieldName} must use http or https.`
486
+ );
487
+ }
488
+ return url.toString();
345
489
  };
346
490
  var buildWellKnownUrl = (issuer) => {
347
491
  const normalizedIssuer = issuer.endsWith("/") ? issuer : `${issuer}/`;
348
492
  return new URL(".well-known/openid-configuration", normalizedIssuer);
349
493
  };
350
- var resolveJwks = (jwksUri) => {
351
- let jwks = jwksCache.get(jwksUri);
352
- if (!jwks) {
353
- jwks = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri));
354
- jwksCache.set(jwksUri, jwks);
494
+ var getProviderRequestTimeoutMs = (options = {}) => {
495
+ const timeout = options.providerRequestTimeoutMs ?? DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
496
+ if (!Number.isFinite(timeout) || !Number.isInteger(timeout) || timeout <= 0) {
497
+ throw new OidcVaultHttpError(
498
+ 500,
499
+ "OIDC_VAULT_INVALID_CONFIG",
500
+ "providerRequestTimeoutMs must be a positive finite integer."
501
+ );
355
502
  }
356
- return jwks;
503
+ return timeout;
357
504
  };
358
- var appendQueryParam = (url, name, value) => {
359
- if (value) {
360
- url.searchParams.set(name, value);
505
+ var evictOldestEntry = (cache, capacity) => {
506
+ while (cache.size > capacity) {
507
+ const oldestKey = cache.keys().next().value;
508
+ if (typeof oldestKey !== "string") {
509
+ return;
510
+ }
511
+ cache.delete(oldestKey);
361
512
  }
362
513
  };
363
- var buildAuthorizationUrl = (metadata, transaction, redirectUri) => {
364
- const url = new URL(metadata.authorizationEndpoint);
365
- appendQueryParam(url, "response_type", "code");
366
- appendQueryParam(url, "client_id", metadata.clientId);
367
- appendQueryParam(url, "redirect_uri", redirectUri);
368
- appendQueryParam(url, "scope", metadata.scopes);
369
- appendQueryParam(url, "state", transaction.state);
370
- appendQueryParam(url, "nonce", transaction.nonce);
371
- appendQueryParam(url, "code_challenge", transaction.codeChallenge);
372
- appendQueryParam(url, "code_challenge_method", "S256");
373
- return url.toString();
514
+ var fetchProvider = async (input, init, errorCode, options) => {
515
+ const controller = new AbortController();
516
+ const timeout = setTimeout(() => controller.abort(), getProviderRequestTimeoutMs(options));
517
+ try {
518
+ return await fetch(input, {
519
+ ...init,
520
+ redirect: "manual",
521
+ signal: controller.signal
522
+ });
523
+ } catch (error) {
524
+ if (error instanceof Error && error.name === "AbortError") {
525
+ throw new OidcVaultHttpError(502, errorCode, "OIDC provider request timed out.");
526
+ }
527
+ throw error;
528
+ } finally {
529
+ clearTimeout(timeout);
530
+ }
374
531
  };
375
- var buildLogoutUrl = (endSessionEndpoint, idTokenHint, postLogoutRedirectUri) => {
376
- const url = new URL(endSessionEndpoint);
377
- appendQueryParam(url, "id_token_hint", idTokenHint);
378
- appendQueryParam(url, "post_logout_redirect_uri", postLogoutRedirectUri);
379
- return url.toString();
532
+ var readBoundedResponseText = async (response, limit, errorCode, options = {}) => {
533
+ if (!response.body) {
534
+ return "";
535
+ }
536
+ const reader = response.body.getReader();
537
+ const decoder = new TextDecoder();
538
+ let size = 0;
539
+ let text = "";
540
+ try {
541
+ for (; ; ) {
542
+ const result = await reader.read();
543
+ if (result.done) {
544
+ break;
545
+ }
546
+ size += result.value.byteLength;
547
+ if (size > limit) {
548
+ await reader.cancel();
549
+ if (options.truncate) {
550
+ return `${text.slice(0, limit)}...`;
551
+ }
552
+ throw new OidcVaultHttpError(502, errorCode, "OIDC provider response is too large.");
553
+ }
554
+ text += decoder.decode(result.value, { stream: true });
555
+ }
556
+ } finally {
557
+ reader.releaseLock();
558
+ }
559
+ return `${text}${decoder.decode()}`;
560
+ };
561
+ var resolveJwks = (jwksUri, options = {}) => {
562
+ let jwks = jwksCache.get(jwksUri);
563
+ if (!jwks) {
564
+ jwks = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri), {
565
+ timeoutDuration: getProviderRequestTimeoutMs(options),
566
+ cacheMaxAge: DEFAULT_DISCOVERY_CACHE_TTL_MS
567
+ });
568
+ jwksCache.set(jwksUri, jwks);
569
+ evictOldestEntry(jwksCache, DEFAULT_JWKS_CACHE_CAPACITY);
570
+ }
571
+ return jwks;
380
572
  };
381
- var toBasicAuthorization = (clientId, clientSecret) => `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
382
- var readJsonResponse = async (response, errorCode) => {
383
- const text = await response.text();
573
+ var formEncodeClientCredential = (value) => new URLSearchParams([[value, ""]]).toString().slice(0, -1);
574
+ var toBasicAuthorization = (clientId, clientSecret) => `Basic ${Buffer.from(`${formEncodeClientCredential(clientId)}:${formEncodeClientCredential(clientSecret)}`).toString("base64")}`;
575
+ var readJsonResponse = async (response, errorCode, invalidJsonMessage = "OIDC provider returned invalid JSON.") => {
576
+ const text = await readBoundedResponseText(response, DEFAULT_PROVIDER_JSON_BODY_LIMIT, errorCode);
384
577
  if (!text) {
385
578
  return {};
386
579
  }
387
580
  try {
388
581
  return JSON.parse(text);
389
582
  } catch {
390
- throw new OidcVaultHttpError(response.status, errorCode, text);
583
+ throw new OidcVaultHttpError(
584
+ response.ok ? 502 : response.status,
585
+ errorCode,
586
+ invalidJsonMessage,
587
+ invalidJsonMessage
588
+ );
391
589
  }
392
590
  };
393
- var getLogoutTokenFromRequest = (req) => {
591
+ async function discoverIssuerMetadata(issuer, options) {
592
+ const now = Date.now();
593
+ const cached = discoveryCache.get(issuer);
594
+ let discoveryPromise = cached && cached.expiresAt > now ? cached.promise : void 0;
595
+ if (cached && !discoveryPromise) {
596
+ discoveryCache.delete(issuer);
597
+ }
598
+ if (!discoveryPromise) {
599
+ discoveryPromise = (async () => {
600
+ const discoveryUrl = buildWellKnownUrl(issuer);
601
+ const response = await fetchProvider(discoveryUrl, void 0, "OIDC_VAULT_DISCOVERY_FAILED", options);
602
+ if (!response.ok) {
603
+ const errorText = await readBoundedResponseText(
604
+ response,
605
+ DEFAULT_PROVIDER_ERROR_BODY_LIMIT,
606
+ "OIDC_VAULT_DISCOVERY_FAILED",
607
+ {
608
+ truncate: true
609
+ }
610
+ );
611
+ throw new OidcVaultHttpError(
612
+ 502,
613
+ "OIDC_VAULT_DISCOVERY_FAILED",
614
+ errorText || `OIDC discovery failed with status ${response.status}.`,
615
+ "OIDC discovery failed."
616
+ );
617
+ }
618
+ const discovered = await readJsonResponse(
619
+ response,
620
+ "OIDC_VAULT_DISCOVERY_INVALID",
621
+ "OIDC discovery response is malformed JSON."
622
+ );
623
+ const authorizationEndpoint = validateDiscoveredHttpUrl(
624
+ getRequiredString(
625
+ discovered.authorization_endpoint,
626
+ "OIDC discovery response is missing authorization_endpoint.",
627
+ "OIDC_VAULT_DISCOVERY_INVALID",
628
+ 502
629
+ ),
630
+ "authorization_endpoint"
631
+ );
632
+ const tokenEndpoint = validateDiscoveredHttpUrl(
633
+ getRequiredString(
634
+ discovered.token_endpoint,
635
+ "OIDC discovery response is missing token_endpoint.",
636
+ "OIDC_VAULT_DISCOVERY_INVALID",
637
+ 502
638
+ ),
639
+ "token_endpoint"
640
+ );
641
+ const jwksUri = validateDiscoveredHttpUrl(
642
+ getRequiredString(
643
+ discovered.jwks_uri,
644
+ "OIDC discovery response is missing jwks_uri.",
645
+ "OIDC_VAULT_DISCOVERY_INVALID",
646
+ 502
647
+ ),
648
+ "jwks_uri"
649
+ );
650
+ const discoveredIssuer = getRequiredString(
651
+ discovered.issuer,
652
+ "OIDC discovery response is missing issuer.",
653
+ "OIDC_VAULT_DISCOVERY_INVALID",
654
+ 502
655
+ );
656
+ assertDiscoveredIssuerMatchesConfig(issuer, discoveredIssuer);
657
+ return {
658
+ issuer: discoveredIssuer,
659
+ authorizationEndpoint,
660
+ tokenEndpoint,
661
+ jwksUri,
662
+ userInfoEndpoint: typeof discovered.userinfo_endpoint === "string" ? validateDiscoveredHttpUrl(discovered.userinfo_endpoint, "userinfo_endpoint") : void 0,
663
+ endSessionEndpoint: typeof discovered.end_session_endpoint === "string" ? validateDiscoveredHttpUrl(discovered.end_session_endpoint, "end_session_endpoint") : void 0
664
+ };
665
+ })();
666
+ discoveryCache.set(issuer, {
667
+ expiresAt: now + DEFAULT_DISCOVERY_CACHE_TTL_MS,
668
+ promise: discoveryPromise
669
+ });
670
+ evictOldestEntry(discoveryCache, DEFAULT_DISCOVERY_CACHE_CAPACITY);
671
+ discoveryPromise.catch(() => {
672
+ if (discoveryCache.get(issuer)?.promise === discoveryPromise) {
673
+ discoveryCache.delete(issuer);
674
+ }
675
+ });
676
+ }
677
+ return discoveryPromise;
678
+ }
679
+ async function resolveProviderMetadata(config, options = {}) {
680
+ if (config.mode === "manual") {
681
+ if (!config.issuer) {
682
+ throw new OidcVaultHttpError(500, "OIDC_VAULT_INVALID_CONFIG", "Manual OIDC configuration requires issuer.");
683
+ }
684
+ return {
685
+ issuer: config.issuer,
686
+ authorizationEndpoint: config.authorizationEndpoint,
687
+ tokenEndpoint: config.tokenEndpoint,
688
+ jwksUri: config.jwksUri,
689
+ userInfoEndpoint: config.userInfoEndpoint,
690
+ endSessionEndpoint: config.endSessionEndpoint,
691
+ clientId: config.clientId,
692
+ clientSecret: config.clientSecret,
693
+ scopes: config.scopes
694
+ };
695
+ }
696
+ if (!config.issuer) {
697
+ throw new OidcVaultHttpError(500, "OIDC_VAULT_INVALID_CONFIG", "Issuer discovery requires an issuer URL.");
698
+ }
699
+ return {
700
+ ...await discoverIssuerMetadata(config.issuer, options),
701
+ clientId: config.clientId,
702
+ clientSecret: config.clientSecret,
703
+ scopes: config.scopes
704
+ };
705
+ }
706
+ async function requestToken(metadata, params, options = {}) {
707
+ const clientId = metadata.clientId;
708
+ if (!clientId) {
709
+ throw new OidcVaultHttpError(500, "OIDC_VAULT_MISSING_CLIENT_ID", "OIDC clientId is required.");
710
+ }
711
+ const body = new URLSearchParams();
712
+ body.set("client_id", clientId);
713
+ for (const [key, value] of Object.entries(params)) {
714
+ if (value !== void 0) {
715
+ body.set(key, value);
716
+ }
717
+ }
718
+ const headers = {
719
+ "content-type": "application/x-www-form-urlencoded"
720
+ };
721
+ if (metadata.clientSecret) {
722
+ headers.authorization = toBasicAuthorization(clientId, metadata.clientSecret);
723
+ }
724
+ const response = await fetchProvider(
725
+ metadata.tokenEndpoint,
726
+ {
727
+ method: "POST",
728
+ headers,
729
+ body
730
+ },
731
+ "OIDC_VAULT_TOKEN_REQUEST_FAILED",
732
+ options
733
+ );
734
+ const json = await readJsonResponse(response, "OIDC_VAULT_TOKEN_REQUEST_FAILED");
735
+ if (!response.ok) {
736
+ throw new OidcVaultHttpError(
737
+ 502,
738
+ "OIDC_VAULT_TOKEN_REQUEST_FAILED",
739
+ typeof json.error_description === "string" ? json.error_description : `OIDC token request failed with status ${response.status}.`,
740
+ "OIDC token request failed."
741
+ );
742
+ }
743
+ return json;
744
+ }
745
+ function validateTokenResponse(tokenResponse) {
746
+ if (typeof tokenResponse.token_type !== "string" || tokenResponse.token_type.toLowerCase() !== "bearer") {
747
+ throw new OidcVaultHttpError(
748
+ 502,
749
+ "OIDC_VAULT_INVALID_TOKEN_RESPONSE",
750
+ "OIDC token response token_type must be Bearer."
751
+ );
752
+ }
753
+ if (tokenResponse.expires_in !== void 0) {
754
+ getRequiredFiniteNonNegativeInteger(
755
+ tokenResponse.expires_in,
756
+ "OIDC token response expires_in must be a finite non-negative integer.",
757
+ "OIDC_VAULT_INVALID_TOKEN_RESPONSE"
758
+ );
759
+ }
760
+ }
761
+ async function fetchUserInfo(metadata, accessToken, options = {}) {
762
+ if (!metadata.userInfoEndpoint) {
763
+ return {};
764
+ }
765
+ const response = await fetchProvider(
766
+ metadata.userInfoEndpoint,
767
+ {
768
+ headers: {
769
+ authorization: `Bearer ${accessToken}`
770
+ }
771
+ },
772
+ "OIDC_VAULT_USERINFO_FAILED",
773
+ options
774
+ );
775
+ const json = await readJsonResponse(response, "OIDC_VAULT_USERINFO_FAILED");
776
+ if (!response.ok) {
777
+ throw new OidcVaultHttpError(
778
+ 502,
779
+ "OIDC_VAULT_USERINFO_FAILED",
780
+ `OIDC userinfo request failed with status ${response.status}.`,
781
+ "OIDC userinfo request failed."
782
+ );
783
+ }
784
+ return json;
785
+ }
786
+
787
+ // src/token-validation.ts
788
+ var import_jose2 = require("jose");
789
+ var BACKCHANNEL_LOGOUT_EVENT_CLAIM = "http://schemas.openid.net/event/backchannel-logout";
790
+ var validateBackchannelLogoutTokenTyp = (typ) => {
791
+ if (typ !== void 0 && (typeof typ !== "string" || typ.toLowerCase() !== "logout+jwt")) {
792
+ throw new OidcVaultHttpError(
793
+ 400,
794
+ "OIDC_VAULT_INVALID_LOGOUT_TOKEN",
795
+ "Backchannel logout token protected header typ must be logout+jwt when present."
796
+ );
797
+ }
798
+ };
799
+ var defaultJwtClaimsMapper = (claims) => {
800
+ const subject = getRequiredString(
801
+ claims.sub,
802
+ "JWT access token is missing sub.",
803
+ "OIDC_VAULT_INVALID_ACCESS_TOKEN",
804
+ 401
805
+ );
806
+ return {
807
+ subject,
808
+ sessionId: typeof claims.sid === "string" ? claims.sid : void 0,
809
+ scope: typeof claims.scope === "string" ? claims.scope : void 0,
810
+ claims
811
+ };
812
+ };
813
+ var assertUserInfoSubject = (userInfo, subject) => {
814
+ if (!isString(userInfo.sub)) {
815
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_USERINFO", "OIDC userinfo response is missing sub.");
816
+ }
817
+ if (userInfo.sub !== subject) {
818
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_USERINFO", "OIDC userinfo sub does not match id_token sub.");
819
+ }
820
+ };
821
+ var mergeUserProfile = (sub, idTokenClaims, userInfo) => ({
822
+ ...idTokenClaims,
823
+ ...userInfo ?? {},
824
+ sub
825
+ });
826
+ async function verifyIdToken(metadata, idToken, nonce, options = {}) {
827
+ if (!metadata.jwksUri) {
828
+ throw new OidcVaultHttpError(500, "OIDC_VAULT_MISSING_JWKS_URI", "OIDC jwksUri is required to validate id_token.");
829
+ }
830
+ if (!metadata.clientId) {
831
+ throw new OidcVaultHttpError(
832
+ 500,
833
+ "OIDC_VAULT_MISSING_CLIENT_ID",
834
+ "OIDC clientId is required to validate id_token."
835
+ );
836
+ }
837
+ const jwks = resolveJwks(metadata.jwksUri, options);
838
+ const result = await (0, import_jose2.jwtVerify)(idToken, jwks, {
839
+ audience: metadata.clientId,
840
+ issuer: metadata.issuer
841
+ });
842
+ if (nonce && result.payload.nonce !== nonce) {
843
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC id_token nonce validation failed.");
844
+ }
845
+ if (typeof result.payload.exp !== "number") {
846
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC id_token is missing exp.");
847
+ }
848
+ if (typeof result.payload.iat !== "number") {
849
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC id_token is missing iat.");
850
+ }
851
+ if (!isString(result.payload.sub)) {
852
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC id_token is missing sub.");
853
+ }
854
+ if (result.payload.azp !== void 0 && result.payload.azp !== metadata.clientId) {
855
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC id_token azp validation failed.");
856
+ }
857
+ if (Array.isArray(result.payload.aud) && result.payload.aud.length > 1 && result.payload.azp !== metadata.clientId) {
858
+ throw new OidcVaultHttpError(
859
+ 502,
860
+ "OIDC_VAULT_INVALID_ID_TOKEN",
861
+ "OIDC id_token azp is required for multiple audiences."
862
+ );
863
+ }
864
+ return result.payload;
865
+ }
866
+ async function verifyBackchannelLogoutToken(metadata, logoutToken, options = {}) {
867
+ if (!metadata.jwksUri) {
868
+ throw new OidcVaultHttpError(
869
+ 500,
870
+ "OIDC_VAULT_MISSING_JWKS_URI",
871
+ "OIDC jwksUri is required to validate logout_token."
872
+ );
873
+ }
874
+ if (!metadata.clientId) {
875
+ throw new OidcVaultHttpError(
876
+ 500,
877
+ "OIDC_VAULT_MISSING_CLIENT_ID",
878
+ "OIDC clientId is required to validate logout_token."
879
+ );
880
+ }
881
+ const jwks = resolveJwks(metadata.jwksUri, options);
882
+ const result = await (0, import_jose2.jwtVerify)(logoutToken, jwks, {
883
+ audience: metadata.clientId,
884
+ issuer: metadata.issuer
885
+ });
886
+ const claims = result.payload;
887
+ validateBackchannelLogoutTokenTyp(result.protectedHeader.typ);
888
+ if (claims.nonce !== void 0) {
889
+ throw new OidcVaultHttpError(
890
+ 400,
891
+ "OIDC_VAULT_INVALID_LOGOUT_TOKEN",
892
+ "Backchannel logout token must not contain nonce."
893
+ );
894
+ }
895
+ if (!isRecord(claims.events) || !isRecord(claims.events[BACKCHANNEL_LOGOUT_EVENT_CLAIM])) {
896
+ throw new OidcVaultHttpError(
897
+ 400,
898
+ "OIDC_VAULT_INVALID_LOGOUT_TOKEN",
899
+ "Backchannel logout token is missing the required event claim."
900
+ );
901
+ }
902
+ if (!isString(claims.sid) && !isString(claims.sub)) {
903
+ throw new OidcVaultHttpError(
904
+ 400,
905
+ "OIDC_VAULT_INVALID_LOGOUT_TOKEN",
906
+ "Backchannel logout token must include sid or sub."
907
+ );
908
+ }
909
+ if (!isString(claims.jti)) {
910
+ throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_LOGOUT_TOKEN", "Backchannel logout token must include jti.");
911
+ }
912
+ getRequiredFiniteNonNegativeInteger(
913
+ claims.iat,
914
+ "Backchannel logout token must include iat.",
915
+ "OIDC_VAULT_INVALID_LOGOUT_TOKEN",
916
+ 400
917
+ );
918
+ getRequiredFiniteNonNegativeInteger(
919
+ claims.exp,
920
+ "Backchannel logout token must include exp.",
921
+ "OIDC_VAULT_INVALID_LOGOUT_TOKEN",
922
+ 400
923
+ );
924
+ return claims;
925
+ }
926
+ function createOidcVaultJwtAccessTokenValidator(options) {
927
+ return {
928
+ async validate(token) {
929
+ const result = await (0, import_jose2.jwtVerify)(token, options.key, {
930
+ issuer: options.issuer,
931
+ audience: options.audience,
932
+ algorithms: options.algorithms
933
+ });
934
+ const claims = result.payload;
935
+ return options.mapClaims ? options.mapClaims(claims) : defaultJwtClaimsMapper(claims);
936
+ }
937
+ };
938
+ }
939
+
940
+ // src/types.ts
941
+ var OidcVaultStoreConflictError = class extends Error {
942
+ constructor(message = "OIDC vault store operation conflicted with concurrent state changes.") {
943
+ super(message);
944
+ this.name = "OidcVaultStoreConflictError";
945
+ }
946
+ };
947
+
948
+ // src/access-token-middleware.ts
949
+ var extractBearerToken = (authorizationHeader) => {
950
+ if (!authorizationHeader) {
951
+ throw new OidcVaultHttpError(401, "OIDC_VAULT_MISSING_BEARER_TOKEN", "Missing bearer token.");
952
+ }
953
+ const [scheme, token, extra] = authorizationHeader.trim().split(/\s+/);
954
+ if (scheme?.toLowerCase() !== "bearer" || !token || extra) {
955
+ throw new OidcVaultHttpError(
956
+ 401,
957
+ "OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
958
+ "Authorization header must use the Bearer scheme."
959
+ );
960
+ }
961
+ return token;
962
+ };
963
+ var setBearerChallengeHeader = (res) => {
964
+ res.setHeader("WWW-Authenticate", "Bearer");
965
+ };
966
+ function createOidcVaultAccessTokenMiddleware(options) {
967
+ return async (req, res, next) => {
968
+ try {
969
+ const token = extractBearerToken(req.get("authorization"));
970
+ const validationResult = await options.validator.validate(token);
971
+ const auth = {
972
+ token,
973
+ ...validationResult
974
+ };
975
+ const authenticatedRequest = req;
976
+ authenticatedRequest.auth = auth;
977
+ await options.onAuthContext?.({ req: authenticatedRequest, res, auth });
978
+ next();
979
+ } catch (error) {
980
+ if (error instanceof OidcVaultHttpError) {
981
+ setBearerChallengeHeader(res);
982
+ res.status(error.status).json({
983
+ code: error.code,
984
+ message: error.clientMessage
985
+ });
986
+ return;
987
+ }
988
+ setBearerChallengeHeader(res);
989
+ res.status(401).json({
990
+ code: "OIDC_VAULT_INVALID_ACCESS_TOKEN",
991
+ message: "Access token validation failed."
992
+ });
993
+ }
994
+ };
995
+ }
996
+
997
+ // src/index.ts
998
+ function normalizeOidcVaultBasePath(value) {
999
+ if (!value || value === "/") {
1000
+ return DEFAULT_OIDC_VAULT_BASE_PATH;
1001
+ }
1002
+ return `/${value.replace(/^\/+|\/+$/g, "")}`;
1003
+ }
1004
+ var getNow = (options) => (options.now ?? Date.now)();
1005
+ var createOpaqueId = (prefix) => `${prefix}_${(0, import_node_crypto.randomBytes)(16).toString("base64url")}`;
1006
+ var createPkceVerifier = () => (0, import_node_crypto.randomBytes)(32).toString("base64url");
1007
+ var createPkceChallenge = (verifier) => (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url");
1008
+ var getCallbackUri = (backendOrigin, basePath) => `${backendOrigin}${basePath}${OIDC_VAULT_ROUTE_PATHS.callback}`;
1009
+ var validateOidcVaultOptions = (options) => {
1010
+ const backendOrigin = resolveBackendOrigin(options);
1011
+ const frontendRedirectUri = resolveFrontendRedirectUri(options);
1012
+ validatePostLogoutRedirectUri(options);
1013
+ validateCookieOptions(options);
1014
+ const config = resolveOidcVaultConfig(options.config);
1015
+ const configuredTrustedOrigins = resolveTrustedOrigins(options);
1016
+ if (usesCrossSiteCookieTransport(options) && configuredTrustedOrigins.size === 0) {
1017
+ throw new Error("trustedOrigins is required when using cross-site cookie transport.");
1018
+ }
1019
+ const trustedOrigins = new Set(configuredTrustedOrigins);
1020
+ trustedOrigins.add(backendOrigin);
1021
+ if (frontendRedirectUri) {
1022
+ options.frontendRedirectUri = frontendRedirectUri;
1023
+ }
1024
+ return { backendOrigin, config, trustedOrigins };
1025
+ };
1026
+ var isStoreConflictError = (error) => error instanceof OidcVaultStoreConflictError || typeof error === "object" && error !== null && "name" in error && error.name === "OidcVaultStoreConflictError";
1027
+ var getSessionIdFromRequest = (req, options, action) => {
1028
+ if (usesCookieTransport(options)) {
1029
+ const cookieSessionId = getSessionIdFromCookie(req, options);
1030
+ if (isString(cookieSessionId)) {
1031
+ return cookieSessionId;
1032
+ }
1033
+ throw new OidcVaultHttpError(
1034
+ 400,
1035
+ "OIDC_VAULT_MISSING_SESSION_ID",
1036
+ `${action === "refresh" ? "Refresh" : "Logout"} request is missing the session cookie.`
1037
+ );
1038
+ }
1039
+ const body = getBody(req);
1040
+ return getRequiredString(
1041
+ body.sessionId,
1042
+ `${action === "refresh" ? "Refresh" : "Logout"} request is missing sessionId.`,
1043
+ "OIDC_VAULT_MISSING_SESSION_ID"
1044
+ );
1045
+ };
1046
+ var createExchangeResponse = (options, session, issuedToken) => ({
1047
+ sessionId: usesCookieTransport(options) ? void 0 : session.sessionId,
1048
+ user: session.user,
1049
+ ...issuedToken
1050
+ });
1051
+ var appendQueryParam = (url, name, value) => {
1052
+ if (value) {
1053
+ url.searchParams.set(name, value);
1054
+ }
1055
+ };
1056
+ var buildAuthorizationUrl = (metadata, transaction, redirectUri) => {
1057
+ const url = new URL(metadata.authorizationEndpoint);
1058
+ appendQueryParam(url, "response_type", "code");
1059
+ appendQueryParam(url, "client_id", metadata.clientId);
1060
+ appendQueryParam(url, "redirect_uri", redirectUri);
1061
+ appendQueryParam(url, "scope", metadata.scopes);
1062
+ appendQueryParam(url, "state", transaction.state);
1063
+ appendQueryParam(url, "nonce", transaction.nonce);
1064
+ appendQueryParam(url, "code_challenge", transaction.codeChallenge);
1065
+ appendQueryParam(url, "code_challenge_method", "S256");
1066
+ return url.toString();
1067
+ };
1068
+ var buildLogoutUrl = (endSessionEndpoint, idTokenHint, postLogoutRedirectUri) => {
1069
+ const url = new URL(endSessionEndpoint);
1070
+ appendQueryParam(url, "id_token_hint", idTokenHint);
1071
+ appendQueryParam(url, "post_logout_redirect_uri", postLogoutRedirectUri);
1072
+ return url.toString();
1073
+ };
1074
+ var getLogoutTokenFromRequest = (req) => {
394
1075
  const body = getBody(req);
395
1076
  return getRequiredString(
396
1077
  body.logout_token,
@@ -399,11 +1080,6 @@ var getLogoutTokenFromRequest = (req) => {
399
1080
  );
400
1081
  };
401
1082
  var createHookContext = (route, req, res, session, metadata) => ({ route, req, res, session, metadata });
402
- var mergeUserProfile = (sub, idTokenClaims, userInfo) => ({
403
- ...idTokenClaims,
404
- ...userInfo ?? {},
405
- sub
406
- });
407
1083
  var resolveReturnTo = (req, options) => {
408
1084
  const rawReturnTo = req.query.returnTo;
409
1085
  if (!isString(rawReturnTo)) {
@@ -417,8 +1093,13 @@ var resolveReturnTo = (req, options) => {
417
1093
  "Custom returnTo requires a configured frontendRedirectUri."
418
1094
  );
419
1095
  }
420
- const configuredUrl = new URL(configuredFrontendUri);
421
- const resolvedUrl = rawReturnTo.startsWith("/") ? new URL(rawReturnTo, configuredUrl) : new URL(rawReturnTo);
1096
+ const configuredUrl = new URL(configuredFrontendUri);
1097
+ let resolvedUrl;
1098
+ try {
1099
+ resolvedUrl = rawReturnTo.startsWith("/") ? new URL(rawReturnTo, configuredUrl) : new URL(rawReturnTo);
1100
+ } catch {
1101
+ throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_RETURN_TO", "returnTo must be a valid URL.");
1102
+ }
422
1103
  if (resolvedUrl.origin !== configuredUrl.origin) {
423
1104
  throw new OidcVaultHttpError(
424
1105
  400,
@@ -433,14 +1114,13 @@ var appendCodeToRedirectUri = (redirectUri, code) => {
433
1114
  url.searchParams.set("code", code);
434
1115
  return url.toString();
435
1116
  };
436
- var getBody = (req) => isRecord(req.body) ? req.body : {};
437
1117
  var withIssuedToken = async (req, res, options, session) => {
438
1118
  if (!options.tokenIssuer) {
439
1119
  return {};
440
1120
  }
441
1121
  return options.tokenIssuer.issue({ req, res, session });
442
1122
  };
443
- var resolveFrontendRedirectUri = (transaction, options) => {
1123
+ var resolveFrontendRedirectUri2 = (transaction, options) => {
444
1124
  const redirectUri = transaction.returnTo ?? options.frontendRedirectUri;
445
1125
  if (!redirectUri) {
446
1126
  throw new OidcVaultHttpError(
@@ -458,6 +1138,22 @@ async function callHook(route, hook, req, res, session, metadata) {
458
1138
  }
459
1139
  await hook(createHookContext(route, req, res, session, metadata));
460
1140
  }
1141
+ async function callPostCommitHook(route, options, hook, req, res, session, metadata) {
1142
+ if (!hook) {
1143
+ return;
1144
+ }
1145
+ try {
1146
+ await hook(createHookContext(route, req, res, session, metadata));
1147
+ } catch (error) {
1148
+ try {
1149
+ await options.hooks?.onError?.({
1150
+ ...createHookContext(route, req, res, session, metadata),
1151
+ error
1152
+ });
1153
+ } catch {
1154
+ }
1155
+ }
1156
+ }
461
1157
  async function handleRouteError(route, req, res, next, options, error) {
462
1158
  try {
463
1159
  await options.hooks?.onError?.({
@@ -485,208 +1181,21 @@ function createAsyncHandler(route, options, handler) {
485
1181
  }
486
1182
  };
487
1183
  }
488
- async function discoverIssuerMetadata(issuer) {
489
- let discoveryPromise = discoveryCache.get(issuer);
490
- if (!discoveryPromise) {
491
- discoveryPromise = (async () => {
492
- const discoveryUrl = buildWellKnownUrl(issuer);
493
- const response = await fetch(discoveryUrl);
494
- if (!response.ok) {
495
- const errorText = await response.text();
496
- throw new OidcVaultHttpError(
497
- 502,
498
- "OIDC_VAULT_DISCOVERY_FAILED",
499
- errorText || `OIDC discovery failed with status ${response.status}.`
500
- );
501
- }
502
- const discovered = await response.json();
503
- const authorizationEndpoint = getRequiredString(
504
- discovered.authorization_endpoint,
505
- "OIDC discovery response is missing authorization_endpoint.",
506
- "OIDC_VAULT_DISCOVERY_INVALID",
507
- 502
508
- );
509
- const tokenEndpoint = getRequiredString(
510
- discovered.token_endpoint,
511
- "OIDC discovery response is missing token_endpoint.",
512
- "OIDC_VAULT_DISCOVERY_INVALID",
513
- 502
514
- );
515
- const jwksUri = getRequiredString(
516
- discovered.jwks_uri,
517
- "OIDC discovery response is missing jwks_uri.",
518
- "OIDC_VAULT_DISCOVERY_INVALID",
519
- 502
520
- );
521
- return {
522
- issuer: getRequiredString(
523
- discovered.issuer,
524
- "OIDC discovery response is missing issuer.",
525
- "OIDC_VAULT_DISCOVERY_INVALID",
526
- 502
527
- ),
528
- authorizationEndpoint,
529
- tokenEndpoint,
530
- jwksUri,
531
- userInfoEndpoint: typeof discovered.userinfo_endpoint === "string" ? discovered.userinfo_endpoint : void 0,
532
- endSessionEndpoint: typeof discovered.end_session_endpoint === "string" ? discovered.end_session_endpoint : void 0
533
- };
534
- })();
535
- discoveryCache.set(issuer, discoveryPromise);
536
- discoveryPromise.catch(() => {
537
- if (discoveryCache.get(issuer) === discoveryPromise) {
538
- discoveryCache.delete(issuer);
539
- }
540
- });
541
- }
542
- return discoveryPromise;
543
- }
544
- async function resolveProviderMetadata(config) {
545
- if (config.mode === "manual") {
546
- return {
547
- issuer: config.issuer,
548
- authorizationEndpoint: config.authorizationEndpoint,
549
- tokenEndpoint: config.tokenEndpoint,
550
- jwksUri: config.jwksUri,
551
- userInfoEndpoint: config.userInfoEndpoint,
552
- endSessionEndpoint: config.endSessionEndpoint,
553
- clientId: config.clientId,
554
- clientSecret: config.clientSecret,
555
- scopes: config.scopes
556
- };
557
- }
558
- if (!config.issuer) {
559
- throw new OidcVaultHttpError(500, "OIDC_VAULT_INVALID_CONFIG", "Issuer discovery requires an issuer URL.");
560
- }
561
- return {
562
- ...await discoverIssuerMetadata(config.issuer),
563
- clientId: config.clientId,
564
- clientSecret: config.clientSecret,
565
- scopes: config.scopes
566
- };
567
- }
568
- async function requestToken(metadata, params) {
569
- const clientId = metadata.clientId;
570
- if (!clientId) {
571
- throw new OidcVaultHttpError(500, "OIDC_VAULT_MISSING_CLIENT_ID", "OIDC clientId is required.");
572
- }
573
- const body = new URLSearchParams();
574
- body.set("client_id", clientId);
575
- for (const [key, value] of Object.entries(params)) {
576
- if (value !== void 0) {
577
- body.set(key, value);
1184
+ function createBodyParserErrorHandler() {
1185
+ return (error, _req, res, next) => {
1186
+ if (!isBodyParserError(error)) {
1187
+ next(error);
1188
+ return;
578
1189
  }
579
- }
580
- const headers = {
581
- "content-type": "application/x-www-form-urlencoded"
1190
+ const payload = toBodyParserErrorPayload(error);
1191
+ res.status(payload.status).json({
1192
+ code: payload.code,
1193
+ message: payload.message
1194
+ });
582
1195
  };
583
- if (metadata.clientSecret) {
584
- headers.authorization = toBasicAuthorization(clientId, metadata.clientSecret);
585
- }
586
- const response = await fetch(metadata.tokenEndpoint, {
587
- method: "POST",
588
- headers,
589
- body
590
- });
591
- const json = await readJsonResponse(response, "OIDC_VAULT_TOKEN_REQUEST_FAILED");
592
- if (!response.ok) {
593
- throw new OidcVaultHttpError(
594
- 502,
595
- "OIDC_VAULT_TOKEN_REQUEST_FAILED",
596
- typeof json.error_description === "string" ? json.error_description : `OIDC token request failed with status ${response.status}.`
597
- );
598
- }
599
- return json;
600
- }
601
- async function fetchUserInfo(metadata, accessToken) {
602
- if (!metadata.userInfoEndpoint) {
603
- return {};
604
- }
605
- const response = await fetch(metadata.userInfoEndpoint, {
606
- headers: {
607
- authorization: `Bearer ${accessToken}`
608
- }
609
- });
610
- const json = await readJsonResponse(response, "OIDC_VAULT_USERINFO_FAILED");
611
- if (!response.ok) {
612
- throw new OidcVaultHttpError(
613
- 502,
614
- "OIDC_VAULT_USERINFO_FAILED",
615
- `OIDC userinfo request failed with status ${response.status}.`
616
- );
617
- }
618
- return json;
619
- }
620
- async function verifyIdToken(metadata, idToken, nonce) {
621
- if (!metadata.jwksUri) {
622
- throw new OidcVaultHttpError(500, "OIDC_VAULT_MISSING_JWKS_URI", "OIDC jwksUri is required to validate id_token.");
623
- }
624
- if (!metadata.clientId) {
625
- throw new OidcVaultHttpError(
626
- 500,
627
- "OIDC_VAULT_MISSING_CLIENT_ID",
628
- "OIDC clientId is required to validate id_token."
629
- );
630
- }
631
- const jwks = resolveJwks(metadata.jwksUri);
632
- const result = await (0, import_jose.jwtVerify)(idToken, jwks, {
633
- audience: metadata.clientId,
634
- issuer: metadata.issuer
635
- });
636
- if (nonce && result.payload.nonce !== nonce) {
637
- throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC id_token nonce validation failed.");
638
- }
639
- return result.payload;
640
- }
641
- async function verifyBackchannelLogoutToken(metadata, logoutToken) {
642
- if (!metadata.jwksUri) {
643
- throw new OidcVaultHttpError(
644
- 500,
645
- "OIDC_VAULT_MISSING_JWKS_URI",
646
- "OIDC jwksUri is required to validate logout_token."
647
- );
648
- }
649
- if (!metadata.clientId) {
650
- throw new OidcVaultHttpError(
651
- 500,
652
- "OIDC_VAULT_MISSING_CLIENT_ID",
653
- "OIDC clientId is required to validate logout_token."
654
- );
655
- }
656
- const jwks = resolveJwks(metadata.jwksUri);
657
- const result = await (0, import_jose.jwtVerify)(logoutToken, jwks, {
658
- audience: metadata.clientId,
659
- issuer: metadata.issuer
660
- });
661
- const claims = result.payload;
662
- if (claims.nonce !== void 0) {
663
- throw new OidcVaultHttpError(
664
- 400,
665
- "OIDC_VAULT_INVALID_LOGOUT_TOKEN",
666
- "Backchannel logout token must not contain nonce."
667
- );
668
- }
669
- if (!isRecord(claims.events) || !(BACKCHANNEL_LOGOUT_EVENT_CLAIM in claims.events)) {
670
- throw new OidcVaultHttpError(
671
- 400,
672
- "OIDC_VAULT_INVALID_LOGOUT_TOKEN",
673
- "Backchannel logout token is missing the required event claim."
674
- );
675
- }
676
- if (!isString(claims.sid) && !isString(claims.sub)) {
677
- throw new OidcVaultHttpError(
678
- 400,
679
- "OIDC_VAULT_INVALID_LOGOUT_TOKEN",
680
- "Backchannel logout token must include sid or sub."
681
- );
682
- }
683
- if (!isString(claims.jti)) {
684
- throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_LOGOUT_TOKEN", "Backchannel logout token must include jti.");
685
- }
686
- return claims;
687
1196
  }
688
- var createLoginHandler = (options, config, basePath) => createAsyncHandler("login", options, async (req, res) => {
689
- const metadata = await resolveProviderMetadata(config);
1197
+ var createLoginHandler = (options, config, backendOrigin, basePath) => createAsyncHandler("login", options, async (req, res) => {
1198
+ const metadata = await resolveProviderMetadata(config, options);
690
1199
  await callHook("login", options.hooks?.onLoginStart, req, res, void 0, { provider: metadata });
691
1200
  const now = getNow(options);
692
1201
  const pkceVerifier = createPkceVerifier();
@@ -700,14 +1209,14 @@ var createLoginHandler = (options, config, basePath) => createAsyncHandler("logi
700
1209
  expiresAt: now + (options.authorizationTransactionTtlMs ?? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS)
701
1210
  };
702
1211
  await options.storeProvider.createAuthorizationTransaction(transaction);
703
- const authorizationUrl = buildAuthorizationUrl(metadata, transaction, getCallbackUri(req, basePath));
1212
+ const authorizationUrl = buildAuthorizationUrl(metadata, transaction, getCallbackUri(backendOrigin, basePath));
704
1213
  await callHook("login", options.hooks?.onAuthorizationUrl, req, res, void 0, {
705
1214
  authorizationUrl,
706
1215
  state: transaction.state
707
1216
  });
708
1217
  res.redirect(302, authorizationUrl);
709
1218
  });
710
- var createCallbackHandler = (options, config, basePath) => createAsyncHandler("callback", options, async (req, res) => {
1219
+ var createCallbackHandler = (options, config, backendOrigin, basePath) => createAsyncHandler("callback", options, async (req, res) => {
711
1220
  if (isString(req.query.error)) {
712
1221
  throw new OidcVaultHttpError(400, "OIDC_VAULT_CALLBACK_ERROR", req.query.error);
713
1222
  }
@@ -721,13 +1230,18 @@ var createCallbackHandler = (options, config, basePath) => createAsyncHandler("c
721
1230
  if (!transaction) {
722
1231
  throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_STATE", "OIDC state is invalid or expired.");
723
1232
  }
724
- const metadata = await resolveProviderMetadata(config);
725
- const tokenResponse = await requestToken(metadata, {
726
- grant_type: "authorization_code",
727
- code,
728
- code_verifier: transaction.pkceVerifier,
729
- redirect_uri: getCallbackUri(req, basePath)
730
- });
1233
+ const metadata = await resolveProviderMetadata(config, options);
1234
+ const tokenResponse = await requestToken(
1235
+ metadata,
1236
+ {
1237
+ grant_type: "authorization_code",
1238
+ code,
1239
+ code_verifier: transaction.pkceVerifier,
1240
+ redirect_uri: getCallbackUri(backendOrigin, basePath)
1241
+ },
1242
+ options
1243
+ );
1244
+ validateTokenResponse(tokenResponse);
731
1245
  if (!isString(tokenResponse.id_token)) {
732
1246
  throw new OidcVaultHttpError(502, "OIDC_VAULT_MISSING_ID_TOKEN", "OIDC token response is missing id_token.");
733
1247
  }
@@ -743,9 +1257,12 @@ var createCallbackHandler = (options, config, basePath) => createAsyncHandler("c
743
1257
  hasRefreshToken: true,
744
1258
  hasIdToken: true
745
1259
  });
746
- const claims = await verifyIdToken(metadata, tokenResponse.id_token, transaction.nonce);
1260
+ const claims = await verifyIdToken(metadata, tokenResponse.id_token, transaction.nonce, options);
747
1261
  const subject = getRequiredString(claims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502);
748
- const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token) : void 0;
1262
+ const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
1263
+ if (userInfo) {
1264
+ assertUserInfoSubject(userInfo, subject);
1265
+ }
749
1266
  if (userInfo) {
750
1267
  await callHook("callback", options.hooks?.onUserInfo, req, res, void 0, { subject });
751
1268
  }
@@ -753,6 +1270,7 @@ var createCallbackHandler = (options, config, basePath) => createAsyncHandler("c
753
1270
  const sessionId = createOpaqueId("sess");
754
1271
  const session = {
755
1272
  sessionId,
1273
+ logicalSessionId: sessionId,
756
1274
  subject,
757
1275
  providerSessionId: typeof claims.sid === "string" ? claims.sid : void 0,
758
1276
  provider: {
@@ -763,7 +1281,6 @@ var createCallbackHandler = (options, config, basePath) => createAsyncHandler("c
763
1281
  idToken: tokenResponse.id_token,
764
1282
  accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : void 0,
765
1283
  scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : metadata.scopes,
766
- expiresAt: typeof tokenResponse.expires_in === "number" ? now + tokenResponse.expires_in * 1e3 : void 0,
767
1284
  createdAt: now,
768
1285
  updatedAt: now,
769
1286
  user: mergeUserProfile(subject, claims, userInfo),
@@ -773,16 +1290,25 @@ var createCallbackHandler = (options, config, basePath) => createAsyncHandler("c
773
1290
  };
774
1291
  await callHook("callback", options.hooks?.onBeforeSessionCreate, req, res, session, { subject });
775
1292
  const createdSession = await options.storeProvider.createSession(session);
776
- await callHook("callback", options.hooks?.onSessionCreated, req, res, createdSession, { subject });
777
1293
  const exchangeCode = createOpaqueId("code");
778
- await options.storeProvider.createExchangeCode({
779
- code: exchangeCode,
780
- sessionId: createdSession.sessionId,
781
- returnTo: transaction.returnTo,
782
- createdAt: now,
783
- expiresAt: now + (options.exchangeCodeTtlMs ?? DEFAULT_EXCHANGE_CODE_TTL_MS)
1294
+ try {
1295
+ await options.storeProvider.createExchangeCode({
1296
+ code: exchangeCode,
1297
+ sessionId: createdSession.sessionId,
1298
+ returnTo: transaction.returnTo,
1299
+ createdAt: now,
1300
+ expiresAt: now + (options.exchangeCodeTtlMs ?? DEFAULT_EXCHANGE_CODE_TTL_MS)
1301
+ });
1302
+ } catch (error) {
1303
+ await options.storeProvider.deleteSessionsByLogicalSessionId({
1304
+ logicalSessionId: createdSession.logicalSessionId ?? createdSession.sessionId
1305
+ });
1306
+ throw error;
1307
+ }
1308
+ await callPostCommitHook("callback", options, options.hooks?.onSessionCreated, req, res, createdSession, {
1309
+ subject
784
1310
  });
785
- res.redirect(302, appendCodeToRedirectUri(resolveFrontendRedirectUri(transaction, options), exchangeCode));
1311
+ res.redirect(302, appendCodeToRedirectUri(resolveFrontendRedirectUri2(transaction, options), exchangeCode));
786
1312
  });
787
1313
  var createExchangeHandler = (options) => createAsyncHandler("exchange", options, async (req, res) => {
788
1314
  const body = getBody(req);
@@ -795,7 +1321,18 @@ var createExchangeHandler = (options) => createAsyncHandler("exchange", options,
795
1321
  if (!session) {
796
1322
  throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
797
1323
  }
798
- const issuedToken = await withIssuedToken(req, res, options, session);
1324
+ let issuedToken;
1325
+ try {
1326
+ issuedToken = await withIssuedToken(req, res, options, session);
1327
+ } catch (error) {
1328
+ await options.storeProvider.deleteSessionsByLogicalSessionId({
1329
+ logicalSessionId: session.logicalSessionId ?? session.sessionId
1330
+ });
1331
+ if (usesCookieTransport(options)) {
1332
+ clearSessionCookie(res, options);
1333
+ }
1334
+ throw error;
1335
+ }
799
1336
  if (usesCookieTransport(options)) {
800
1337
  setSessionCookie(res, options, session.sessionId);
801
1338
  }
@@ -812,30 +1349,43 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
812
1349
  }
813
1350
  throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
814
1351
  }
815
- const metadata = await resolveProviderMetadata(config);
816
- const tokenResponse = await requestToken(metadata, {
817
- grant_type: "refresh_token",
818
- refresh_token: currentSession.refreshToken
819
- });
820
- const idToken = isString(tokenResponse.id_token) ? tokenResponse.id_token : currentSession.idToken;
821
- const claims = await verifyIdToken(metadata, idToken);
822
- const subject = getRequiredString(claims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502);
823
- const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token) : void 0;
1352
+ const metadata = await resolveProviderMetadata(config, options);
1353
+ const tokenResponse = await requestToken(
1354
+ metadata,
1355
+ {
1356
+ grant_type: "refresh_token",
1357
+ refresh_token: currentSession.refreshToken
1358
+ },
1359
+ options
1360
+ );
1361
+ validateTokenResponse(tokenResponse);
1362
+ const newIdToken = isString(tokenResponse.id_token) ? tokenResponse.id_token : void 0;
1363
+ const idToken = newIdToken ?? currentSession.idToken;
1364
+ const claims = newIdToken ? await verifyIdToken(metadata, newIdToken, void 0, options) : currentSession.user ?? { sub: currentSession.subject };
1365
+ const subject = newIdToken ? getRequiredString(claims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502) : currentSession.subject;
1366
+ if (newIdToken && subject !== currentSession.subject) {
1367
+ throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC refreshed id_token sub changed.");
1368
+ }
1369
+ const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
1370
+ if (userInfo) {
1371
+ assertUserInfoSubject(userInfo, subject);
1372
+ }
824
1373
  const now = getNow(options);
1374
+ const logicalSessionId = currentSession.logicalSessionId ?? currentSession.sessionId;
825
1375
  const nextSession = {
826
1376
  ...currentSession,
827
1377
  sessionId: createOpaqueId("sess"),
1378
+ logicalSessionId,
828
1379
  subject,
829
1380
  providerSessionId: typeof claims.sid === "string" ? claims.sid : currentSession.providerSessionId,
830
1381
  refreshToken: isString(tokenResponse.refresh_token) ? tokenResponse.refresh_token : currentSession.refreshToken,
831
1382
  idToken,
832
1383
  accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : currentSession.accessToken,
833
1384
  scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : currentSession.scope,
834
- expiresAt: typeof tokenResponse.expires_in === "number" ? now + tokenResponse.expires_in * 1e3 : currentSession.expiresAt,
1385
+ expiresAt: currentSession.expiresAt,
835
1386
  updatedAt: now,
836
1387
  user: mergeUserProfile(subject, claims, userInfo ?? currentSession.user)
837
1388
  };
838
- const issuedToken = await withIssuedToken(req, res, options, nextSession);
839
1389
  let rotatedSession;
840
1390
  try {
841
1391
  rotatedSession = await options.storeProvider.rotateSession({
@@ -843,7 +1393,7 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
843
1393
  nextSession
844
1394
  });
845
1395
  } catch (error) {
846
- if (error instanceof OidcVaultStoreConflictError) {
1396
+ if (isStoreConflictError(error)) {
847
1397
  if (usesCookieTransport(options)) {
848
1398
  clearSessionCookie(res, options);
849
1399
  }
@@ -851,7 +1401,19 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
851
1401
  }
852
1402
  throw error;
853
1403
  }
854
- await callHook("refresh", options.hooks?.onSessionRefreshed, req, res, rotatedSession, {
1404
+ let issuedToken;
1405
+ try {
1406
+ issuedToken = await withIssuedToken(req, res, options, rotatedSession);
1407
+ } catch (error) {
1408
+ await options.storeProvider.deleteSessionsByLogicalSessionId({
1409
+ logicalSessionId: rotatedSession.logicalSessionId ?? rotatedSession.sessionId
1410
+ });
1411
+ if (usesCookieTransport(options)) {
1412
+ clearSessionCookie(res, options);
1413
+ }
1414
+ throw error;
1415
+ }
1416
+ await callPostCommitHook("refresh", options, options.hooks?.onSessionRefreshed, req, res, rotatedSession, {
855
1417
  previousSessionId: currentSession.sessionId
856
1418
  });
857
1419
  if (usesCookieTransport(options)) {
@@ -867,6 +1429,7 @@ var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandle
867
1429
  const redirect = body.redirect === true;
868
1430
  const session = await options.storeProvider.getSession(sessionId);
869
1431
  if (!session) {
1432
+ await options.storeProvider.deleteSession(sessionId);
870
1433
  if (usesCookieTransport(options)) {
871
1434
  clearSessionCookie(res, options);
872
1435
  }
@@ -874,26 +1437,38 @@ var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandle
874
1437
  return;
875
1438
  }
876
1439
  await callHook("logout", options.hooks?.onBeforeLogout, req, res, session, void 0);
877
- await options.storeProvider.deleteSession(sessionId);
1440
+ await options.storeProvider.deleteSessionsByLogicalSessionId({
1441
+ logicalSessionId: session.logicalSessionId ?? session.sessionId
1442
+ });
878
1443
  if (usesCookieTransport(options)) {
879
1444
  clearSessionCookie(res, options);
880
1445
  }
881
- const metadata = await resolveProviderMetadata(config);
1446
+ const metadata = await resolveProviderMetadata(config, options);
882
1447
  const upstreamLogoutUrl = metadata.endSessionEndpoint ? buildLogoutUrl(metadata.endSessionEndpoint, session.idToken, options.postLogoutRedirectUri) : void 0;
883
- await callHook("logout", options.hooks?.onLogout, req, res, session, { upstreamLogoutUrl });
1448
+ await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, session);
884
1449
  if (redirect && upstreamLogoutUrl) {
885
1450
  res.redirect(302, upstreamLogoutUrl);
886
1451
  return;
887
1452
  }
888
1453
  res.status(200).json({
889
- loggedOut: true,
890
- upstreamLogoutUrl
1454
+ loggedOut: true
891
1455
  });
892
1456
  });
893
1457
  var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("backchannel-logout", options, async (req, res) => {
894
- const metadata = await resolveProviderMetadata(config);
1458
+ const metadata = await resolveProviderMetadata(config, options);
895
1459
  const logoutToken = getLogoutTokenFromRequest(req);
896
- const claims = await verifyBackchannelLogoutToken(metadata, logoutToken);
1460
+ const claims = await verifyBackchannelLogoutToken(metadata, logoutToken, options);
1461
+ const firstUse = await options.storeProvider.consumeBackchannelLogoutTokenJti({
1462
+ jti: claims.jti,
1463
+ expiresAt: claims.exp * 1e3
1464
+ });
1465
+ if (!firstUse) {
1466
+ res.status(200).json({
1467
+ loggedOut: true,
1468
+ revokedSessions: 0
1469
+ });
1470
+ return;
1471
+ }
897
1472
  const revokedSessions = isString(claims.sid) ? await options.storeProvider.deleteSessionsByProviderSessionId({
898
1473
  providerSessionId: claims.sid,
899
1474
  issuer: metadata.issuer,
@@ -903,7 +1478,7 @@ var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("ba
903
1478
  issuer: metadata.issuer,
904
1479
  clientId: metadata.clientId
905
1480
  });
906
- await callHook("backchannel-logout", options.hooks?.onLogout, req, res, void 0, {
1481
+ await callPostCommitHook("backchannel-logout", options, options.hooks?.onLogout, req, res, void 0, {
907
1482
  providerSessionId: claims.sid,
908
1483
  subject: claims.sub,
909
1484
  revokedSessions
@@ -913,73 +1488,30 @@ var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("ba
913
1488
  revokedSessions
914
1489
  });
915
1490
  });
916
- function registerRoutes(router, options, config, trustedOrigins, basePath) {
917
- router.get(OIDC_VAULT_ROUTE_PATHS.login, createLoginHandler(options, config, basePath));
918
- router.get(OIDC_VAULT_ROUTE_PATHS.callback, createCallbackHandler(options, config, basePath));
1491
+ function registerRoutes(router, options, config, trustedOrigins, backendOrigin, basePath) {
1492
+ router.get(OIDC_VAULT_ROUTE_PATHS.login, createLoginHandler(options, config, backendOrigin, basePath));
1493
+ router.get(OIDC_VAULT_ROUTE_PATHS.callback, createCallbackHandler(options, config, backendOrigin, basePath));
919
1494
  router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options));
920
1495
  router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins));
921
1496
  router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins));
922
1497
  router.post(OIDC_VAULT_ROUTE_PATHS["backchannel-logout"], createBackchannelLogoutHandler(options, config));
923
1498
  }
924
- function createOidcVaultJwtAccessTokenValidator(options) {
925
- return {
926
- async validate(token) {
927
- const result = await (0, import_jose.jwtVerify)(token, options.key, {
928
- issuer: options.issuer,
929
- audience: options.audience,
930
- algorithms: options.algorithms
931
- });
932
- const claims = result.payload;
933
- return options.mapClaims ? options.mapClaims(claims) : defaultJwtClaimsMapper(claims);
934
- }
935
- };
936
- }
937
- function createOidcVaultAccessTokenMiddleware(options) {
938
- return async (req, res, next) => {
939
- try {
940
- const token = extractBearerToken(req.get("authorization"));
941
- const validationResult = await options.validator.validate(token);
942
- const auth = {
943
- token,
944
- ...validationResult
945
- };
946
- const authenticatedRequest = req;
947
- authenticatedRequest.auth = auth;
948
- await options.onAuthContext?.({ req: authenticatedRequest, res, auth });
949
- next();
950
- } catch (error) {
951
- if (error instanceof OidcVaultHttpError) {
952
- setBearerChallengeHeader(res);
953
- res.status(error.status).json({
954
- code: error.code,
955
- message: error.message
956
- });
957
- return;
958
- }
959
- if (error instanceof Error) {
960
- setBearerChallengeHeader(res);
961
- res.status(401).json({
962
- code: "OIDC_VAULT_INVALID_ACCESS_TOKEN",
963
- message: error.message || "Access token validation failed."
964
- });
965
- return;
966
- }
967
- setBearerChallengeHeader(res);
968
- res.status(401).json({
969
- code: "OIDC_VAULT_INVALID_ACCESS_TOKEN",
970
- message: "Access token validation failed."
971
- });
972
- }
973
- };
974
- }
975
1499
  function createOidcVaultMiddleware(options) {
976
- const { config, trustedOrigins } = validateOidcVaultOptions(options);
1500
+ const { backendOrigin, config, trustedOrigins } = validateOidcVaultOptions(options);
977
1501
  const rootRouter = import_express.default.Router();
978
1502
  const baseRouter = import_express.default.Router();
979
1503
  const basePath = normalizeOidcVaultBasePath(options.basePath);
980
- baseRouter.use(import_express.default.json());
981
- baseRouter.use(import_express.default.urlencoded({ extended: false }));
982
- registerRoutes(baseRouter, options, config, trustedOrigins, basePath);
1504
+ const requestBodyLimit = options.requestBodyLimit ?? DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT;
1505
+ baseRouter.use(import_express.default.json({ limit: requestBodyLimit }));
1506
+ baseRouter.use(
1507
+ import_express.default.urlencoded({
1508
+ extended: false,
1509
+ limit: requestBodyLimit,
1510
+ parameterLimit: OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT
1511
+ })
1512
+ );
1513
+ baseRouter.use(createBodyParserErrorHandler());
1514
+ registerRoutes(baseRouter, options, config, trustedOrigins, backendOrigin, basePath);
983
1515
  rootRouter.use(basePath, baseRouter);
984
1516
  return rootRouter;
985
1517
  }
@@ -989,8 +1521,9 @@ function createOidcVaultMiddleware(options) {
989
1521
  DEFAULT_EXCHANGE_CODE_TTL_MS,
990
1522
  DEFAULT_OIDC_SCOPES,
991
1523
  DEFAULT_OIDC_VAULT_BASE_PATH,
992
- DEFAULT_SESSION_COOKIE_NAME,
1524
+ DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT,
993
1525
  OIDC_VAULT_ROUTE_PATHS,
1526
+ OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT,
994
1527
  OidcVaultStoreConflictError,
995
1528
  createOidcVaultAccessTokenMiddleware,
996
1529
  createOidcVaultJwtAccessTokenValidator,