@web-ts-toolkit/express-oidc-vault 0.42.2 → 0.47.1
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 +261 -51
- package/index.d.mts +300 -9
- package/index.d.ts +300 -9
- package/index.js +619 -194
- package/index.mjs +620 -195
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -52,6 +52,7 @@ var import_express = __toESM(require("express"));
|
|
|
52
52
|
// src/config.ts
|
|
53
53
|
var DEFAULT_OIDC_SCOPES = "openid email profile";
|
|
54
54
|
var MANUAL_CONFIG_REQUIREMENTS = ["issuer", "authorizationEndpoint", "tokenEndpoint", "jwksUri"];
|
|
55
|
+
var MANUAL_CONFIG_SELECTORS = ["authorizationEndpoint", "tokenEndpoint", "jwksUri", "userInfoEndpoint", "endSessionEndpoint"];
|
|
55
56
|
var normalizeOptionalString = (value) => {
|
|
56
57
|
const trimmed = value?.trim();
|
|
57
58
|
return trimmed ? trimmed : void 0;
|
|
@@ -71,19 +72,38 @@ var normalizeHttpUrl = (value, optionName) => {
|
|
|
71
72
|
}
|
|
72
73
|
return url.toString();
|
|
73
74
|
};
|
|
75
|
+
var normalizeIssuerIdentifier = (value, optionName) => {
|
|
76
|
+
if (value === void 0) {
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
let url;
|
|
80
|
+
try {
|
|
81
|
+
url = new URL(value);
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error(`${optionName} must be an absolute HTTP(S) URL.`);
|
|
84
|
+
}
|
|
85
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
|
|
86
|
+
throw new Error(`${optionName} must use http or https.`);
|
|
87
|
+
}
|
|
88
|
+
if (url.username !== "" || url.password !== "") {
|
|
89
|
+
throw new Error(`${optionName} must not include userinfo.`);
|
|
90
|
+
}
|
|
91
|
+
if (url.search !== "" || url.hash !== "") {
|
|
92
|
+
throw new Error(`${optionName} must not include query or fragment.`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
};
|
|
74
96
|
var withOptionalValue = (target, key, value) => {
|
|
75
97
|
if (value !== void 0) {
|
|
76
98
|
target[key] = value;
|
|
77
99
|
}
|
|
78
100
|
};
|
|
79
101
|
function resolveOidcVaultConfig(config = {}) {
|
|
80
|
-
const issuer =
|
|
102
|
+
const issuer = normalizeIssuerIdentifier(normalizeOptionalString(config.issuer), "config.issuer");
|
|
81
103
|
const clientId = normalizeOptionalString(config.clientId);
|
|
82
104
|
const clientSecret = normalizeOptionalString(config.clientSecret);
|
|
83
105
|
const scopes = normalizeOptionalString(config.scopes) ?? DEFAULT_OIDC_SCOPES;
|
|
84
|
-
const hasManualEndpoints =
|
|
85
|
-
(key) => key !== "issuer" && normalizeOptionalString(config[key])
|
|
86
|
-
);
|
|
106
|
+
const hasManualEndpoints = MANUAL_CONFIG_SELECTORS.some((key) => normalizeOptionalString(config[key]));
|
|
87
107
|
if (!clientId) {
|
|
88
108
|
throw new Error("OIDC clientId is required.");
|
|
89
109
|
}
|
|
@@ -243,37 +263,11 @@ var getRequiredFiniteNonNegativeInteger = (value, message, code, status = 502) =
|
|
|
243
263
|
// src/cookies.ts
|
|
244
264
|
var getSessionTransport = (options) => options.sessionTransport ?? "body";
|
|
245
265
|
var usesCookieTransport = (options) => getSessionTransport(options) === "cookie";
|
|
246
|
-
var parseCookieHeader = (headerValue) => {
|
|
247
|
-
if (!headerValue) {
|
|
248
|
-
return {};
|
|
249
|
-
}
|
|
250
|
-
return headerValue.split(";").reduce((cookies, part) => {
|
|
251
|
-
const separatorIndex = part.indexOf("=");
|
|
252
|
-
if (separatorIndex === -1) {
|
|
253
|
-
return cookies;
|
|
254
|
-
}
|
|
255
|
-
const name = part.slice(0, separatorIndex).trim();
|
|
256
|
-
const value = part.slice(separatorIndex + 1).trim();
|
|
257
|
-
if (!name) {
|
|
258
|
-
return cookies;
|
|
259
|
-
}
|
|
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
|
-
}
|
|
269
|
-
return cookies;
|
|
270
|
-
}, {});
|
|
271
|
-
};
|
|
272
266
|
var resolveCookieOptions = (options) => {
|
|
273
267
|
const cookieOptions = options.cookie ?? {};
|
|
274
268
|
const deploymentMode = cookieOptions.deploymentMode ?? "same-origin";
|
|
275
269
|
const sameSite = cookieOptions.sameSite ?? (deploymentMode === "cross-site" ? "none" : "lax");
|
|
276
|
-
const secure = cookieOptions.secure ?? (sameSite === "none" || deploymentMode === "cross-site");
|
|
270
|
+
const secure = cookieOptions.secure ?? (sameSite === "none" || deploymentMode === "cross-site" || isHttpsBackendOrigin(options));
|
|
277
271
|
return {
|
|
278
272
|
name: cookieOptions.name ?? DEFAULT_SESSION_COOKIE_NAME,
|
|
279
273
|
sameSite,
|
|
@@ -283,12 +277,24 @@ var resolveCookieOptions = (options) => {
|
|
|
283
277
|
httpOnly: cookieOptions.httpOnly ?? true
|
|
284
278
|
};
|
|
285
279
|
};
|
|
280
|
+
var isEffectivelySecureCookie = (cookieOptions) => cookieOptions.secure || cookieOptions.sameSite === "none";
|
|
281
|
+
var isHttpsBackendOrigin = (options) => {
|
|
282
|
+
try {
|
|
283
|
+
const raw = options.backendOrigin;
|
|
284
|
+
if (typeof raw !== "string") {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
return new URL(raw.trim()).protocol === "https:";
|
|
288
|
+
} catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
286
292
|
var COOKIE_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
287
293
|
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
294
|
var hasUnsafeCookieValueCharacters = (value) => {
|
|
289
295
|
for (let index = 0; index < value.length; index += 1) {
|
|
290
296
|
const code = value.charCodeAt(index);
|
|
291
|
-
if (code
|
|
297
|
+
if (code < 32 || code > 126 || value[index] === ";") {
|
|
292
298
|
return true;
|
|
293
299
|
}
|
|
294
300
|
}
|
|
@@ -311,6 +317,22 @@ var validateCookieOptions = (options) => {
|
|
|
311
317
|
if (!cookieOptions.httpOnly) {
|
|
312
318
|
throw new Error("cookie.httpOnly must be true for cookie session transport.");
|
|
313
319
|
}
|
|
320
|
+
const effectiveSecure = isEffectivelySecureCookie(cookieOptions);
|
|
321
|
+
if (cookieOptions.name.startsWith("__Host-")) {
|
|
322
|
+
if (!effectiveSecure) {
|
|
323
|
+
throw new Error("cookie.name with the __Host- prefix must be Secure.");
|
|
324
|
+
}
|
|
325
|
+
if (cookieOptions.domain !== void 0) {
|
|
326
|
+
throw new Error("cookie.name with the __Host- prefix must not set cookie.domain.");
|
|
327
|
+
}
|
|
328
|
+
if (cookieOptions.path !== "/") {
|
|
329
|
+
throw new Error('cookie.name with the __Host- prefix must use cookie.path "/".');
|
|
330
|
+
}
|
|
331
|
+
} else if (cookieOptions.name.startsWith("__Secure-")) {
|
|
332
|
+
if (!effectiveSecure) {
|
|
333
|
+
throw new Error("cookie.name with the __Secure- prefix must be Secure.");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
314
336
|
};
|
|
315
337
|
var usesCrossSiteCookieTransport = (options) => usesCookieTransport(options) && resolveCookieOptions(options).sameSite === "none";
|
|
316
338
|
var serializeCookie = (name, value, options, overrides) => {
|
|
@@ -348,8 +370,62 @@ var clearSessionCookie = (res, options) => {
|
|
|
348
370
|
);
|
|
349
371
|
};
|
|
350
372
|
var getSessionIdFromCookie = (req, options) => {
|
|
351
|
-
const
|
|
352
|
-
|
|
373
|
+
const raw = parseSelectedCookieValue(req.headers.cookie, resolveCookieOptions(options).name);
|
|
374
|
+
if (raw === void 0) {
|
|
375
|
+
return void 0;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
return decodeURIComponent(raw);
|
|
379
|
+
} catch {
|
|
380
|
+
throw new OidcVaultHttpError(400, "OIDC_VAULT_MALFORMED_SESSION_COOKIE", "OIDC vault session cookie is malformed.");
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
var parseSelectedCookieValue = (headerValue, selectedName) => {
|
|
384
|
+
if (!headerValue) {
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
const segments = headerValue.split(";");
|
|
388
|
+
for (const segment of segments) {
|
|
389
|
+
const separatorIndex = segment.indexOf("=");
|
|
390
|
+
if (separatorIndex === -1) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const name = segment.slice(0, separatorIndex).trim();
|
|
394
|
+
if (!name || name !== selectedName) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
return segment.slice(separatorIndex + 1).trim();
|
|
398
|
+
}
|
|
399
|
+
return void 0;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/lifetime-policy.ts
|
|
403
|
+
var MAX_EPOCH_MS = 864e13;
|
|
404
|
+
var isUsableEpochMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && Math.abs(value) <= MAX_EPOCH_MS;
|
|
405
|
+
var computeExpiresAt = (now, ttlMs, name) => {
|
|
406
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
|
|
407
|
+
throw new Error(`${name} must be a positive safe integer in milliseconds.`);
|
|
408
|
+
}
|
|
409
|
+
const expiresAt = now + ttlMs;
|
|
410
|
+
if (!isUsableEpochMs(now) || !isUsableEpochMs(expiresAt) || expiresAt <= now) {
|
|
411
|
+
throw new Error(`${name} must produce a usable epoch-millisecond expiry after now.`);
|
|
412
|
+
}
|
|
413
|
+
return expiresAt;
|
|
414
|
+
};
|
|
415
|
+
var validateLifetimeOptions = (options, now) => {
|
|
416
|
+
computeExpiresAt(
|
|
417
|
+
now,
|
|
418
|
+
options.authorizationTransactionTtlMs === void 0 ? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS : options.authorizationTransactionTtlMs,
|
|
419
|
+
"authorizationTransactionTtlMs"
|
|
420
|
+
);
|
|
421
|
+
computeExpiresAt(
|
|
422
|
+
now,
|
|
423
|
+
options.exchangeCodeTtlMs === void 0 ? DEFAULT_EXCHANGE_CODE_TTL_MS : options.exchangeCodeTtlMs,
|
|
424
|
+
"exchangeCodeTtlMs"
|
|
425
|
+
);
|
|
426
|
+
if (options.sessionTtlMs !== void 0) {
|
|
427
|
+
computeExpiresAt(now, options.sessionTtlMs, "sessionTtlMs");
|
|
428
|
+
}
|
|
353
429
|
};
|
|
354
430
|
|
|
355
431
|
// src/utils.ts
|
|
@@ -455,11 +531,29 @@ var DEFAULT_DISCOVERY_CACHE_CAPACITY = 32;
|
|
|
455
531
|
var DEFAULT_JWKS_CACHE_CAPACITY = 32;
|
|
456
532
|
var DEFAULT_PROVIDER_ERROR_BODY_LIMIT = 1024;
|
|
457
533
|
var DEFAULT_PROVIDER_JSON_BODY_LIMIT = 1024 * 1024;
|
|
534
|
+
var DEFAULT_JWKS_BODY_LIMIT = 1024 * 1024;
|
|
535
|
+
var DEFAULT_JWKS_MAX_KEYS = 100;
|
|
458
536
|
var discoveryCache = /* @__PURE__ */ new Map();
|
|
459
537
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
460
|
-
var normalizeIssuerForDiscoveryComparison = (issuer) => issuer.replace(/\/+$/g, "");
|
|
461
538
|
var assertDiscoveredIssuerMatchesConfig = (configuredIssuer, discoveredIssuer) => {
|
|
462
|
-
|
|
539
|
+
let discoveredUrl;
|
|
540
|
+
try {
|
|
541
|
+
discoveredUrl = new URL(discoveredIssuer);
|
|
542
|
+
} catch {
|
|
543
|
+
throw new OidcVaultHttpError(
|
|
544
|
+
502,
|
|
545
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
546
|
+
"OIDC discovery response issuer must be an absolute HTTP(S) URL."
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (discoveredUrl.protocol !== "https:" && discoveredUrl.protocol !== "http:" || discoveredUrl.origin === "null" || discoveredUrl.username !== "" || discoveredUrl.password !== "" || discoveredUrl.search !== "" || discoveredUrl.hash !== "") {
|
|
550
|
+
throw new OidcVaultHttpError(
|
|
551
|
+
502,
|
|
552
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
553
|
+
"OIDC discovery response issuer must be an http(s) URL without userinfo, query, or fragment."
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (configuredIssuer !== discoveredIssuer) {
|
|
463
557
|
throw new OidcVaultHttpError(
|
|
464
558
|
502,
|
|
465
559
|
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
@@ -487,6 +581,20 @@ var validateDiscoveredHttpUrl = (value, fieldName) => {
|
|
|
487
581
|
}
|
|
488
582
|
return url.toString();
|
|
489
583
|
};
|
|
584
|
+
var validateOptionalDiscoveredHttpUrl = (value, fieldName) => {
|
|
585
|
+
if (value === void 0) {
|
|
586
|
+
return void 0;
|
|
587
|
+
}
|
|
588
|
+
return validateDiscoveredHttpUrl(
|
|
589
|
+
getRequiredString(
|
|
590
|
+
value,
|
|
591
|
+
`OIDC discovery response ${fieldName} must be a non-empty string when present.`,
|
|
592
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
593
|
+
502
|
|
594
|
+
),
|
|
595
|
+
fieldName
|
|
596
|
+
);
|
|
597
|
+
};
|
|
490
598
|
var buildWellKnownUrl = (issuer) => {
|
|
491
599
|
const normalizedIssuer = issuer.endsWith("/") ? issuer : `${issuer}/`;
|
|
492
600
|
return new URL(".well-known/openid-configuration", normalizedIssuer);
|
|
@@ -511,42 +619,91 @@ var evictOldestEntry = (cache, capacity) => {
|
|
|
511
619
|
cache.delete(oldestKey);
|
|
512
620
|
}
|
|
513
621
|
};
|
|
622
|
+
var isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
623
|
+
var createProviderTimeoutError = (errorCode) => new OidcVaultHttpError(502, errorCode, "OIDC provider request timed out.");
|
|
624
|
+
var createProviderTransportError = (errorCode, cause) => {
|
|
625
|
+
const error = new OidcVaultHttpError(502, errorCode, "OIDC provider request failed.");
|
|
626
|
+
Object.defineProperty(error, "cause", { value: cause, configurable: true, writable: true });
|
|
627
|
+
return error;
|
|
628
|
+
};
|
|
629
|
+
var cancelBestEffort = (cancel) => {
|
|
630
|
+
try {
|
|
631
|
+
void cancel()?.catch(() => {
|
|
632
|
+
});
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
};
|
|
514
636
|
var fetchProvider = async (input, init, errorCode, options) => {
|
|
637
|
+
const timeoutMs = getProviderRequestTimeoutMs(options);
|
|
515
638
|
const controller = new AbortController();
|
|
516
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
639
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
640
|
+
const done = () => clearTimeout(timeout);
|
|
517
641
|
try {
|
|
518
|
-
|
|
642
|
+
const response = await fetch(input, {
|
|
519
643
|
...init,
|
|
520
644
|
redirect: "manual",
|
|
521
645
|
signal: controller.signal
|
|
522
646
|
});
|
|
647
|
+
return { response, signal: controller.signal, done };
|
|
523
648
|
} catch (error) {
|
|
524
|
-
|
|
525
|
-
|
|
649
|
+
done();
|
|
650
|
+
if (controller.signal.aborted || isAbortError(error)) {
|
|
651
|
+
throw createProviderTimeoutError(errorCode);
|
|
526
652
|
}
|
|
527
|
-
throw error;
|
|
528
|
-
} finally {
|
|
529
|
-
clearTimeout(timeout);
|
|
653
|
+
throw createProviderTransportError(errorCode, error);
|
|
530
654
|
}
|
|
531
655
|
};
|
|
656
|
+
var throwBoundedReadTimeout = (errorCode, timeoutAsTimeoutError) => {
|
|
657
|
+
if (timeoutAsTimeoutError) {
|
|
658
|
+
const timeoutError = new Error("JSON Web Key Set request timed out.");
|
|
659
|
+
timeoutError.name = "TimeoutError";
|
|
660
|
+
throw timeoutError;
|
|
661
|
+
}
|
|
662
|
+
throw createProviderTimeoutError(errorCode);
|
|
663
|
+
};
|
|
532
664
|
var readBoundedResponseText = async (response, limit, errorCode, options = {}) => {
|
|
533
665
|
if (!response.body) {
|
|
534
666
|
return "";
|
|
535
667
|
}
|
|
536
668
|
const reader = response.body.getReader();
|
|
537
669
|
const decoder = new TextDecoder();
|
|
670
|
+
const { signal } = options;
|
|
538
671
|
let size = 0;
|
|
539
672
|
let text = "";
|
|
673
|
+
let onAbort;
|
|
674
|
+
let abortPromise;
|
|
675
|
+
if (signal) {
|
|
676
|
+
if (signal.aborted) {
|
|
677
|
+
cancelBestEffort(() => reader.cancel());
|
|
678
|
+
reader.releaseLock();
|
|
679
|
+
throwBoundedReadTimeout(errorCode, options.timeoutAsTimeoutError === true);
|
|
680
|
+
}
|
|
681
|
+
abortPromise = new Promise((_resolve, reject) => {
|
|
682
|
+
onAbort = () => {
|
|
683
|
+
reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
|
|
684
|
+
};
|
|
685
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
686
|
+
});
|
|
687
|
+
abortPromise.catch(() => {
|
|
688
|
+
});
|
|
689
|
+
}
|
|
540
690
|
try {
|
|
541
691
|
for (; ; ) {
|
|
542
|
-
|
|
692
|
+
let result;
|
|
693
|
+
try {
|
|
694
|
+
result = abortPromise === void 0 ? await reader.read() : await Promise.race([reader.read(), abortPromise]);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (signal?.aborted === true || isAbortError(error)) {
|
|
697
|
+
throwBoundedReadTimeout(errorCode, options.timeoutAsTimeoutError === true);
|
|
698
|
+
}
|
|
699
|
+
throw createProviderTransportError(options.transportErrorCode ?? errorCode, error);
|
|
700
|
+
}
|
|
543
701
|
if (result.done) {
|
|
544
702
|
break;
|
|
545
703
|
}
|
|
546
704
|
size += result.value.byteLength;
|
|
547
705
|
if (size > limit) {
|
|
548
|
-
|
|
549
|
-
if (options.truncate) {
|
|
706
|
+
if (options.truncate === true) {
|
|
550
707
|
return `${text.slice(0, limit)}...`;
|
|
551
708
|
}
|
|
552
709
|
throw new OidcVaultHttpError(502, errorCode, "OIDC provider response is too large.");
|
|
@@ -554,123 +711,219 @@ var readBoundedResponseText = async (response, limit, errorCode, options = {}) =
|
|
|
554
711
|
text += decoder.decode(result.value, { stream: true });
|
|
555
712
|
}
|
|
556
713
|
} finally {
|
|
714
|
+
if (signal !== void 0 && onAbort !== void 0) {
|
|
715
|
+
signal.removeEventListener("abort", onAbort);
|
|
716
|
+
}
|
|
717
|
+
cancelBestEffort(() => reader.cancel());
|
|
557
718
|
reader.releaseLock();
|
|
558
719
|
}
|
|
559
720
|
return `${text}${decoder.decode()}`;
|
|
560
721
|
};
|
|
722
|
+
var createBoundedJwksFetch = (timeoutMs) => {
|
|
723
|
+
void timeoutMs;
|
|
724
|
+
return async (url, { headers, method, redirect, signal }) => {
|
|
725
|
+
let response;
|
|
726
|
+
try {
|
|
727
|
+
response = await fetch(url, { method, headers, redirect, signal });
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (signal.aborted || isAbortError(error)) {
|
|
730
|
+
const timeoutError = new Error("JSON Web Key Set request timed out.");
|
|
731
|
+
timeoutError.name = "TimeoutError";
|
|
732
|
+
throw timeoutError;
|
|
733
|
+
}
|
|
734
|
+
throw createProviderTransportError("OIDC_VAULT_JWKS_FAILED", error);
|
|
735
|
+
}
|
|
736
|
+
if (response.status !== 200) {
|
|
737
|
+
cancelBestEffort(() => response.body?.cancel());
|
|
738
|
+
return response;
|
|
739
|
+
}
|
|
740
|
+
const text = await readBoundedResponseText(response, DEFAULT_JWKS_BODY_LIMIT, "OIDC_VAULT_JWKS_FAILED", {
|
|
741
|
+
signal,
|
|
742
|
+
timeoutAsTimeoutError: true
|
|
743
|
+
});
|
|
744
|
+
try {
|
|
745
|
+
const parsed = JSON.parse(text);
|
|
746
|
+
if (Array.isArray(parsed.keys) && parsed.keys.length > DEFAULT_JWKS_MAX_KEYS) {
|
|
747
|
+
throw new OidcVaultHttpError(502, "OIDC_VAULT_JWKS_FAILED", "JSON Web Key Set contains too many keys.");
|
|
748
|
+
}
|
|
749
|
+
} catch (error) {
|
|
750
|
+
if (error instanceof OidcVaultHttpError) {
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return new Response(text, {
|
|
755
|
+
status: 200,
|
|
756
|
+
headers: { "content-type": "application/json" }
|
|
757
|
+
});
|
|
758
|
+
};
|
|
759
|
+
};
|
|
561
760
|
var resolveJwks = (jwksUri, options = {}) => {
|
|
562
|
-
|
|
761
|
+
const timeoutMs = getProviderRequestTimeoutMs(options);
|
|
762
|
+
const cacheKey = `${jwksUri}\0${timeoutMs}`;
|
|
763
|
+
let jwks = jwksCache.get(cacheKey);
|
|
563
764
|
if (!jwks) {
|
|
564
765
|
jwks = (0, import_jose.createRemoteJWKSet)(new URL(jwksUri), {
|
|
565
|
-
timeoutDuration:
|
|
566
|
-
cacheMaxAge: DEFAULT_DISCOVERY_CACHE_TTL_MS
|
|
766
|
+
timeoutDuration: timeoutMs,
|
|
767
|
+
cacheMaxAge: DEFAULT_DISCOVERY_CACHE_TTL_MS,
|
|
768
|
+
[import_jose.customFetch]: createBoundedJwksFetch(timeoutMs)
|
|
567
769
|
});
|
|
568
|
-
jwksCache.set(
|
|
770
|
+
jwksCache.set(cacheKey, jwks);
|
|
569
771
|
evictOldestEntry(jwksCache, DEFAULT_JWKS_CACHE_CAPACITY);
|
|
570
772
|
}
|
|
571
773
|
return jwks;
|
|
572
774
|
};
|
|
573
775
|
var formEncodeClientCredential = (value) => new URLSearchParams([[value, ""]]).toString().slice(0, -1);
|
|
574
776
|
var toBasicAuthorization = (clientId, clientSecret) => `Basic ${Buffer.from(`${formEncodeClientCredential(clientId)}:${formEncodeClientCredential(clientSecret)}`).toString("base64")}`;
|
|
575
|
-
var
|
|
576
|
-
|
|
777
|
+
var isProviderJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
778
|
+
var readJsonResponse = async (response, errorCode, invalidJsonMessage = "OIDC provider returned invalid JSON.", readOptions = {}) => {
|
|
779
|
+
const text = await readBoundedResponseText(response, DEFAULT_PROVIDER_JSON_BODY_LIMIT, errorCode, {
|
|
780
|
+
signal: readOptions.signal,
|
|
781
|
+
transportErrorCode: readOptions.transportErrorCode
|
|
782
|
+
});
|
|
577
783
|
if (!text) {
|
|
578
784
|
return {};
|
|
579
785
|
}
|
|
786
|
+
let parsed;
|
|
580
787
|
try {
|
|
581
|
-
|
|
788
|
+
parsed = JSON.parse(text);
|
|
582
789
|
} catch {
|
|
583
|
-
throw new OidcVaultHttpError(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
790
|
+
throw new OidcVaultHttpError(502, errorCode, invalidJsonMessage, invalidJsonMessage);
|
|
791
|
+
}
|
|
792
|
+
if (!isProviderJsonObject(parsed)) {
|
|
793
|
+
throw new OidcVaultHttpError(502, errorCode, invalidJsonMessage, invalidJsonMessage);
|
|
794
|
+
}
|
|
795
|
+
return parsed;
|
|
796
|
+
};
|
|
797
|
+
var drainProviderErrorBody = async (response, signal, errorCode) => {
|
|
798
|
+
try {
|
|
799
|
+
await readBoundedResponseText(response, DEFAULT_PROVIDER_JSON_BODY_LIMIT, errorCode, { signal });
|
|
800
|
+
} catch (error) {
|
|
801
|
+
if (error instanceof OidcVaultHttpError) {
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
throw error;
|
|
589
805
|
}
|
|
590
806
|
};
|
|
591
807
|
async function discoverIssuerMetadata(issuer, options) {
|
|
808
|
+
const callerTimeoutMs = getProviderRequestTimeoutMs(options);
|
|
809
|
+
const policyKey = `${issuer}\0${callerTimeoutMs}`;
|
|
592
810
|
const now = Date.now();
|
|
593
|
-
const
|
|
594
|
-
let discoveryPromise =
|
|
595
|
-
if (
|
|
596
|
-
discoveryCache.delete(
|
|
597
|
-
}
|
|
598
|
-
if (
|
|
811
|
+
const policyCached = discoveryCache.get(policyKey);
|
|
812
|
+
let discoveryPromise = policyCached !== void 0 && policyCached.expiresAt > now ? policyCached.promise : void 0;
|
|
813
|
+
if (policyCached !== void 0 && discoveryPromise === void 0) {
|
|
814
|
+
discoveryCache.delete(policyKey);
|
|
815
|
+
}
|
|
816
|
+
if (discoveryPromise === void 0) {
|
|
817
|
+
const sharedCached = discoveryCache.get(issuer);
|
|
818
|
+
const sharedPromise = sharedCached !== void 0 && sharedCached.expiresAt > now ? sharedCached.promise : void 0;
|
|
819
|
+
if (sharedCached !== void 0 && sharedPromise === void 0) {
|
|
820
|
+
discoveryCache.delete(issuer);
|
|
821
|
+
}
|
|
822
|
+
if (sharedPromise !== void 0) {
|
|
823
|
+
return sharedPromise;
|
|
824
|
+
}
|
|
599
825
|
discoveryPromise = (async () => {
|
|
600
826
|
const discoveryUrl = buildWellKnownUrl(issuer);
|
|
601
|
-
const response = await fetchProvider(
|
|
602
|
-
|
|
603
|
-
|
|
827
|
+
const { response, signal, done } = await fetchProvider(
|
|
828
|
+
discoveryUrl,
|
|
829
|
+
void 0,
|
|
830
|
+
"OIDC_VAULT_DISCOVERY_FAILED",
|
|
831
|
+
options
|
|
832
|
+
);
|
|
833
|
+
try {
|
|
834
|
+
if (!response.ok) {
|
|
835
|
+
const errorText = await readBoundedResponseText(
|
|
836
|
+
response,
|
|
837
|
+
DEFAULT_PROVIDER_ERROR_BODY_LIMIT,
|
|
838
|
+
"OIDC_VAULT_DISCOVERY_FAILED",
|
|
839
|
+
{
|
|
840
|
+
truncate: true,
|
|
841
|
+
signal
|
|
842
|
+
}
|
|
843
|
+
);
|
|
844
|
+
throw new OidcVaultHttpError(
|
|
845
|
+
502,
|
|
846
|
+
"OIDC_VAULT_DISCOVERY_FAILED",
|
|
847
|
+
errorText || `OIDC discovery failed with status ${response.status}.`,
|
|
848
|
+
"OIDC discovery failed."
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
const discovered = await readJsonResponse(
|
|
604
852
|
response,
|
|
605
|
-
|
|
606
|
-
"
|
|
607
|
-
{
|
|
608
|
-
truncate: true
|
|
609
|
-
}
|
|
853
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
854
|
+
"OIDC discovery response is malformed JSON.",
|
|
855
|
+
{ signal, transportErrorCode: "OIDC_VAULT_DISCOVERY_FAILED" }
|
|
610
856
|
);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
857
|
+
const authorizationEndpoint = validateDiscoveredHttpUrl(
|
|
858
|
+
getRequiredString(
|
|
859
|
+
discovered.authorization_endpoint,
|
|
860
|
+
"OIDC discovery response is missing authorization_endpoint.",
|
|
861
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
862
|
+
502
|
|
863
|
+
),
|
|
864
|
+
"authorization_endpoint"
|
|
616
865
|
);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
),
|
|
639
|
-
"token_endpoint"
|
|
640
|
-
);
|
|
641
|
-
const jwksUri = validateDiscoveredHttpUrl(
|
|
642
|
-
getRequiredString(
|
|
643
|
-
discovered.jwks_uri,
|
|
644
|
-
"OIDC discovery response is missing jwks_uri.",
|
|
866
|
+
const tokenEndpoint = validateDiscoveredHttpUrl(
|
|
867
|
+
getRequiredString(
|
|
868
|
+
discovered.token_endpoint,
|
|
869
|
+
"OIDC discovery response is missing token_endpoint.",
|
|
870
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
871
|
+
502
|
|
872
|
+
),
|
|
873
|
+
"token_endpoint"
|
|
874
|
+
);
|
|
875
|
+
const jwksUri = validateDiscoveredHttpUrl(
|
|
876
|
+
getRequiredString(
|
|
877
|
+
discovered.jwks_uri,
|
|
878
|
+
"OIDC discovery response is missing jwks_uri.",
|
|
879
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
880
|
+
502
|
|
881
|
+
),
|
|
882
|
+
"jwks_uri"
|
|
883
|
+
);
|
|
884
|
+
const discoveredIssuer = getRequiredString(
|
|
885
|
+
discovered.issuer,
|
|
886
|
+
"OIDC discovery response is missing issuer.",
|
|
645
887
|
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
646
888
|
502
|
|
647
|
-
)
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
};
|
|
889
|
+
);
|
|
890
|
+
assertDiscoveredIssuerMatchesConfig(issuer, discoveredIssuer);
|
|
891
|
+
return {
|
|
892
|
+
issuer: discoveredIssuer,
|
|
893
|
+
authorizationEndpoint,
|
|
894
|
+
tokenEndpoint,
|
|
895
|
+
jwksUri,
|
|
896
|
+
userInfoEndpoint: validateOptionalDiscoveredHttpUrl(discovered.userinfo_endpoint, "userinfo_endpoint"),
|
|
897
|
+
endSessionEndpoint: validateOptionalDiscoveredHttpUrl(
|
|
898
|
+
discovered.end_session_endpoint,
|
|
899
|
+
"end_session_endpoint"
|
|
900
|
+
)
|
|
901
|
+
};
|
|
902
|
+
} finally {
|
|
903
|
+
done();
|
|
904
|
+
cancelBestEffort(() => response.body?.cancel());
|
|
905
|
+
}
|
|
665
906
|
})();
|
|
666
|
-
|
|
907
|
+
discoveryPromise.then(
|
|
908
|
+
() => {
|
|
909
|
+
const fetchedAt = Date.now();
|
|
910
|
+
discoveryCache.set(issuer, {
|
|
911
|
+
expiresAt: fetchedAt + DEFAULT_DISCOVERY_CACHE_TTL_MS,
|
|
912
|
+
promise: discoveryPromise
|
|
913
|
+
});
|
|
914
|
+
evictOldestEntry(discoveryCache, DEFAULT_DISCOVERY_CACHE_CAPACITY);
|
|
915
|
+
},
|
|
916
|
+
() => {
|
|
917
|
+
}
|
|
918
|
+
);
|
|
919
|
+
discoveryCache.set(policyKey, {
|
|
667
920
|
expiresAt: now + DEFAULT_DISCOVERY_CACHE_TTL_MS,
|
|
668
921
|
promise: discoveryPromise
|
|
669
922
|
});
|
|
670
923
|
evictOldestEntry(discoveryCache, DEFAULT_DISCOVERY_CACHE_CAPACITY);
|
|
671
924
|
discoveryPromise.catch(() => {
|
|
672
|
-
if (discoveryCache.get(
|
|
673
|
-
discoveryCache.delete(
|
|
925
|
+
if (discoveryCache.get(policyKey)?.promise === discoveryPromise) {
|
|
926
|
+
discoveryCache.delete(policyKey);
|
|
674
927
|
}
|
|
675
928
|
});
|
|
676
929
|
}
|
|
@@ -721,7 +974,7 @@ async function requestToken(metadata, params, options = {}) {
|
|
|
721
974
|
if (metadata.clientSecret) {
|
|
722
975
|
headers.authorization = toBasicAuthorization(clientId, metadata.clientSecret);
|
|
723
976
|
}
|
|
724
|
-
const response = await fetchProvider(
|
|
977
|
+
const { response, signal, done } = await fetchProvider(
|
|
725
978
|
metadata.tokenEndpoint,
|
|
726
979
|
{
|
|
727
980
|
method: "POST",
|
|
@@ -731,17 +984,33 @@ async function requestToken(metadata, params, options = {}) {
|
|
|
731
984
|
"OIDC_VAULT_TOKEN_REQUEST_FAILED",
|
|
732
985
|
options
|
|
733
986
|
);
|
|
734
|
-
|
|
735
|
-
|
|
987
|
+
try {
|
|
988
|
+
if (!response.ok) {
|
|
989
|
+
await drainProviderErrorBody(response, signal, "OIDC_VAULT_TOKEN_REQUEST_FAILED");
|
|
990
|
+
throw new OidcVaultHttpError(
|
|
991
|
+
502,
|
|
992
|
+
"OIDC_VAULT_TOKEN_REQUEST_FAILED",
|
|
993
|
+
"OIDC token request failed.",
|
|
994
|
+
"OIDC token request failed."
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
const json = await readJsonResponse(response, "OIDC_VAULT_TOKEN_REQUEST_FAILED", void 0, { signal });
|
|
998
|
+
return json;
|
|
999
|
+
} finally {
|
|
1000
|
+
done();
|
|
1001
|
+
cancelBestEffort(() => response.body?.cancel());
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
var assertPresentTokenStringField = (tokenResponse, field) => {
|
|
1005
|
+
const value = tokenResponse[field];
|
|
1006
|
+
if (value !== void 0 && (typeof value !== "string" || value.length === 0)) {
|
|
736
1007
|
throw new OidcVaultHttpError(
|
|
737
1008
|
502,
|
|
738
|
-
"
|
|
739
|
-
|
|
740
|
-
"OIDC token request failed."
|
|
1009
|
+
"OIDC_VAULT_INVALID_TOKEN_RESPONSE",
|
|
1010
|
+
`OIDC token response ${field} must be a non-empty string when present.`
|
|
741
1011
|
);
|
|
742
1012
|
}
|
|
743
|
-
|
|
744
|
-
}
|
|
1013
|
+
};
|
|
745
1014
|
function validateTokenResponse(tokenResponse) {
|
|
746
1015
|
if (typeof tokenResponse.token_type !== "string" || tokenResponse.token_type.toLowerCase() !== "bearer") {
|
|
747
1016
|
throw new OidcVaultHttpError(
|
|
@@ -757,12 +1026,38 @@ function validateTokenResponse(tokenResponse) {
|
|
|
757
1026
|
"OIDC_VAULT_INVALID_TOKEN_RESPONSE"
|
|
758
1027
|
);
|
|
759
1028
|
}
|
|
1029
|
+
assertPresentTokenStringField(tokenResponse, "access_token");
|
|
1030
|
+
assertPresentTokenStringField(tokenResponse, "id_token");
|
|
1031
|
+
assertPresentTokenStringField(tokenResponse, "refresh_token");
|
|
1032
|
+
if (tokenResponse.scope !== void 0 && typeof tokenResponse.scope !== "string") {
|
|
1033
|
+
throw new OidcVaultHttpError(
|
|
1034
|
+
502,
|
|
1035
|
+
"OIDC_VAULT_INVALID_TOKEN_RESPONSE",
|
|
1036
|
+
"OIDC token response scope must be a string when present."
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
function validateCallbackTokenResponse(tokenResponse) {
|
|
1041
|
+
validateTokenResponse(tokenResponse);
|
|
1042
|
+
if (typeof tokenResponse.id_token !== "string" || tokenResponse.id_token.length === 0) {
|
|
1043
|
+
throw new OidcVaultHttpError(502, "OIDC_VAULT_MISSING_ID_TOKEN", "OIDC token response is missing id_token.");
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof tokenResponse.refresh_token !== "string" || tokenResponse.refresh_token.length === 0) {
|
|
1046
|
+
throw new OidcVaultHttpError(
|
|
1047
|
+
502,
|
|
1048
|
+
"OIDC_VAULT_MISSING_REFRESH_TOKEN",
|
|
1049
|
+
"OIDC token response is missing refresh_token."
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function validateRefreshTokenResponse(tokenResponse) {
|
|
1054
|
+
validateTokenResponse(tokenResponse);
|
|
760
1055
|
}
|
|
761
1056
|
async function fetchUserInfo(metadata, accessToken, options = {}) {
|
|
762
1057
|
if (!metadata.userInfoEndpoint) {
|
|
763
1058
|
return {};
|
|
764
1059
|
}
|
|
765
|
-
const response = await fetchProvider(
|
|
1060
|
+
const { response, signal, done } = await fetchProvider(
|
|
766
1061
|
metadata.userInfoEndpoint,
|
|
767
1062
|
{
|
|
768
1063
|
headers: {
|
|
@@ -772,16 +1067,22 @@ async function fetchUserInfo(metadata, accessToken, options = {}) {
|
|
|
772
1067
|
"OIDC_VAULT_USERINFO_FAILED",
|
|
773
1068
|
options
|
|
774
1069
|
);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1070
|
+
try {
|
|
1071
|
+
if (!response.ok) {
|
|
1072
|
+
await drainProviderErrorBody(response, signal, "OIDC_VAULT_USERINFO_FAILED");
|
|
1073
|
+
throw new OidcVaultHttpError(
|
|
1074
|
+
502,
|
|
1075
|
+
"OIDC_VAULT_USERINFO_FAILED",
|
|
1076
|
+
"OIDC userinfo request failed.",
|
|
1077
|
+
"OIDC userinfo request failed."
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
const json = await readJsonResponse(response, "OIDC_VAULT_USERINFO_FAILED", void 0, { signal });
|
|
1081
|
+
return json;
|
|
1082
|
+
} finally {
|
|
1083
|
+
done();
|
|
1084
|
+
cancelBestEffort(() => response.body?.cancel());
|
|
783
1085
|
}
|
|
784
|
-
return json;
|
|
785
1086
|
}
|
|
786
1087
|
|
|
787
1088
|
// src/token-validation.ts
|
|
@@ -811,6 +1112,9 @@ var defaultJwtClaimsMapper = (claims) => {
|
|
|
811
1112
|
};
|
|
812
1113
|
};
|
|
813
1114
|
var assertUserInfoSubject = (userInfo, subject) => {
|
|
1115
|
+
if (typeof userInfo !== "object" || userInfo === null || Array.isArray(userInfo)) {
|
|
1116
|
+
throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_USERINFO", "OIDC userinfo response is invalid.");
|
|
1117
|
+
}
|
|
814
1118
|
if (!isString(userInfo.sub)) {
|
|
815
1119
|
throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_USERINFO", "OIDC userinfo response is missing sub.");
|
|
816
1120
|
}
|
|
@@ -823,6 +1127,12 @@ var mergeUserProfile = (sub, idTokenClaims, userInfo) => ({
|
|
|
823
1127
|
...userInfo ?? {},
|
|
824
1128
|
sub
|
|
825
1129
|
});
|
|
1130
|
+
var composeRefreshedUserProfile = (sub, currentUser, freshIdClaims, freshUserInfo) => {
|
|
1131
|
+
if (freshIdClaims === void 0) {
|
|
1132
|
+
return mergeUserProfile(sub, { ...currentUser ?? { sub } }, freshUserInfo);
|
|
1133
|
+
}
|
|
1134
|
+
return mergeUserProfile(sub, { ...freshIdClaims }, freshUserInfo);
|
|
1135
|
+
};
|
|
826
1136
|
async function verifyIdToken(metadata, idToken, nonce, options = {}) {
|
|
827
1137
|
if (!metadata.jwksUri) {
|
|
828
1138
|
throw new OidcVaultHttpError(500, "OIDC_VAULT_MISSING_JWKS_URI", "OIDC jwksUri is required to validate id_token.");
|
|
@@ -963,10 +1273,17 @@ var extractBearerToken = (authorizationHeader) => {
|
|
|
963
1273
|
var setBearerChallengeHeader = (res) => {
|
|
964
1274
|
res.setHeader("WWW-Authenticate", "Bearer");
|
|
965
1275
|
};
|
|
1276
|
+
var notifyBearerMiddlewareError = async (options, context) => {
|
|
1277
|
+
try {
|
|
1278
|
+
await options.onError?.(context);
|
|
1279
|
+
} catch {
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
966
1282
|
function createOidcVaultAccessTokenMiddleware(options) {
|
|
967
1283
|
return async (req, res, next) => {
|
|
1284
|
+
let token;
|
|
968
1285
|
try {
|
|
969
|
-
|
|
1286
|
+
token = extractBearerToken(req.get("authorization"));
|
|
970
1287
|
const validationResult = await options.validator.validate(token);
|
|
971
1288
|
const auth = {
|
|
972
1289
|
token,
|
|
@@ -974,9 +1291,38 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
974
1291
|
};
|
|
975
1292
|
const authenticatedRequest = req;
|
|
976
1293
|
authenticatedRequest.auth = auth;
|
|
977
|
-
|
|
1294
|
+
try {
|
|
1295
|
+
await options.onAuthContext?.({ req: authenticatedRequest, res, auth });
|
|
1296
|
+
} catch (hookError) {
|
|
1297
|
+
delete authenticatedRequest.auth;
|
|
1298
|
+
await notifyBearerMiddlewareError(options, { error: hookError, req, res, token, auth });
|
|
1299
|
+
if (res.headersSent) {
|
|
1300
|
+
next(hookError);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (hookError instanceof OidcVaultHttpError) {
|
|
1304
|
+
if (hookError.status === 401) {
|
|
1305
|
+
setBearerChallengeHeader(res);
|
|
1306
|
+
}
|
|
1307
|
+
res.status(hookError.status).json({
|
|
1308
|
+
code: hookError.code,
|
|
1309
|
+
message: hookError.clientMessage
|
|
1310
|
+
});
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
res.status(500).json({
|
|
1314
|
+
code: "OIDC_VAULT_AUTH_CONTEXT_FAILED",
|
|
1315
|
+
message: "Auth context hook failed."
|
|
1316
|
+
});
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
978
1319
|
next();
|
|
979
1320
|
} catch (error) {
|
|
1321
|
+
await notifyBearerMiddlewareError(options, { error, req, res, ...token ? { token } : {} });
|
|
1322
|
+
if (res.headersSent) {
|
|
1323
|
+
next(error);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
980
1326
|
if (error instanceof OidcVaultHttpError) {
|
|
981
1327
|
setBearerChallengeHeader(res);
|
|
982
1328
|
res.status(error.status).json({
|
|
@@ -1002,28 +1348,43 @@ function normalizeOidcVaultBasePath(value) {
|
|
|
1002
1348
|
return `/${value.replace(/^\/+|\/+$/g, "")}`;
|
|
1003
1349
|
}
|
|
1004
1350
|
var getNow = (options) => (options.now ?? Date.now)();
|
|
1351
|
+
var CREDENTIAL_RESPONSE_CACHE_CONTROL = "no-store";
|
|
1352
|
+
var applyCredentialResponseCachePolicy = (res) => {
|
|
1353
|
+
res.setHeader("Cache-Control", CREDENTIAL_RESPONSE_CACHE_CONTROL);
|
|
1354
|
+
};
|
|
1005
1355
|
var createOpaqueId = (prefix) => `${prefix}_${(0, import_node_crypto.randomBytes)(16).toString("base64url")}`;
|
|
1006
1356
|
var createPkceVerifier = () => (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
1007
1357
|
var createPkceChallenge = (verifier) => (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url");
|
|
1008
1358
|
var getCallbackUri = (backendOrigin, basePath) => `${backendOrigin}${basePath}${OIDC_VAULT_ROUTE_PATHS.callback}`;
|
|
1009
1359
|
var validateOidcVaultOptions = (options) => {
|
|
1360
|
+
validateLifetimeOptions(options, getNow(options));
|
|
1010
1361
|
const backendOrigin = resolveBackendOrigin(options);
|
|
1011
1362
|
const frontendRedirectUri = resolveFrontendRedirectUri(options);
|
|
1012
1363
|
validatePostLogoutRedirectUri(options);
|
|
1013
1364
|
validateCookieOptions(options);
|
|
1014
|
-
const config = resolveOidcVaultConfig(options.config);
|
|
1365
|
+
const config = resolveOidcVaultConfig(options.config ? { ...options.config } : void 0);
|
|
1015
1366
|
const configuredTrustedOrigins = resolveTrustedOrigins(options);
|
|
1016
1367
|
if (usesCrossSiteCookieTransport(options) && configuredTrustedOrigins.size === 0) {
|
|
1017
1368
|
throw new Error("trustedOrigins is required when using cross-site cookie transport.");
|
|
1018
1369
|
}
|
|
1019
1370
|
const trustedOrigins = new Set(configuredTrustedOrigins);
|
|
1020
1371
|
trustedOrigins.add(backendOrigin);
|
|
1021
|
-
|
|
1022
|
-
options
|
|
1023
|
-
|
|
1024
|
-
|
|
1372
|
+
const resolvedOptions = {
|
|
1373
|
+
...options,
|
|
1374
|
+
...frontendRedirectUri !== void 0 ? { frontendRedirectUri } : {},
|
|
1375
|
+
...options.trustedOrigins ? { trustedOrigins: [...options.trustedOrigins] } : {},
|
|
1376
|
+
...options.cookie ? { cookie: { ...options.cookie } } : {},
|
|
1377
|
+
...options.config ? { config: { ...options.config } } : {}
|
|
1378
|
+
};
|
|
1379
|
+
return { backendOrigin, config, trustedOrigins, resolvedOptions };
|
|
1025
1380
|
};
|
|
1026
1381
|
var isStoreConflictError = (error) => error instanceof OidcVaultStoreConflictError || typeof error === "object" && error !== null && "name" in error && error.name === "OidcVaultStoreConflictError";
|
|
1382
|
+
var assertSessionIdentity = (session, config) => {
|
|
1383
|
+
const provider = session.provider;
|
|
1384
|
+
if (provider?.issuer !== void 0 && provider.issuer !== config.issuer || provider?.clientId !== void 0 && provider.clientId !== config.clientId) {
|
|
1385
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1027
1388
|
var getSessionIdFromRequest = (req, options, action) => {
|
|
1028
1389
|
if (usesCookieTransport(options)) {
|
|
1029
1390
|
const cookieSessionId = getSessionIdFromCookie(req, options);
|
|
@@ -1046,7 +1407,9 @@ var getSessionIdFromRequest = (req, options, action) => {
|
|
|
1046
1407
|
var createExchangeResponse = (options, session, issuedToken) => ({
|
|
1047
1408
|
sessionId: usesCookieTransport(options) ? void 0 : session.sessionId,
|
|
1048
1409
|
user: session.user,
|
|
1049
|
-
|
|
1410
|
+
accessToken: issuedToken.accessToken,
|
|
1411
|
+
expiresIn: issuedToken.expiresIn,
|
|
1412
|
+
tokenType: issuedToken.tokenType
|
|
1050
1413
|
});
|
|
1051
1414
|
var appendQueryParam = (url, name, value) => {
|
|
1052
1415
|
if (value) {
|
|
@@ -1079,6 +1442,10 @@ var getLogoutTokenFromRequest = (req) => {
|
|
|
1079
1442
|
"OIDC_VAULT_MISSING_LOGOUT_TOKEN"
|
|
1080
1443
|
);
|
|
1081
1444
|
};
|
|
1445
|
+
var buildBackchannelLogoutReplayKey = (input) => {
|
|
1446
|
+
const encode = (value) => Buffer.from(value, "utf8").toString("base64url");
|
|
1447
|
+
return `v2:${encode(input.issuer ?? "")}:${encode(input.clientId ?? "")}:${encode(input.jti)}`;
|
|
1448
|
+
};
|
|
1082
1449
|
var createHookContext = (route, req, res, session, metadata) => ({ route, req, res, session, metadata });
|
|
1083
1450
|
var resolveReturnTo = (req, options) => {
|
|
1084
1451
|
const rawReturnTo = req.query.returnTo;
|
|
@@ -1118,7 +1485,21 @@ var withIssuedToken = async (req, res, options, session) => {
|
|
|
1118
1485
|
if (!options.tokenIssuer) {
|
|
1119
1486
|
return {};
|
|
1120
1487
|
}
|
|
1121
|
-
|
|
1488
|
+
const result = await options.tokenIssuer.issue({ req, res, session });
|
|
1489
|
+
if (typeof result !== "object" || result === null || Array.isArray(result)) {
|
|
1490
|
+
throw new TypeError("tokenIssuer.issue must return a token result object.");
|
|
1491
|
+
}
|
|
1492
|
+
const { accessToken, expiresIn, tokenType } = result;
|
|
1493
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
1494
|
+
throw new TypeError("tokenIssuer.issue accessToken must be a nonempty string.");
|
|
1495
|
+
}
|
|
1496
|
+
if (typeof expiresIn !== "number" || !Number.isSafeInteger(expiresIn) || expiresIn < 0) {
|
|
1497
|
+
throw new TypeError("tokenIssuer.issue expiresIn must be a finite nonnegative safe integer.");
|
|
1498
|
+
}
|
|
1499
|
+
if (tokenType !== void 0 && tokenType !== "Bearer") {
|
|
1500
|
+
throw new TypeError("tokenIssuer.issue tokenType must be Bearer when provided.");
|
|
1501
|
+
}
|
|
1502
|
+
return { accessToken, expiresIn, ...tokenType === void 0 ? {} : { tokenType } };
|
|
1122
1503
|
};
|
|
1123
1504
|
var resolveFrontendRedirectUri2 = (transaction, options) => {
|
|
1124
1505
|
const redirectUri = transaction.returnTo ?? options.frontendRedirectUri;
|
|
@@ -1166,6 +1547,7 @@ async function handleRouteError(route, req, res, next, options, error) {
|
|
|
1166
1547
|
next(error);
|
|
1167
1548
|
return;
|
|
1168
1549
|
}
|
|
1550
|
+
applyCredentialResponseCachePolicy(res);
|
|
1169
1551
|
const payload = toErrorPayload(error);
|
|
1170
1552
|
res.status(payload.status).json({
|
|
1171
1553
|
code: payload.code,
|
|
@@ -1187,6 +1569,7 @@ function createBodyParserErrorHandler() {
|
|
|
1187
1569
|
next(error);
|
|
1188
1570
|
return;
|
|
1189
1571
|
}
|
|
1572
|
+
applyCredentialResponseCachePolicy(res);
|
|
1190
1573
|
const payload = toBodyParserErrorPayload(error);
|
|
1191
1574
|
res.status(payload.status).json({
|
|
1192
1575
|
code: payload.code,
|
|
@@ -1206,7 +1589,11 @@ var createLoginHandler = (options, config, backendOrigin, basePath) => createAsy
|
|
|
1206
1589
|
codeChallenge: createPkceChallenge(pkceVerifier),
|
|
1207
1590
|
returnTo: resolveReturnTo(req, options),
|
|
1208
1591
|
createdAt: now,
|
|
1209
|
-
expiresAt:
|
|
1592
|
+
expiresAt: computeExpiresAt(
|
|
1593
|
+
now,
|
|
1594
|
+
options.authorizationTransactionTtlMs ?? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS,
|
|
1595
|
+
"authorizationTransactionTtlMs"
|
|
1596
|
+
)
|
|
1210
1597
|
};
|
|
1211
1598
|
await options.storeProvider.createAuthorizationTransaction(transaction);
|
|
1212
1599
|
const authorizationUrl = buildAuthorizationUrl(metadata, transaction, getCallbackUri(backendOrigin, basePath));
|
|
@@ -1230,6 +1617,7 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1230
1617
|
if (!transaction) {
|
|
1231
1618
|
throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_STATE", "OIDC state is invalid or expired.");
|
|
1232
1619
|
}
|
|
1620
|
+
const frontendDestination = resolveFrontendRedirectUri2(transaction, options);
|
|
1233
1621
|
const metadata = await resolveProviderMetadata(config, options);
|
|
1234
1622
|
const tokenResponse = await requestToken(
|
|
1235
1623
|
metadata,
|
|
@@ -1241,17 +1629,7 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1241
1629
|
},
|
|
1242
1630
|
options
|
|
1243
1631
|
);
|
|
1244
|
-
|
|
1245
|
-
if (!isString(tokenResponse.id_token)) {
|
|
1246
|
-
throw new OidcVaultHttpError(502, "OIDC_VAULT_MISSING_ID_TOKEN", "OIDC token response is missing id_token.");
|
|
1247
|
-
}
|
|
1248
|
-
if (!isString(tokenResponse.refresh_token)) {
|
|
1249
|
-
throw new OidcVaultHttpError(
|
|
1250
|
-
502,
|
|
1251
|
-
"OIDC_VAULT_MISSING_REFRESH_TOKEN",
|
|
1252
|
-
"OIDC token response is missing refresh_token."
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1632
|
+
validateCallbackTokenResponse(tokenResponse);
|
|
1255
1633
|
await callHook("callback", options.hooks?.onCallbackTokens, req, res, void 0, {
|
|
1256
1634
|
hasAccessToken: Boolean(tokenResponse.access_token),
|
|
1257
1635
|
hasRefreshToken: true,
|
|
@@ -1260,13 +1638,19 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1260
1638
|
const claims = await verifyIdToken(metadata, tokenResponse.id_token, transaction.nonce, options);
|
|
1261
1639
|
const subject = getRequiredString(claims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502);
|
|
1262
1640
|
const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
|
|
1263
|
-
if (userInfo) {
|
|
1641
|
+
if (userInfo !== void 0) {
|
|
1264
1642
|
assertUserInfoSubject(userInfo, subject);
|
|
1265
1643
|
}
|
|
1266
|
-
if (userInfo) {
|
|
1644
|
+
if (userInfo !== void 0) {
|
|
1267
1645
|
await callHook("callback", options.hooks?.onUserInfo, req, res, void 0, { subject });
|
|
1268
1646
|
}
|
|
1269
1647
|
const now = getNow(options);
|
|
1648
|
+
const sessionExpiresAt = options.sessionTtlMs === void 0 ? void 0 : computeExpiresAt(now, options.sessionTtlMs, "sessionTtlMs");
|
|
1649
|
+
const exchangeExpiresAt = computeExpiresAt(
|
|
1650
|
+
now,
|
|
1651
|
+
options.exchangeCodeTtlMs ?? DEFAULT_EXCHANGE_CODE_TTL_MS,
|
|
1652
|
+
"exchangeCodeTtlMs"
|
|
1653
|
+
);
|
|
1270
1654
|
const sessionId = createOpaqueId("sess");
|
|
1271
1655
|
const session = {
|
|
1272
1656
|
sessionId,
|
|
@@ -1283,12 +1667,16 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1283
1667
|
scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : metadata.scopes,
|
|
1284
1668
|
createdAt: now,
|
|
1285
1669
|
updatedAt: now,
|
|
1670
|
+
...sessionExpiresAt === void 0 ? {} : { expiresAt: sessionExpiresAt },
|
|
1286
1671
|
user: mergeUserProfile(subject, claims, userInfo),
|
|
1287
1672
|
metadata: {
|
|
1288
1673
|
tokenType: tokenResponse.token_type
|
|
1289
1674
|
}
|
|
1290
1675
|
};
|
|
1291
1676
|
await callHook("callback", options.hooks?.onBeforeSessionCreate, req, res, session, { subject });
|
|
1677
|
+
if (sessionExpiresAt !== void 0) {
|
|
1678
|
+
session.expiresAt = isUsableEpochMs(session.expiresAt) ? Math.min(session.expiresAt, sessionExpiresAt) : sessionExpiresAt;
|
|
1679
|
+
}
|
|
1292
1680
|
const createdSession = await options.storeProvider.createSession(session);
|
|
1293
1681
|
const exchangeCode = createOpaqueId("code");
|
|
1294
1682
|
try {
|
|
@@ -1297,7 +1685,7 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1297
1685
|
sessionId: createdSession.sessionId,
|
|
1298
1686
|
returnTo: transaction.returnTo,
|
|
1299
1687
|
createdAt: now,
|
|
1300
|
-
expiresAt:
|
|
1688
|
+
expiresAt: exchangeExpiresAt
|
|
1301
1689
|
});
|
|
1302
1690
|
} catch (error) {
|
|
1303
1691
|
await options.storeProvider.deleteSessionsByLogicalSessionId({
|
|
@@ -1308,9 +1696,9 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1308
1696
|
await callPostCommitHook("callback", options, options.hooks?.onSessionCreated, req, res, createdSession, {
|
|
1309
1697
|
subject
|
|
1310
1698
|
});
|
|
1311
|
-
res.redirect(302, appendCodeToRedirectUri(
|
|
1699
|
+
res.redirect(302, appendCodeToRedirectUri(frontendDestination, exchangeCode));
|
|
1312
1700
|
});
|
|
1313
|
-
var createExchangeHandler = (options) => createAsyncHandler("exchange", options, async (req, res) => {
|
|
1701
|
+
var createExchangeHandler = (options, config) => createAsyncHandler("exchange", options, async (req, res) => {
|
|
1314
1702
|
const body = getBody(req);
|
|
1315
1703
|
const code = getRequiredString(body.code, "Exchange request is missing code.", "OIDC_VAULT_MISSING_EXCHANGE_CODE");
|
|
1316
1704
|
const exchangeRecord = await options.storeProvider.consumeExchangeCode(code);
|
|
@@ -1321,6 +1709,7 @@ var createExchangeHandler = (options) => createAsyncHandler("exchange", options,
|
|
|
1321
1709
|
if (!session) {
|
|
1322
1710
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
1323
1711
|
}
|
|
1712
|
+
assertSessionIdentity(session, config);
|
|
1324
1713
|
let issuedToken;
|
|
1325
1714
|
try {
|
|
1326
1715
|
issuedToken = await withIssuedToken(req, res, options, session);
|
|
@@ -1349,6 +1738,7 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
|
|
|
1349
1738
|
}
|
|
1350
1739
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
1351
1740
|
}
|
|
1741
|
+
assertSessionIdentity(currentSession, config);
|
|
1352
1742
|
const metadata = await resolveProviderMetadata(config, options);
|
|
1353
1743
|
const tokenResponse = await requestToken(
|
|
1354
1744
|
metadata,
|
|
@@ -1358,16 +1748,16 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
|
|
|
1358
1748
|
},
|
|
1359
1749
|
options
|
|
1360
1750
|
);
|
|
1361
|
-
|
|
1751
|
+
validateRefreshTokenResponse(tokenResponse);
|
|
1362
1752
|
const newIdToken = isString(tokenResponse.id_token) ? tokenResponse.id_token : void 0;
|
|
1363
1753
|
const idToken = newIdToken ?? currentSession.idToken;
|
|
1364
|
-
const
|
|
1365
|
-
const subject =
|
|
1754
|
+
const freshClaims = newIdToken ? await verifyIdToken(metadata, newIdToken, void 0, options) : void 0;
|
|
1755
|
+
const subject = freshClaims ? getRequiredString(freshClaims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502) : currentSession.subject;
|
|
1366
1756
|
if (newIdToken && subject !== currentSession.subject) {
|
|
1367
1757
|
throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC refreshed id_token sub changed.");
|
|
1368
1758
|
}
|
|
1369
1759
|
const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
|
|
1370
|
-
if (userInfo) {
|
|
1760
|
+
if (userInfo !== void 0) {
|
|
1371
1761
|
assertUserInfoSubject(userInfo, subject);
|
|
1372
1762
|
}
|
|
1373
1763
|
const now = getNow(options);
|
|
@@ -1377,14 +1767,14 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
|
|
|
1377
1767
|
sessionId: createOpaqueId("sess"),
|
|
1378
1768
|
logicalSessionId,
|
|
1379
1769
|
subject,
|
|
1380
|
-
providerSessionId: typeof
|
|
1770
|
+
providerSessionId: freshClaims && typeof freshClaims.sid === "string" ? freshClaims.sid : currentSession.providerSessionId,
|
|
1381
1771
|
refreshToken: isString(tokenResponse.refresh_token) ? tokenResponse.refresh_token : currentSession.refreshToken,
|
|
1382
1772
|
idToken,
|
|
1383
1773
|
accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : currentSession.accessToken,
|
|
1384
1774
|
scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : currentSession.scope,
|
|
1385
1775
|
expiresAt: currentSession.expiresAt,
|
|
1386
1776
|
updatedAt: now,
|
|
1387
|
-
user:
|
|
1777
|
+
user: composeRefreshedUserProfile(subject, currentSession.user, freshClaims, userInfo)
|
|
1388
1778
|
};
|
|
1389
1779
|
let rotatedSession;
|
|
1390
1780
|
try {
|
|
@@ -1436,6 +1826,7 @@ var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandle
|
|
|
1436
1826
|
res.status(200).json({ loggedOut: true });
|
|
1437
1827
|
return;
|
|
1438
1828
|
}
|
|
1829
|
+
assertSessionIdentity(session, config);
|
|
1439
1830
|
await callHook("logout", options.hooks?.onBeforeLogout, req, res, session, void 0);
|
|
1440
1831
|
await options.storeProvider.deleteSessionsByLogicalSessionId({
|
|
1441
1832
|
logicalSessionId: session.logicalSessionId ?? session.sessionId
|
|
@@ -1443,8 +1834,24 @@ var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandle
|
|
|
1443
1834
|
if (usesCookieTransport(options)) {
|
|
1444
1835
|
clearSessionCookie(res, options);
|
|
1445
1836
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1837
|
+
if (!redirect) {
|
|
1838
|
+
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, session);
|
|
1839
|
+
res.status(200).json({ loggedOut: true });
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
let upstreamLogoutUrl;
|
|
1843
|
+
try {
|
|
1844
|
+
const metadata = await resolveProviderMetadata(config, options);
|
|
1845
|
+
upstreamLogoutUrl = metadata.endSessionEndpoint ? buildLogoutUrl(metadata.endSessionEndpoint, session.idToken, options.postLogoutRedirectUri) : void 0;
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
try {
|
|
1848
|
+
await options.hooks?.onError?.({
|
|
1849
|
+
...createHookContext("logout", req, res, session),
|
|
1850
|
+
error
|
|
1851
|
+
});
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1448
1855
|
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, session);
|
|
1449
1856
|
if (redirect && upstreamLogoutUrl) {
|
|
1450
1857
|
res.redirect(302, upstreamLogoutUrl);
|
|
@@ -1458,26 +1865,40 @@ var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("ba
|
|
|
1458
1865
|
const metadata = await resolveProviderMetadata(config, options);
|
|
1459
1866
|
const logoutToken = getLogoutTokenFromRequest(req);
|
|
1460
1867
|
const claims = await verifyBackchannelLogoutToken(metadata, logoutToken, options);
|
|
1868
|
+
const replayKey = buildBackchannelLogoutReplayKey({
|
|
1869
|
+
issuer: metadata.issuer,
|
|
1870
|
+
clientId: metadata.clientId,
|
|
1871
|
+
jti: claims.jti
|
|
1872
|
+
});
|
|
1461
1873
|
const firstUse = await options.storeProvider.consumeBackchannelLogoutTokenJti({
|
|
1462
|
-
jti:
|
|
1874
|
+
jti: replayKey,
|
|
1463
1875
|
expiresAt: claims.exp * 1e3
|
|
1464
1876
|
});
|
|
1465
|
-
|
|
1466
|
-
res.status(200).json({
|
|
1467
|
-
loggedOut: true,
|
|
1468
|
-
revokedSessions: 0
|
|
1469
|
-
});
|
|
1470
|
-
return;
|
|
1471
|
-
}
|
|
1472
|
-
const revokedSessions = isString(claims.sid) ? await options.storeProvider.deleteSessionsByProviderSessionId({
|
|
1877
|
+
const revokeMatchingSessions = () => isString(claims.sid) ? options.storeProvider.deleteSessionsByProviderSessionId({
|
|
1473
1878
|
providerSessionId: claims.sid,
|
|
1474
1879
|
issuer: metadata.issuer,
|
|
1475
1880
|
clientId: metadata.clientId
|
|
1476
|
-
}) :
|
|
1881
|
+
}) : options.storeProvider.deleteSessionsBySubject({
|
|
1477
1882
|
subject: String(claims.sub),
|
|
1478
1883
|
issuer: metadata.issuer,
|
|
1479
1884
|
clientId: metadata.clientId
|
|
1480
1885
|
});
|
|
1886
|
+
if (!firstUse) {
|
|
1887
|
+
const catchUpRevokedSessions = await revokeMatchingSessions();
|
|
1888
|
+
if (catchUpRevokedSessions > 0) {
|
|
1889
|
+
await callPostCommitHook("backchannel-logout", options, options.hooks?.onLogout, req, res, void 0, {
|
|
1890
|
+
providerSessionId: claims.sid,
|
|
1891
|
+
subject: claims.sub,
|
|
1892
|
+
revokedSessions: catchUpRevokedSessions
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
res.status(200).json({
|
|
1896
|
+
loggedOut: true,
|
|
1897
|
+
revokedSessions: catchUpRevokedSessions
|
|
1898
|
+
});
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
const revokedSessions = await revokeMatchingSessions();
|
|
1481
1902
|
await callPostCommitHook("backchannel-logout", options, options.hooks?.onLogout, req, res, void 0, {
|
|
1482
1903
|
providerSessionId: claims.sid,
|
|
1483
1904
|
subject: claims.sub,
|
|
@@ -1491,17 +1912,21 @@ var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("ba
|
|
|
1491
1912
|
function registerRoutes(router, options, config, trustedOrigins, backendOrigin, basePath) {
|
|
1492
1913
|
router.get(OIDC_VAULT_ROUTE_PATHS.login, createLoginHandler(options, config, backendOrigin, basePath));
|
|
1493
1914
|
router.get(OIDC_VAULT_ROUTE_PATHS.callback, createCallbackHandler(options, config, backendOrigin, basePath));
|
|
1494
|
-
router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options));
|
|
1915
|
+
router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options, config));
|
|
1495
1916
|
router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins));
|
|
1496
1917
|
router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins));
|
|
1497
1918
|
router.post(OIDC_VAULT_ROUTE_PATHS["backchannel-logout"], createBackchannelLogoutHandler(options, config));
|
|
1498
1919
|
}
|
|
1499
1920
|
function createOidcVaultMiddleware(options) {
|
|
1500
|
-
const { backendOrigin, config, trustedOrigins } = validateOidcVaultOptions(options);
|
|
1921
|
+
const { backendOrigin, config, trustedOrigins, resolvedOptions } = validateOidcVaultOptions(options);
|
|
1501
1922
|
const rootRouter = import_express.default.Router();
|
|
1502
1923
|
const baseRouter = import_express.default.Router();
|
|
1503
|
-
const basePath = normalizeOidcVaultBasePath(
|
|
1504
|
-
const requestBodyLimit =
|
|
1924
|
+
const basePath = normalizeOidcVaultBasePath(resolvedOptions.basePath);
|
|
1925
|
+
const requestBodyLimit = resolvedOptions.requestBodyLimit ?? DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT;
|
|
1926
|
+
baseRouter.use((_req, res, next) => {
|
|
1927
|
+
applyCredentialResponseCachePolicy(res);
|
|
1928
|
+
next();
|
|
1929
|
+
});
|
|
1505
1930
|
baseRouter.use(import_express.default.json({ limit: requestBodyLimit }));
|
|
1506
1931
|
baseRouter.use(
|
|
1507
1932
|
import_express.default.urlencoded({
|
|
@@ -1511,7 +1936,7 @@ function createOidcVaultMiddleware(options) {
|
|
|
1511
1936
|
})
|
|
1512
1937
|
);
|
|
1513
1938
|
baseRouter.use(createBodyParserErrorHandler());
|
|
1514
|
-
registerRoutes(baseRouter,
|
|
1939
|
+
registerRoutes(baseRouter, resolvedOptions, config, trustedOrigins, backendOrigin, basePath);
|
|
1515
1940
|
rootRouter.use(basePath, baseRouter);
|
|
1516
1941
|
return rootRouter;
|
|
1517
1942
|
}
|