@web-ts-toolkit/express-oidc-vault 0.43.0 → 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.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import express from "express";
|
|
|
5
5
|
// src/config.ts
|
|
6
6
|
var DEFAULT_OIDC_SCOPES = "openid email profile";
|
|
7
7
|
var MANUAL_CONFIG_REQUIREMENTS = ["issuer", "authorizationEndpoint", "tokenEndpoint", "jwksUri"];
|
|
8
|
+
var MANUAL_CONFIG_SELECTORS = ["authorizationEndpoint", "tokenEndpoint", "jwksUri", "userInfoEndpoint", "endSessionEndpoint"];
|
|
8
9
|
var normalizeOptionalString = (value) => {
|
|
9
10
|
const trimmed = value?.trim();
|
|
10
11
|
return trimmed ? trimmed : void 0;
|
|
@@ -24,19 +25,38 @@ var normalizeHttpUrl = (value, optionName) => {
|
|
|
24
25
|
}
|
|
25
26
|
return url.toString();
|
|
26
27
|
};
|
|
28
|
+
var normalizeIssuerIdentifier = (value, optionName) => {
|
|
29
|
+
if (value === void 0) {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(value);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(`${optionName} must be an absolute HTTP(S) URL.`);
|
|
37
|
+
}
|
|
38
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
|
|
39
|
+
throw new Error(`${optionName} must use http or https.`);
|
|
40
|
+
}
|
|
41
|
+
if (url.username !== "" || url.password !== "") {
|
|
42
|
+
throw new Error(`${optionName} must not include userinfo.`);
|
|
43
|
+
}
|
|
44
|
+
if (url.search !== "" || url.hash !== "") {
|
|
45
|
+
throw new Error(`${optionName} must not include query or fragment.`);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
27
49
|
var withOptionalValue = (target, key, value) => {
|
|
28
50
|
if (value !== void 0) {
|
|
29
51
|
target[key] = value;
|
|
30
52
|
}
|
|
31
53
|
};
|
|
32
54
|
function resolveOidcVaultConfig(config = {}) {
|
|
33
|
-
const issuer =
|
|
55
|
+
const issuer = normalizeIssuerIdentifier(normalizeOptionalString(config.issuer), "config.issuer");
|
|
34
56
|
const clientId = normalizeOptionalString(config.clientId);
|
|
35
57
|
const clientSecret = normalizeOptionalString(config.clientSecret);
|
|
36
58
|
const scopes = normalizeOptionalString(config.scopes) ?? DEFAULT_OIDC_SCOPES;
|
|
37
|
-
const hasManualEndpoints =
|
|
38
|
-
(key) => key !== "issuer" && normalizeOptionalString(config[key])
|
|
39
|
-
);
|
|
59
|
+
const hasManualEndpoints = MANUAL_CONFIG_SELECTORS.some((key) => normalizeOptionalString(config[key]));
|
|
40
60
|
if (!clientId) {
|
|
41
61
|
throw new Error("OIDC clientId is required.");
|
|
42
62
|
}
|
|
@@ -196,37 +216,11 @@ var getRequiredFiniteNonNegativeInteger = (value, message, code, status = 502) =
|
|
|
196
216
|
// src/cookies.ts
|
|
197
217
|
var getSessionTransport = (options) => options.sessionTransport ?? "body";
|
|
198
218
|
var usesCookieTransport = (options) => getSessionTransport(options) === "cookie";
|
|
199
|
-
var parseCookieHeader = (headerValue) => {
|
|
200
|
-
if (!headerValue) {
|
|
201
|
-
return {};
|
|
202
|
-
}
|
|
203
|
-
return headerValue.split(";").reduce((cookies, part) => {
|
|
204
|
-
const separatorIndex = part.indexOf("=");
|
|
205
|
-
if (separatorIndex === -1) {
|
|
206
|
-
return cookies;
|
|
207
|
-
}
|
|
208
|
-
const name = part.slice(0, separatorIndex).trim();
|
|
209
|
-
const value = part.slice(separatorIndex + 1).trim();
|
|
210
|
-
if (!name) {
|
|
211
|
-
return cookies;
|
|
212
|
-
}
|
|
213
|
-
try {
|
|
214
|
-
cookies[name] = decodeURIComponent(value);
|
|
215
|
-
} catch {
|
|
216
|
-
throw new OidcVaultHttpError(
|
|
217
|
-
400,
|
|
218
|
-
"OIDC_VAULT_MALFORMED_SESSION_COOKIE",
|
|
219
|
-
"OIDC vault session cookie is malformed."
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
return cookies;
|
|
223
|
-
}, {});
|
|
224
|
-
};
|
|
225
219
|
var resolveCookieOptions = (options) => {
|
|
226
220
|
const cookieOptions = options.cookie ?? {};
|
|
227
221
|
const deploymentMode = cookieOptions.deploymentMode ?? "same-origin";
|
|
228
222
|
const sameSite = cookieOptions.sameSite ?? (deploymentMode === "cross-site" ? "none" : "lax");
|
|
229
|
-
const secure = cookieOptions.secure ?? (sameSite === "none" || deploymentMode === "cross-site");
|
|
223
|
+
const secure = cookieOptions.secure ?? (sameSite === "none" || deploymentMode === "cross-site" || isHttpsBackendOrigin(options));
|
|
230
224
|
return {
|
|
231
225
|
name: cookieOptions.name ?? DEFAULT_SESSION_COOKIE_NAME,
|
|
232
226
|
sameSite,
|
|
@@ -236,12 +230,24 @@ var resolveCookieOptions = (options) => {
|
|
|
236
230
|
httpOnly: cookieOptions.httpOnly ?? true
|
|
237
231
|
};
|
|
238
232
|
};
|
|
233
|
+
var isEffectivelySecureCookie = (cookieOptions) => cookieOptions.secure || cookieOptions.sameSite === "none";
|
|
234
|
+
var isHttpsBackendOrigin = (options) => {
|
|
235
|
+
try {
|
|
236
|
+
const raw = options.backendOrigin;
|
|
237
|
+
if (typeof raw !== "string") {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return new URL(raw.trim()).protocol === "https:";
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
239
245
|
var COOKIE_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
240
246
|
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])?)*$/;
|
|
241
247
|
var hasUnsafeCookieValueCharacters = (value) => {
|
|
242
248
|
for (let index = 0; index < value.length; index += 1) {
|
|
243
249
|
const code = value.charCodeAt(index);
|
|
244
|
-
if (code
|
|
250
|
+
if (code < 32 || code > 126 || value[index] === ";") {
|
|
245
251
|
return true;
|
|
246
252
|
}
|
|
247
253
|
}
|
|
@@ -264,6 +270,22 @@ var validateCookieOptions = (options) => {
|
|
|
264
270
|
if (!cookieOptions.httpOnly) {
|
|
265
271
|
throw new Error("cookie.httpOnly must be true for cookie session transport.");
|
|
266
272
|
}
|
|
273
|
+
const effectiveSecure = isEffectivelySecureCookie(cookieOptions);
|
|
274
|
+
if (cookieOptions.name.startsWith("__Host-")) {
|
|
275
|
+
if (!effectiveSecure) {
|
|
276
|
+
throw new Error("cookie.name with the __Host- prefix must be Secure.");
|
|
277
|
+
}
|
|
278
|
+
if (cookieOptions.domain !== void 0) {
|
|
279
|
+
throw new Error("cookie.name with the __Host- prefix must not set cookie.domain.");
|
|
280
|
+
}
|
|
281
|
+
if (cookieOptions.path !== "/") {
|
|
282
|
+
throw new Error('cookie.name with the __Host- prefix must use cookie.path "/".');
|
|
283
|
+
}
|
|
284
|
+
} else if (cookieOptions.name.startsWith("__Secure-")) {
|
|
285
|
+
if (!effectiveSecure) {
|
|
286
|
+
throw new Error("cookie.name with the __Secure- prefix must be Secure.");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
267
289
|
};
|
|
268
290
|
var usesCrossSiteCookieTransport = (options) => usesCookieTransport(options) && resolveCookieOptions(options).sameSite === "none";
|
|
269
291
|
var serializeCookie = (name, value, options, overrides) => {
|
|
@@ -301,8 +323,62 @@ var clearSessionCookie = (res, options) => {
|
|
|
301
323
|
);
|
|
302
324
|
};
|
|
303
325
|
var getSessionIdFromCookie = (req, options) => {
|
|
304
|
-
const
|
|
305
|
-
|
|
326
|
+
const raw = parseSelectedCookieValue(req.headers.cookie, resolveCookieOptions(options).name);
|
|
327
|
+
if (raw === void 0) {
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
return decodeURIComponent(raw);
|
|
332
|
+
} catch {
|
|
333
|
+
throw new OidcVaultHttpError(400, "OIDC_VAULT_MALFORMED_SESSION_COOKIE", "OIDC vault session cookie is malformed.");
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var parseSelectedCookieValue = (headerValue, selectedName) => {
|
|
337
|
+
if (!headerValue) {
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
const segments = headerValue.split(";");
|
|
341
|
+
for (const segment of segments) {
|
|
342
|
+
const separatorIndex = segment.indexOf("=");
|
|
343
|
+
if (separatorIndex === -1) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const name = segment.slice(0, separatorIndex).trim();
|
|
347
|
+
if (!name || name !== selectedName) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
return segment.slice(separatorIndex + 1).trim();
|
|
351
|
+
}
|
|
352
|
+
return void 0;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/lifetime-policy.ts
|
|
356
|
+
var MAX_EPOCH_MS = 864e13;
|
|
357
|
+
var isUsableEpochMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && Math.abs(value) <= MAX_EPOCH_MS;
|
|
358
|
+
var computeExpiresAt = (now, ttlMs, name) => {
|
|
359
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) {
|
|
360
|
+
throw new Error(`${name} must be a positive safe integer in milliseconds.`);
|
|
361
|
+
}
|
|
362
|
+
const expiresAt = now + ttlMs;
|
|
363
|
+
if (!isUsableEpochMs(now) || !isUsableEpochMs(expiresAt) || expiresAt <= now) {
|
|
364
|
+
throw new Error(`${name} must produce a usable epoch-millisecond expiry after now.`);
|
|
365
|
+
}
|
|
366
|
+
return expiresAt;
|
|
367
|
+
};
|
|
368
|
+
var validateLifetimeOptions = (options, now) => {
|
|
369
|
+
computeExpiresAt(
|
|
370
|
+
now,
|
|
371
|
+
options.authorizationTransactionTtlMs === void 0 ? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS : options.authorizationTransactionTtlMs,
|
|
372
|
+
"authorizationTransactionTtlMs"
|
|
373
|
+
);
|
|
374
|
+
computeExpiresAt(
|
|
375
|
+
now,
|
|
376
|
+
options.exchangeCodeTtlMs === void 0 ? DEFAULT_EXCHANGE_CODE_TTL_MS : options.exchangeCodeTtlMs,
|
|
377
|
+
"exchangeCodeTtlMs"
|
|
378
|
+
);
|
|
379
|
+
if (options.sessionTtlMs !== void 0) {
|
|
380
|
+
computeExpiresAt(now, options.sessionTtlMs, "sessionTtlMs");
|
|
381
|
+
}
|
|
306
382
|
};
|
|
307
383
|
|
|
308
384
|
// src/utils.ts
|
|
@@ -401,18 +477,36 @@ var assertTrustedOrigin = (req, options, trustedOrigins, action) => {
|
|
|
401
477
|
};
|
|
402
478
|
|
|
403
479
|
// src/provider-client.ts
|
|
404
|
-
import { createRemoteJWKSet } from "jose";
|
|
480
|
+
import { createRemoteJWKSet, customFetch } from "jose";
|
|
405
481
|
var DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 5e3;
|
|
406
482
|
var DEFAULT_DISCOVERY_CACHE_TTL_MS = 10 * 6e4;
|
|
407
483
|
var DEFAULT_DISCOVERY_CACHE_CAPACITY = 32;
|
|
408
484
|
var DEFAULT_JWKS_CACHE_CAPACITY = 32;
|
|
409
485
|
var DEFAULT_PROVIDER_ERROR_BODY_LIMIT = 1024;
|
|
410
486
|
var DEFAULT_PROVIDER_JSON_BODY_LIMIT = 1024 * 1024;
|
|
487
|
+
var DEFAULT_JWKS_BODY_LIMIT = 1024 * 1024;
|
|
488
|
+
var DEFAULT_JWKS_MAX_KEYS = 100;
|
|
411
489
|
var discoveryCache = /* @__PURE__ */ new Map();
|
|
412
490
|
var jwksCache = /* @__PURE__ */ new Map();
|
|
413
|
-
var normalizeIssuerForDiscoveryComparison = (issuer) => issuer.replace(/\/+$/g, "");
|
|
414
491
|
var assertDiscoveredIssuerMatchesConfig = (configuredIssuer, discoveredIssuer) => {
|
|
415
|
-
|
|
492
|
+
let discoveredUrl;
|
|
493
|
+
try {
|
|
494
|
+
discoveredUrl = new URL(discoveredIssuer);
|
|
495
|
+
} catch {
|
|
496
|
+
throw new OidcVaultHttpError(
|
|
497
|
+
502,
|
|
498
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
499
|
+
"OIDC discovery response issuer must be an absolute HTTP(S) URL."
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (discoveredUrl.protocol !== "https:" && discoveredUrl.protocol !== "http:" || discoveredUrl.origin === "null" || discoveredUrl.username !== "" || discoveredUrl.password !== "" || discoveredUrl.search !== "" || discoveredUrl.hash !== "") {
|
|
503
|
+
throw new OidcVaultHttpError(
|
|
504
|
+
502,
|
|
505
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
506
|
+
"OIDC discovery response issuer must be an http(s) URL without userinfo, query, or fragment."
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
if (configuredIssuer !== discoveredIssuer) {
|
|
416
510
|
throw new OidcVaultHttpError(
|
|
417
511
|
502,
|
|
418
512
|
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
@@ -440,6 +534,20 @@ var validateDiscoveredHttpUrl = (value, fieldName) => {
|
|
|
440
534
|
}
|
|
441
535
|
return url.toString();
|
|
442
536
|
};
|
|
537
|
+
var validateOptionalDiscoveredHttpUrl = (value, fieldName) => {
|
|
538
|
+
if (value === void 0) {
|
|
539
|
+
return void 0;
|
|
540
|
+
}
|
|
541
|
+
return validateDiscoveredHttpUrl(
|
|
542
|
+
getRequiredString(
|
|
543
|
+
value,
|
|
544
|
+
`OIDC discovery response ${fieldName} must be a non-empty string when present.`,
|
|
545
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
546
|
+
502
|
|
547
|
+
),
|
|
548
|
+
fieldName
|
|
549
|
+
);
|
|
550
|
+
};
|
|
443
551
|
var buildWellKnownUrl = (issuer) => {
|
|
444
552
|
const normalizedIssuer = issuer.endsWith("/") ? issuer : `${issuer}/`;
|
|
445
553
|
return new URL(".well-known/openid-configuration", normalizedIssuer);
|
|
@@ -464,42 +572,91 @@ var evictOldestEntry = (cache, capacity) => {
|
|
|
464
572
|
cache.delete(oldestKey);
|
|
465
573
|
}
|
|
466
574
|
};
|
|
575
|
+
var isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
576
|
+
var createProviderTimeoutError = (errorCode) => new OidcVaultHttpError(502, errorCode, "OIDC provider request timed out.");
|
|
577
|
+
var createProviderTransportError = (errorCode, cause) => {
|
|
578
|
+
const error = new OidcVaultHttpError(502, errorCode, "OIDC provider request failed.");
|
|
579
|
+
Object.defineProperty(error, "cause", { value: cause, configurable: true, writable: true });
|
|
580
|
+
return error;
|
|
581
|
+
};
|
|
582
|
+
var cancelBestEffort = (cancel) => {
|
|
583
|
+
try {
|
|
584
|
+
void cancel()?.catch(() => {
|
|
585
|
+
});
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
};
|
|
467
589
|
var fetchProvider = async (input, init, errorCode, options) => {
|
|
590
|
+
const timeoutMs = getProviderRequestTimeoutMs(options);
|
|
468
591
|
const controller = new AbortController();
|
|
469
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
592
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
593
|
+
const done = () => clearTimeout(timeout);
|
|
470
594
|
try {
|
|
471
|
-
|
|
595
|
+
const response = await fetch(input, {
|
|
472
596
|
...init,
|
|
473
597
|
redirect: "manual",
|
|
474
598
|
signal: controller.signal
|
|
475
599
|
});
|
|
600
|
+
return { response, signal: controller.signal, done };
|
|
476
601
|
} catch (error) {
|
|
477
|
-
|
|
478
|
-
|
|
602
|
+
done();
|
|
603
|
+
if (controller.signal.aborted || isAbortError(error)) {
|
|
604
|
+
throw createProviderTimeoutError(errorCode);
|
|
479
605
|
}
|
|
480
|
-
throw error;
|
|
481
|
-
} finally {
|
|
482
|
-
clearTimeout(timeout);
|
|
606
|
+
throw createProviderTransportError(errorCode, error);
|
|
483
607
|
}
|
|
484
608
|
};
|
|
609
|
+
var throwBoundedReadTimeout = (errorCode, timeoutAsTimeoutError) => {
|
|
610
|
+
if (timeoutAsTimeoutError) {
|
|
611
|
+
const timeoutError = new Error("JSON Web Key Set request timed out.");
|
|
612
|
+
timeoutError.name = "TimeoutError";
|
|
613
|
+
throw timeoutError;
|
|
614
|
+
}
|
|
615
|
+
throw createProviderTimeoutError(errorCode);
|
|
616
|
+
};
|
|
485
617
|
var readBoundedResponseText = async (response, limit, errorCode, options = {}) => {
|
|
486
618
|
if (!response.body) {
|
|
487
619
|
return "";
|
|
488
620
|
}
|
|
489
621
|
const reader = response.body.getReader();
|
|
490
622
|
const decoder = new TextDecoder();
|
|
623
|
+
const { signal } = options;
|
|
491
624
|
let size = 0;
|
|
492
625
|
let text = "";
|
|
626
|
+
let onAbort;
|
|
627
|
+
let abortPromise;
|
|
628
|
+
if (signal) {
|
|
629
|
+
if (signal.aborted) {
|
|
630
|
+
cancelBestEffort(() => reader.cancel());
|
|
631
|
+
reader.releaseLock();
|
|
632
|
+
throwBoundedReadTimeout(errorCode, options.timeoutAsTimeoutError === true);
|
|
633
|
+
}
|
|
634
|
+
abortPromise = new Promise((_resolve, reject) => {
|
|
635
|
+
onAbort = () => {
|
|
636
|
+
reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
|
|
637
|
+
};
|
|
638
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
639
|
+
});
|
|
640
|
+
abortPromise.catch(() => {
|
|
641
|
+
});
|
|
642
|
+
}
|
|
493
643
|
try {
|
|
494
644
|
for (; ; ) {
|
|
495
|
-
|
|
645
|
+
let result;
|
|
646
|
+
try {
|
|
647
|
+
result = abortPromise === void 0 ? await reader.read() : await Promise.race([reader.read(), abortPromise]);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (signal?.aborted === true || isAbortError(error)) {
|
|
650
|
+
throwBoundedReadTimeout(errorCode, options.timeoutAsTimeoutError === true);
|
|
651
|
+
}
|
|
652
|
+
throw createProviderTransportError(options.transportErrorCode ?? errorCode, error);
|
|
653
|
+
}
|
|
496
654
|
if (result.done) {
|
|
497
655
|
break;
|
|
498
656
|
}
|
|
499
657
|
size += result.value.byteLength;
|
|
500
658
|
if (size > limit) {
|
|
501
|
-
|
|
502
|
-
if (options.truncate) {
|
|
659
|
+
if (options.truncate === true) {
|
|
503
660
|
return `${text.slice(0, limit)}...`;
|
|
504
661
|
}
|
|
505
662
|
throw new OidcVaultHttpError(502, errorCode, "OIDC provider response is too large.");
|
|
@@ -507,123 +664,219 @@ var readBoundedResponseText = async (response, limit, errorCode, options = {}) =
|
|
|
507
664
|
text += decoder.decode(result.value, { stream: true });
|
|
508
665
|
}
|
|
509
666
|
} finally {
|
|
667
|
+
if (signal !== void 0 && onAbort !== void 0) {
|
|
668
|
+
signal.removeEventListener("abort", onAbort);
|
|
669
|
+
}
|
|
670
|
+
cancelBestEffort(() => reader.cancel());
|
|
510
671
|
reader.releaseLock();
|
|
511
672
|
}
|
|
512
673
|
return `${text}${decoder.decode()}`;
|
|
513
674
|
};
|
|
675
|
+
var createBoundedJwksFetch = (timeoutMs) => {
|
|
676
|
+
void timeoutMs;
|
|
677
|
+
return async (url, { headers, method, redirect, signal }) => {
|
|
678
|
+
let response;
|
|
679
|
+
try {
|
|
680
|
+
response = await fetch(url, { method, headers, redirect, signal });
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (signal.aborted || isAbortError(error)) {
|
|
683
|
+
const timeoutError = new Error("JSON Web Key Set request timed out.");
|
|
684
|
+
timeoutError.name = "TimeoutError";
|
|
685
|
+
throw timeoutError;
|
|
686
|
+
}
|
|
687
|
+
throw createProviderTransportError("OIDC_VAULT_JWKS_FAILED", error);
|
|
688
|
+
}
|
|
689
|
+
if (response.status !== 200) {
|
|
690
|
+
cancelBestEffort(() => response.body?.cancel());
|
|
691
|
+
return response;
|
|
692
|
+
}
|
|
693
|
+
const text = await readBoundedResponseText(response, DEFAULT_JWKS_BODY_LIMIT, "OIDC_VAULT_JWKS_FAILED", {
|
|
694
|
+
signal,
|
|
695
|
+
timeoutAsTimeoutError: true
|
|
696
|
+
});
|
|
697
|
+
try {
|
|
698
|
+
const parsed = JSON.parse(text);
|
|
699
|
+
if (Array.isArray(parsed.keys) && parsed.keys.length > DEFAULT_JWKS_MAX_KEYS) {
|
|
700
|
+
throw new OidcVaultHttpError(502, "OIDC_VAULT_JWKS_FAILED", "JSON Web Key Set contains too many keys.");
|
|
701
|
+
}
|
|
702
|
+
} catch (error) {
|
|
703
|
+
if (error instanceof OidcVaultHttpError) {
|
|
704
|
+
throw error;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return new Response(text, {
|
|
708
|
+
status: 200,
|
|
709
|
+
headers: { "content-type": "application/json" }
|
|
710
|
+
});
|
|
711
|
+
};
|
|
712
|
+
};
|
|
514
713
|
var resolveJwks = (jwksUri, options = {}) => {
|
|
515
|
-
|
|
714
|
+
const timeoutMs = getProviderRequestTimeoutMs(options);
|
|
715
|
+
const cacheKey = `${jwksUri}\0${timeoutMs}`;
|
|
716
|
+
let jwks = jwksCache.get(cacheKey);
|
|
516
717
|
if (!jwks) {
|
|
517
718
|
jwks = createRemoteJWKSet(new URL(jwksUri), {
|
|
518
|
-
timeoutDuration:
|
|
519
|
-
cacheMaxAge: DEFAULT_DISCOVERY_CACHE_TTL_MS
|
|
719
|
+
timeoutDuration: timeoutMs,
|
|
720
|
+
cacheMaxAge: DEFAULT_DISCOVERY_CACHE_TTL_MS,
|
|
721
|
+
[customFetch]: createBoundedJwksFetch(timeoutMs)
|
|
520
722
|
});
|
|
521
|
-
jwksCache.set(
|
|
723
|
+
jwksCache.set(cacheKey, jwks);
|
|
522
724
|
evictOldestEntry(jwksCache, DEFAULT_JWKS_CACHE_CAPACITY);
|
|
523
725
|
}
|
|
524
726
|
return jwks;
|
|
525
727
|
};
|
|
526
728
|
var formEncodeClientCredential = (value) => new URLSearchParams([[value, ""]]).toString().slice(0, -1);
|
|
527
729
|
var toBasicAuthorization = (clientId, clientSecret) => `Basic ${Buffer.from(`${formEncodeClientCredential(clientId)}:${formEncodeClientCredential(clientSecret)}`).toString("base64")}`;
|
|
528
|
-
var
|
|
529
|
-
|
|
730
|
+
var isProviderJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
731
|
+
var readJsonResponse = async (response, errorCode, invalidJsonMessage = "OIDC provider returned invalid JSON.", readOptions = {}) => {
|
|
732
|
+
const text = await readBoundedResponseText(response, DEFAULT_PROVIDER_JSON_BODY_LIMIT, errorCode, {
|
|
733
|
+
signal: readOptions.signal,
|
|
734
|
+
transportErrorCode: readOptions.transportErrorCode
|
|
735
|
+
});
|
|
530
736
|
if (!text) {
|
|
531
737
|
return {};
|
|
532
738
|
}
|
|
739
|
+
let parsed;
|
|
533
740
|
try {
|
|
534
|
-
|
|
741
|
+
parsed = JSON.parse(text);
|
|
535
742
|
} catch {
|
|
536
|
-
throw new OidcVaultHttpError(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
743
|
+
throw new OidcVaultHttpError(502, errorCode, invalidJsonMessage, invalidJsonMessage);
|
|
744
|
+
}
|
|
745
|
+
if (!isProviderJsonObject(parsed)) {
|
|
746
|
+
throw new OidcVaultHttpError(502, errorCode, invalidJsonMessage, invalidJsonMessage);
|
|
747
|
+
}
|
|
748
|
+
return parsed;
|
|
749
|
+
};
|
|
750
|
+
var drainProviderErrorBody = async (response, signal, errorCode) => {
|
|
751
|
+
try {
|
|
752
|
+
await readBoundedResponseText(response, DEFAULT_PROVIDER_JSON_BODY_LIMIT, errorCode, { signal });
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (error instanceof OidcVaultHttpError) {
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
throw error;
|
|
542
758
|
}
|
|
543
759
|
};
|
|
544
760
|
async function discoverIssuerMetadata(issuer, options) {
|
|
761
|
+
const callerTimeoutMs = getProviderRequestTimeoutMs(options);
|
|
762
|
+
const policyKey = `${issuer}\0${callerTimeoutMs}`;
|
|
545
763
|
const now = Date.now();
|
|
546
|
-
const
|
|
547
|
-
let discoveryPromise =
|
|
548
|
-
if (
|
|
549
|
-
discoveryCache.delete(
|
|
550
|
-
}
|
|
551
|
-
if (
|
|
764
|
+
const policyCached = discoveryCache.get(policyKey);
|
|
765
|
+
let discoveryPromise = policyCached !== void 0 && policyCached.expiresAt > now ? policyCached.promise : void 0;
|
|
766
|
+
if (policyCached !== void 0 && discoveryPromise === void 0) {
|
|
767
|
+
discoveryCache.delete(policyKey);
|
|
768
|
+
}
|
|
769
|
+
if (discoveryPromise === void 0) {
|
|
770
|
+
const sharedCached = discoveryCache.get(issuer);
|
|
771
|
+
const sharedPromise = sharedCached !== void 0 && sharedCached.expiresAt > now ? sharedCached.promise : void 0;
|
|
772
|
+
if (sharedCached !== void 0 && sharedPromise === void 0) {
|
|
773
|
+
discoveryCache.delete(issuer);
|
|
774
|
+
}
|
|
775
|
+
if (sharedPromise !== void 0) {
|
|
776
|
+
return sharedPromise;
|
|
777
|
+
}
|
|
552
778
|
discoveryPromise = (async () => {
|
|
553
779
|
const discoveryUrl = buildWellKnownUrl(issuer);
|
|
554
|
-
const response = await fetchProvider(
|
|
555
|
-
|
|
556
|
-
|
|
780
|
+
const { response, signal, done } = await fetchProvider(
|
|
781
|
+
discoveryUrl,
|
|
782
|
+
void 0,
|
|
783
|
+
"OIDC_VAULT_DISCOVERY_FAILED",
|
|
784
|
+
options
|
|
785
|
+
);
|
|
786
|
+
try {
|
|
787
|
+
if (!response.ok) {
|
|
788
|
+
const errorText = await readBoundedResponseText(
|
|
789
|
+
response,
|
|
790
|
+
DEFAULT_PROVIDER_ERROR_BODY_LIMIT,
|
|
791
|
+
"OIDC_VAULT_DISCOVERY_FAILED",
|
|
792
|
+
{
|
|
793
|
+
truncate: true,
|
|
794
|
+
signal
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
throw new OidcVaultHttpError(
|
|
798
|
+
502,
|
|
799
|
+
"OIDC_VAULT_DISCOVERY_FAILED",
|
|
800
|
+
errorText || `OIDC discovery failed with status ${response.status}.`,
|
|
801
|
+
"OIDC discovery failed."
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
const discovered = await readJsonResponse(
|
|
557
805
|
response,
|
|
558
|
-
|
|
559
|
-
"
|
|
560
|
-
{
|
|
561
|
-
truncate: true
|
|
562
|
-
}
|
|
806
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
807
|
+
"OIDC discovery response is malformed JSON.",
|
|
808
|
+
{ signal, transportErrorCode: "OIDC_VAULT_DISCOVERY_FAILED" }
|
|
563
809
|
);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
810
|
+
const authorizationEndpoint = validateDiscoveredHttpUrl(
|
|
811
|
+
getRequiredString(
|
|
812
|
+
discovered.authorization_endpoint,
|
|
813
|
+
"OIDC discovery response is missing authorization_endpoint.",
|
|
814
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
815
|
+
502
|
|
816
|
+
),
|
|
817
|
+
"authorization_endpoint"
|
|
569
818
|
);
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
),
|
|
592
|
-
"token_endpoint"
|
|
593
|
-
);
|
|
594
|
-
const jwksUri = validateDiscoveredHttpUrl(
|
|
595
|
-
getRequiredString(
|
|
596
|
-
discovered.jwks_uri,
|
|
597
|
-
"OIDC discovery response is missing jwks_uri.",
|
|
819
|
+
const tokenEndpoint = validateDiscoveredHttpUrl(
|
|
820
|
+
getRequiredString(
|
|
821
|
+
discovered.token_endpoint,
|
|
822
|
+
"OIDC discovery response is missing token_endpoint.",
|
|
823
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
824
|
+
502
|
|
825
|
+
),
|
|
826
|
+
"token_endpoint"
|
|
827
|
+
);
|
|
828
|
+
const jwksUri = validateDiscoveredHttpUrl(
|
|
829
|
+
getRequiredString(
|
|
830
|
+
discovered.jwks_uri,
|
|
831
|
+
"OIDC discovery response is missing jwks_uri.",
|
|
832
|
+
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
833
|
+
502
|
|
834
|
+
),
|
|
835
|
+
"jwks_uri"
|
|
836
|
+
);
|
|
837
|
+
const discoveredIssuer = getRequiredString(
|
|
838
|
+
discovered.issuer,
|
|
839
|
+
"OIDC discovery response is missing issuer.",
|
|
598
840
|
"OIDC_VAULT_DISCOVERY_INVALID",
|
|
599
841
|
502
|
|
600
|
-
)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
};
|
|
842
|
+
);
|
|
843
|
+
assertDiscoveredIssuerMatchesConfig(issuer, discoveredIssuer);
|
|
844
|
+
return {
|
|
845
|
+
issuer: discoveredIssuer,
|
|
846
|
+
authorizationEndpoint,
|
|
847
|
+
tokenEndpoint,
|
|
848
|
+
jwksUri,
|
|
849
|
+
userInfoEndpoint: validateOptionalDiscoveredHttpUrl(discovered.userinfo_endpoint, "userinfo_endpoint"),
|
|
850
|
+
endSessionEndpoint: validateOptionalDiscoveredHttpUrl(
|
|
851
|
+
discovered.end_session_endpoint,
|
|
852
|
+
"end_session_endpoint"
|
|
853
|
+
)
|
|
854
|
+
};
|
|
855
|
+
} finally {
|
|
856
|
+
done();
|
|
857
|
+
cancelBestEffort(() => response.body?.cancel());
|
|
858
|
+
}
|
|
618
859
|
})();
|
|
619
|
-
|
|
860
|
+
discoveryPromise.then(
|
|
861
|
+
() => {
|
|
862
|
+
const fetchedAt = Date.now();
|
|
863
|
+
discoveryCache.set(issuer, {
|
|
864
|
+
expiresAt: fetchedAt + DEFAULT_DISCOVERY_CACHE_TTL_MS,
|
|
865
|
+
promise: discoveryPromise
|
|
866
|
+
});
|
|
867
|
+
evictOldestEntry(discoveryCache, DEFAULT_DISCOVERY_CACHE_CAPACITY);
|
|
868
|
+
},
|
|
869
|
+
() => {
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
discoveryCache.set(policyKey, {
|
|
620
873
|
expiresAt: now + DEFAULT_DISCOVERY_CACHE_TTL_MS,
|
|
621
874
|
promise: discoveryPromise
|
|
622
875
|
});
|
|
623
876
|
evictOldestEntry(discoveryCache, DEFAULT_DISCOVERY_CACHE_CAPACITY);
|
|
624
877
|
discoveryPromise.catch(() => {
|
|
625
|
-
if (discoveryCache.get(
|
|
626
|
-
discoveryCache.delete(
|
|
878
|
+
if (discoveryCache.get(policyKey)?.promise === discoveryPromise) {
|
|
879
|
+
discoveryCache.delete(policyKey);
|
|
627
880
|
}
|
|
628
881
|
});
|
|
629
882
|
}
|
|
@@ -674,7 +927,7 @@ async function requestToken(metadata, params, options = {}) {
|
|
|
674
927
|
if (metadata.clientSecret) {
|
|
675
928
|
headers.authorization = toBasicAuthorization(clientId, metadata.clientSecret);
|
|
676
929
|
}
|
|
677
|
-
const response = await fetchProvider(
|
|
930
|
+
const { response, signal, done } = await fetchProvider(
|
|
678
931
|
metadata.tokenEndpoint,
|
|
679
932
|
{
|
|
680
933
|
method: "POST",
|
|
@@ -684,17 +937,33 @@ async function requestToken(metadata, params, options = {}) {
|
|
|
684
937
|
"OIDC_VAULT_TOKEN_REQUEST_FAILED",
|
|
685
938
|
options
|
|
686
939
|
);
|
|
687
|
-
|
|
688
|
-
|
|
940
|
+
try {
|
|
941
|
+
if (!response.ok) {
|
|
942
|
+
await drainProviderErrorBody(response, signal, "OIDC_VAULT_TOKEN_REQUEST_FAILED");
|
|
943
|
+
throw new OidcVaultHttpError(
|
|
944
|
+
502,
|
|
945
|
+
"OIDC_VAULT_TOKEN_REQUEST_FAILED",
|
|
946
|
+
"OIDC token request failed.",
|
|
947
|
+
"OIDC token request failed."
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
const json = await readJsonResponse(response, "OIDC_VAULT_TOKEN_REQUEST_FAILED", void 0, { signal });
|
|
951
|
+
return json;
|
|
952
|
+
} finally {
|
|
953
|
+
done();
|
|
954
|
+
cancelBestEffort(() => response.body?.cancel());
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
var assertPresentTokenStringField = (tokenResponse, field) => {
|
|
958
|
+
const value = tokenResponse[field];
|
|
959
|
+
if (value !== void 0 && (typeof value !== "string" || value.length === 0)) {
|
|
689
960
|
throw new OidcVaultHttpError(
|
|
690
961
|
502,
|
|
691
|
-
"
|
|
692
|
-
|
|
693
|
-
"OIDC token request failed."
|
|
962
|
+
"OIDC_VAULT_INVALID_TOKEN_RESPONSE",
|
|
963
|
+
`OIDC token response ${field} must be a non-empty string when present.`
|
|
694
964
|
);
|
|
695
965
|
}
|
|
696
|
-
|
|
697
|
-
}
|
|
966
|
+
};
|
|
698
967
|
function validateTokenResponse(tokenResponse) {
|
|
699
968
|
if (typeof tokenResponse.token_type !== "string" || tokenResponse.token_type.toLowerCase() !== "bearer") {
|
|
700
969
|
throw new OidcVaultHttpError(
|
|
@@ -710,12 +979,38 @@ function validateTokenResponse(tokenResponse) {
|
|
|
710
979
|
"OIDC_VAULT_INVALID_TOKEN_RESPONSE"
|
|
711
980
|
);
|
|
712
981
|
}
|
|
982
|
+
assertPresentTokenStringField(tokenResponse, "access_token");
|
|
983
|
+
assertPresentTokenStringField(tokenResponse, "id_token");
|
|
984
|
+
assertPresentTokenStringField(tokenResponse, "refresh_token");
|
|
985
|
+
if (tokenResponse.scope !== void 0 && typeof tokenResponse.scope !== "string") {
|
|
986
|
+
throw new OidcVaultHttpError(
|
|
987
|
+
502,
|
|
988
|
+
"OIDC_VAULT_INVALID_TOKEN_RESPONSE",
|
|
989
|
+
"OIDC token response scope must be a string when present."
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function validateCallbackTokenResponse(tokenResponse) {
|
|
994
|
+
validateTokenResponse(tokenResponse);
|
|
995
|
+
if (typeof tokenResponse.id_token !== "string" || tokenResponse.id_token.length === 0) {
|
|
996
|
+
throw new OidcVaultHttpError(502, "OIDC_VAULT_MISSING_ID_TOKEN", "OIDC token response is missing id_token.");
|
|
997
|
+
}
|
|
998
|
+
if (typeof tokenResponse.refresh_token !== "string" || tokenResponse.refresh_token.length === 0) {
|
|
999
|
+
throw new OidcVaultHttpError(
|
|
1000
|
+
502,
|
|
1001
|
+
"OIDC_VAULT_MISSING_REFRESH_TOKEN",
|
|
1002
|
+
"OIDC token response is missing refresh_token."
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function validateRefreshTokenResponse(tokenResponse) {
|
|
1007
|
+
validateTokenResponse(tokenResponse);
|
|
713
1008
|
}
|
|
714
1009
|
async function fetchUserInfo(metadata, accessToken, options = {}) {
|
|
715
1010
|
if (!metadata.userInfoEndpoint) {
|
|
716
1011
|
return {};
|
|
717
1012
|
}
|
|
718
|
-
const response = await fetchProvider(
|
|
1013
|
+
const { response, signal, done } = await fetchProvider(
|
|
719
1014
|
metadata.userInfoEndpoint,
|
|
720
1015
|
{
|
|
721
1016
|
headers: {
|
|
@@ -725,16 +1020,22 @@ async function fetchUserInfo(metadata, accessToken, options = {}) {
|
|
|
725
1020
|
"OIDC_VAULT_USERINFO_FAILED",
|
|
726
1021
|
options
|
|
727
1022
|
);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1023
|
+
try {
|
|
1024
|
+
if (!response.ok) {
|
|
1025
|
+
await drainProviderErrorBody(response, signal, "OIDC_VAULT_USERINFO_FAILED");
|
|
1026
|
+
throw new OidcVaultHttpError(
|
|
1027
|
+
502,
|
|
1028
|
+
"OIDC_VAULT_USERINFO_FAILED",
|
|
1029
|
+
"OIDC userinfo request failed.",
|
|
1030
|
+
"OIDC userinfo request failed."
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
const json = await readJsonResponse(response, "OIDC_VAULT_USERINFO_FAILED", void 0, { signal });
|
|
1034
|
+
return json;
|
|
1035
|
+
} finally {
|
|
1036
|
+
done();
|
|
1037
|
+
cancelBestEffort(() => response.body?.cancel());
|
|
736
1038
|
}
|
|
737
|
-
return json;
|
|
738
1039
|
}
|
|
739
1040
|
|
|
740
1041
|
// src/token-validation.ts
|
|
@@ -764,6 +1065,9 @@ var defaultJwtClaimsMapper = (claims) => {
|
|
|
764
1065
|
};
|
|
765
1066
|
};
|
|
766
1067
|
var assertUserInfoSubject = (userInfo, subject) => {
|
|
1068
|
+
if (typeof userInfo !== "object" || userInfo === null || Array.isArray(userInfo)) {
|
|
1069
|
+
throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_USERINFO", "OIDC userinfo response is invalid.");
|
|
1070
|
+
}
|
|
767
1071
|
if (!isString(userInfo.sub)) {
|
|
768
1072
|
throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_USERINFO", "OIDC userinfo response is missing sub.");
|
|
769
1073
|
}
|
|
@@ -776,6 +1080,12 @@ var mergeUserProfile = (sub, idTokenClaims, userInfo) => ({
|
|
|
776
1080
|
...userInfo ?? {},
|
|
777
1081
|
sub
|
|
778
1082
|
});
|
|
1083
|
+
var composeRefreshedUserProfile = (sub, currentUser, freshIdClaims, freshUserInfo) => {
|
|
1084
|
+
if (freshIdClaims === void 0) {
|
|
1085
|
+
return mergeUserProfile(sub, { ...currentUser ?? { sub } }, freshUserInfo);
|
|
1086
|
+
}
|
|
1087
|
+
return mergeUserProfile(sub, { ...freshIdClaims }, freshUserInfo);
|
|
1088
|
+
};
|
|
779
1089
|
async function verifyIdToken(metadata, idToken, nonce, options = {}) {
|
|
780
1090
|
if (!metadata.jwksUri) {
|
|
781
1091
|
throw new OidcVaultHttpError(500, "OIDC_VAULT_MISSING_JWKS_URI", "OIDC jwksUri is required to validate id_token.");
|
|
@@ -916,10 +1226,17 @@ var extractBearerToken = (authorizationHeader) => {
|
|
|
916
1226
|
var setBearerChallengeHeader = (res) => {
|
|
917
1227
|
res.setHeader("WWW-Authenticate", "Bearer");
|
|
918
1228
|
};
|
|
1229
|
+
var notifyBearerMiddlewareError = async (options, context) => {
|
|
1230
|
+
try {
|
|
1231
|
+
await options.onError?.(context);
|
|
1232
|
+
} catch {
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
919
1235
|
function createOidcVaultAccessTokenMiddleware(options) {
|
|
920
1236
|
return async (req, res, next) => {
|
|
1237
|
+
let token;
|
|
921
1238
|
try {
|
|
922
|
-
|
|
1239
|
+
token = extractBearerToken(req.get("authorization"));
|
|
923
1240
|
const validationResult = await options.validator.validate(token);
|
|
924
1241
|
const auth = {
|
|
925
1242
|
token,
|
|
@@ -927,9 +1244,38 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
927
1244
|
};
|
|
928
1245
|
const authenticatedRequest = req;
|
|
929
1246
|
authenticatedRequest.auth = auth;
|
|
930
|
-
|
|
1247
|
+
try {
|
|
1248
|
+
await options.onAuthContext?.({ req: authenticatedRequest, res, auth });
|
|
1249
|
+
} catch (hookError) {
|
|
1250
|
+
delete authenticatedRequest.auth;
|
|
1251
|
+
await notifyBearerMiddlewareError(options, { error: hookError, req, res, token, auth });
|
|
1252
|
+
if (res.headersSent) {
|
|
1253
|
+
next(hookError);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (hookError instanceof OidcVaultHttpError) {
|
|
1257
|
+
if (hookError.status === 401) {
|
|
1258
|
+
setBearerChallengeHeader(res);
|
|
1259
|
+
}
|
|
1260
|
+
res.status(hookError.status).json({
|
|
1261
|
+
code: hookError.code,
|
|
1262
|
+
message: hookError.clientMessage
|
|
1263
|
+
});
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
res.status(500).json({
|
|
1267
|
+
code: "OIDC_VAULT_AUTH_CONTEXT_FAILED",
|
|
1268
|
+
message: "Auth context hook failed."
|
|
1269
|
+
});
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
931
1272
|
next();
|
|
932
1273
|
} catch (error) {
|
|
1274
|
+
await notifyBearerMiddlewareError(options, { error, req, res, ...token ? { token } : {} });
|
|
1275
|
+
if (res.headersSent) {
|
|
1276
|
+
next(error);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
933
1279
|
if (error instanceof OidcVaultHttpError) {
|
|
934
1280
|
setBearerChallengeHeader(res);
|
|
935
1281
|
res.status(error.status).json({
|
|
@@ -955,28 +1301,43 @@ function normalizeOidcVaultBasePath(value) {
|
|
|
955
1301
|
return `/${value.replace(/^\/+|\/+$/g, "")}`;
|
|
956
1302
|
}
|
|
957
1303
|
var getNow = (options) => (options.now ?? Date.now)();
|
|
1304
|
+
var CREDENTIAL_RESPONSE_CACHE_CONTROL = "no-store";
|
|
1305
|
+
var applyCredentialResponseCachePolicy = (res) => {
|
|
1306
|
+
res.setHeader("Cache-Control", CREDENTIAL_RESPONSE_CACHE_CONTROL);
|
|
1307
|
+
};
|
|
958
1308
|
var createOpaqueId = (prefix) => `${prefix}_${randomBytes(16).toString("base64url")}`;
|
|
959
1309
|
var createPkceVerifier = () => randomBytes(32).toString("base64url");
|
|
960
1310
|
var createPkceChallenge = (verifier) => createHash("sha256").update(verifier).digest("base64url");
|
|
961
1311
|
var getCallbackUri = (backendOrigin, basePath) => `${backendOrigin}${basePath}${OIDC_VAULT_ROUTE_PATHS.callback}`;
|
|
962
1312
|
var validateOidcVaultOptions = (options) => {
|
|
1313
|
+
validateLifetimeOptions(options, getNow(options));
|
|
963
1314
|
const backendOrigin = resolveBackendOrigin(options);
|
|
964
1315
|
const frontendRedirectUri = resolveFrontendRedirectUri(options);
|
|
965
1316
|
validatePostLogoutRedirectUri(options);
|
|
966
1317
|
validateCookieOptions(options);
|
|
967
|
-
const config = resolveOidcVaultConfig(options.config);
|
|
1318
|
+
const config = resolveOidcVaultConfig(options.config ? { ...options.config } : void 0);
|
|
968
1319
|
const configuredTrustedOrigins = resolveTrustedOrigins(options);
|
|
969
1320
|
if (usesCrossSiteCookieTransport(options) && configuredTrustedOrigins.size === 0) {
|
|
970
1321
|
throw new Error("trustedOrigins is required when using cross-site cookie transport.");
|
|
971
1322
|
}
|
|
972
1323
|
const trustedOrigins = new Set(configuredTrustedOrigins);
|
|
973
1324
|
trustedOrigins.add(backendOrigin);
|
|
974
|
-
|
|
975
|
-
options
|
|
976
|
-
|
|
977
|
-
|
|
1325
|
+
const resolvedOptions = {
|
|
1326
|
+
...options,
|
|
1327
|
+
...frontendRedirectUri !== void 0 ? { frontendRedirectUri } : {},
|
|
1328
|
+
...options.trustedOrigins ? { trustedOrigins: [...options.trustedOrigins] } : {},
|
|
1329
|
+
...options.cookie ? { cookie: { ...options.cookie } } : {},
|
|
1330
|
+
...options.config ? { config: { ...options.config } } : {}
|
|
1331
|
+
};
|
|
1332
|
+
return { backendOrigin, config, trustedOrigins, resolvedOptions };
|
|
978
1333
|
};
|
|
979
1334
|
var isStoreConflictError = (error) => error instanceof OidcVaultStoreConflictError || typeof error === "object" && error !== null && "name" in error && error.name === "OidcVaultStoreConflictError";
|
|
1335
|
+
var assertSessionIdentity = (session, config) => {
|
|
1336
|
+
const provider = session.provider;
|
|
1337
|
+
if (provider?.issuer !== void 0 && provider.issuer !== config.issuer || provider?.clientId !== void 0 && provider.clientId !== config.clientId) {
|
|
1338
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
980
1341
|
var getSessionIdFromRequest = (req, options, action) => {
|
|
981
1342
|
if (usesCookieTransport(options)) {
|
|
982
1343
|
const cookieSessionId = getSessionIdFromCookie(req, options);
|
|
@@ -999,7 +1360,9 @@ var getSessionIdFromRequest = (req, options, action) => {
|
|
|
999
1360
|
var createExchangeResponse = (options, session, issuedToken) => ({
|
|
1000
1361
|
sessionId: usesCookieTransport(options) ? void 0 : session.sessionId,
|
|
1001
1362
|
user: session.user,
|
|
1002
|
-
|
|
1363
|
+
accessToken: issuedToken.accessToken,
|
|
1364
|
+
expiresIn: issuedToken.expiresIn,
|
|
1365
|
+
tokenType: issuedToken.tokenType
|
|
1003
1366
|
});
|
|
1004
1367
|
var appendQueryParam = (url, name, value) => {
|
|
1005
1368
|
if (value) {
|
|
@@ -1032,6 +1395,10 @@ var getLogoutTokenFromRequest = (req) => {
|
|
|
1032
1395
|
"OIDC_VAULT_MISSING_LOGOUT_TOKEN"
|
|
1033
1396
|
);
|
|
1034
1397
|
};
|
|
1398
|
+
var buildBackchannelLogoutReplayKey = (input) => {
|
|
1399
|
+
const encode = (value) => Buffer.from(value, "utf8").toString("base64url");
|
|
1400
|
+
return `v2:${encode(input.issuer ?? "")}:${encode(input.clientId ?? "")}:${encode(input.jti)}`;
|
|
1401
|
+
};
|
|
1035
1402
|
var createHookContext = (route, req, res, session, metadata) => ({ route, req, res, session, metadata });
|
|
1036
1403
|
var resolveReturnTo = (req, options) => {
|
|
1037
1404
|
const rawReturnTo = req.query.returnTo;
|
|
@@ -1071,7 +1438,21 @@ var withIssuedToken = async (req, res, options, session) => {
|
|
|
1071
1438
|
if (!options.tokenIssuer) {
|
|
1072
1439
|
return {};
|
|
1073
1440
|
}
|
|
1074
|
-
|
|
1441
|
+
const result = await options.tokenIssuer.issue({ req, res, session });
|
|
1442
|
+
if (typeof result !== "object" || result === null || Array.isArray(result)) {
|
|
1443
|
+
throw new TypeError("tokenIssuer.issue must return a token result object.");
|
|
1444
|
+
}
|
|
1445
|
+
const { accessToken, expiresIn, tokenType } = result;
|
|
1446
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
1447
|
+
throw new TypeError("tokenIssuer.issue accessToken must be a nonempty string.");
|
|
1448
|
+
}
|
|
1449
|
+
if (typeof expiresIn !== "number" || !Number.isSafeInteger(expiresIn) || expiresIn < 0) {
|
|
1450
|
+
throw new TypeError("tokenIssuer.issue expiresIn must be a finite nonnegative safe integer.");
|
|
1451
|
+
}
|
|
1452
|
+
if (tokenType !== void 0 && tokenType !== "Bearer") {
|
|
1453
|
+
throw new TypeError("tokenIssuer.issue tokenType must be Bearer when provided.");
|
|
1454
|
+
}
|
|
1455
|
+
return { accessToken, expiresIn, ...tokenType === void 0 ? {} : { tokenType } };
|
|
1075
1456
|
};
|
|
1076
1457
|
var resolveFrontendRedirectUri2 = (transaction, options) => {
|
|
1077
1458
|
const redirectUri = transaction.returnTo ?? options.frontendRedirectUri;
|
|
@@ -1119,6 +1500,7 @@ async function handleRouteError(route, req, res, next, options, error) {
|
|
|
1119
1500
|
next(error);
|
|
1120
1501
|
return;
|
|
1121
1502
|
}
|
|
1503
|
+
applyCredentialResponseCachePolicy(res);
|
|
1122
1504
|
const payload = toErrorPayload(error);
|
|
1123
1505
|
res.status(payload.status).json({
|
|
1124
1506
|
code: payload.code,
|
|
@@ -1140,6 +1522,7 @@ function createBodyParserErrorHandler() {
|
|
|
1140
1522
|
next(error);
|
|
1141
1523
|
return;
|
|
1142
1524
|
}
|
|
1525
|
+
applyCredentialResponseCachePolicy(res);
|
|
1143
1526
|
const payload = toBodyParserErrorPayload(error);
|
|
1144
1527
|
res.status(payload.status).json({
|
|
1145
1528
|
code: payload.code,
|
|
@@ -1159,7 +1542,11 @@ var createLoginHandler = (options, config, backendOrigin, basePath) => createAsy
|
|
|
1159
1542
|
codeChallenge: createPkceChallenge(pkceVerifier),
|
|
1160
1543
|
returnTo: resolveReturnTo(req, options),
|
|
1161
1544
|
createdAt: now,
|
|
1162
|
-
expiresAt:
|
|
1545
|
+
expiresAt: computeExpiresAt(
|
|
1546
|
+
now,
|
|
1547
|
+
options.authorizationTransactionTtlMs ?? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS,
|
|
1548
|
+
"authorizationTransactionTtlMs"
|
|
1549
|
+
)
|
|
1163
1550
|
};
|
|
1164
1551
|
await options.storeProvider.createAuthorizationTransaction(transaction);
|
|
1165
1552
|
const authorizationUrl = buildAuthorizationUrl(metadata, transaction, getCallbackUri(backendOrigin, basePath));
|
|
@@ -1183,6 +1570,7 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1183
1570
|
if (!transaction) {
|
|
1184
1571
|
throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_STATE", "OIDC state is invalid or expired.");
|
|
1185
1572
|
}
|
|
1573
|
+
const frontendDestination = resolveFrontendRedirectUri2(transaction, options);
|
|
1186
1574
|
const metadata = await resolveProviderMetadata(config, options);
|
|
1187
1575
|
const tokenResponse = await requestToken(
|
|
1188
1576
|
metadata,
|
|
@@ -1194,17 +1582,7 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1194
1582
|
},
|
|
1195
1583
|
options
|
|
1196
1584
|
);
|
|
1197
|
-
|
|
1198
|
-
if (!isString(tokenResponse.id_token)) {
|
|
1199
|
-
throw new OidcVaultHttpError(502, "OIDC_VAULT_MISSING_ID_TOKEN", "OIDC token response is missing id_token.");
|
|
1200
|
-
}
|
|
1201
|
-
if (!isString(tokenResponse.refresh_token)) {
|
|
1202
|
-
throw new OidcVaultHttpError(
|
|
1203
|
-
502,
|
|
1204
|
-
"OIDC_VAULT_MISSING_REFRESH_TOKEN",
|
|
1205
|
-
"OIDC token response is missing refresh_token."
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
1585
|
+
validateCallbackTokenResponse(tokenResponse);
|
|
1208
1586
|
await callHook("callback", options.hooks?.onCallbackTokens, req, res, void 0, {
|
|
1209
1587
|
hasAccessToken: Boolean(tokenResponse.access_token),
|
|
1210
1588
|
hasRefreshToken: true,
|
|
@@ -1213,13 +1591,19 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1213
1591
|
const claims = await verifyIdToken(metadata, tokenResponse.id_token, transaction.nonce, options);
|
|
1214
1592
|
const subject = getRequiredString(claims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502);
|
|
1215
1593
|
const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
|
|
1216
|
-
if (userInfo) {
|
|
1594
|
+
if (userInfo !== void 0) {
|
|
1217
1595
|
assertUserInfoSubject(userInfo, subject);
|
|
1218
1596
|
}
|
|
1219
|
-
if (userInfo) {
|
|
1597
|
+
if (userInfo !== void 0) {
|
|
1220
1598
|
await callHook("callback", options.hooks?.onUserInfo, req, res, void 0, { subject });
|
|
1221
1599
|
}
|
|
1222
1600
|
const now = getNow(options);
|
|
1601
|
+
const sessionExpiresAt = options.sessionTtlMs === void 0 ? void 0 : computeExpiresAt(now, options.sessionTtlMs, "sessionTtlMs");
|
|
1602
|
+
const exchangeExpiresAt = computeExpiresAt(
|
|
1603
|
+
now,
|
|
1604
|
+
options.exchangeCodeTtlMs ?? DEFAULT_EXCHANGE_CODE_TTL_MS,
|
|
1605
|
+
"exchangeCodeTtlMs"
|
|
1606
|
+
);
|
|
1223
1607
|
const sessionId = createOpaqueId("sess");
|
|
1224
1608
|
const session = {
|
|
1225
1609
|
sessionId,
|
|
@@ -1236,12 +1620,16 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1236
1620
|
scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : metadata.scopes,
|
|
1237
1621
|
createdAt: now,
|
|
1238
1622
|
updatedAt: now,
|
|
1623
|
+
...sessionExpiresAt === void 0 ? {} : { expiresAt: sessionExpiresAt },
|
|
1239
1624
|
user: mergeUserProfile(subject, claims, userInfo),
|
|
1240
1625
|
metadata: {
|
|
1241
1626
|
tokenType: tokenResponse.token_type
|
|
1242
1627
|
}
|
|
1243
1628
|
};
|
|
1244
1629
|
await callHook("callback", options.hooks?.onBeforeSessionCreate, req, res, session, { subject });
|
|
1630
|
+
if (sessionExpiresAt !== void 0) {
|
|
1631
|
+
session.expiresAt = isUsableEpochMs(session.expiresAt) ? Math.min(session.expiresAt, sessionExpiresAt) : sessionExpiresAt;
|
|
1632
|
+
}
|
|
1245
1633
|
const createdSession = await options.storeProvider.createSession(session);
|
|
1246
1634
|
const exchangeCode = createOpaqueId("code");
|
|
1247
1635
|
try {
|
|
@@ -1250,7 +1638,7 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1250
1638
|
sessionId: createdSession.sessionId,
|
|
1251
1639
|
returnTo: transaction.returnTo,
|
|
1252
1640
|
createdAt: now,
|
|
1253
|
-
expiresAt:
|
|
1641
|
+
expiresAt: exchangeExpiresAt
|
|
1254
1642
|
});
|
|
1255
1643
|
} catch (error) {
|
|
1256
1644
|
await options.storeProvider.deleteSessionsByLogicalSessionId({
|
|
@@ -1261,9 +1649,9 @@ var createCallbackHandler = (options, config, backendOrigin, basePath) => create
|
|
|
1261
1649
|
await callPostCommitHook("callback", options, options.hooks?.onSessionCreated, req, res, createdSession, {
|
|
1262
1650
|
subject
|
|
1263
1651
|
});
|
|
1264
|
-
res.redirect(302, appendCodeToRedirectUri(
|
|
1652
|
+
res.redirect(302, appendCodeToRedirectUri(frontendDestination, exchangeCode));
|
|
1265
1653
|
});
|
|
1266
|
-
var createExchangeHandler = (options) => createAsyncHandler("exchange", options, async (req, res) => {
|
|
1654
|
+
var createExchangeHandler = (options, config) => createAsyncHandler("exchange", options, async (req, res) => {
|
|
1267
1655
|
const body = getBody(req);
|
|
1268
1656
|
const code = getRequiredString(body.code, "Exchange request is missing code.", "OIDC_VAULT_MISSING_EXCHANGE_CODE");
|
|
1269
1657
|
const exchangeRecord = await options.storeProvider.consumeExchangeCode(code);
|
|
@@ -1274,6 +1662,7 @@ var createExchangeHandler = (options) => createAsyncHandler("exchange", options,
|
|
|
1274
1662
|
if (!session) {
|
|
1275
1663
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
1276
1664
|
}
|
|
1665
|
+
assertSessionIdentity(session, config);
|
|
1277
1666
|
let issuedToken;
|
|
1278
1667
|
try {
|
|
1279
1668
|
issuedToken = await withIssuedToken(req, res, options, session);
|
|
@@ -1302,6 +1691,7 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
|
|
|
1302
1691
|
}
|
|
1303
1692
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
1304
1693
|
}
|
|
1694
|
+
assertSessionIdentity(currentSession, config);
|
|
1305
1695
|
const metadata = await resolveProviderMetadata(config, options);
|
|
1306
1696
|
const tokenResponse = await requestToken(
|
|
1307
1697
|
metadata,
|
|
@@ -1311,16 +1701,16 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
|
|
|
1311
1701
|
},
|
|
1312
1702
|
options
|
|
1313
1703
|
);
|
|
1314
|
-
|
|
1704
|
+
validateRefreshTokenResponse(tokenResponse);
|
|
1315
1705
|
const newIdToken = isString(tokenResponse.id_token) ? tokenResponse.id_token : void 0;
|
|
1316
1706
|
const idToken = newIdToken ?? currentSession.idToken;
|
|
1317
|
-
const
|
|
1318
|
-
const subject =
|
|
1707
|
+
const freshClaims = newIdToken ? await verifyIdToken(metadata, newIdToken, void 0, options) : void 0;
|
|
1708
|
+
const subject = freshClaims ? getRequiredString(freshClaims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502) : currentSession.subject;
|
|
1319
1709
|
if (newIdToken && subject !== currentSession.subject) {
|
|
1320
1710
|
throw new OidcVaultHttpError(502, "OIDC_VAULT_INVALID_ID_TOKEN", "OIDC refreshed id_token sub changed.");
|
|
1321
1711
|
}
|
|
1322
1712
|
const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
|
|
1323
|
-
if (userInfo) {
|
|
1713
|
+
if (userInfo !== void 0) {
|
|
1324
1714
|
assertUserInfoSubject(userInfo, subject);
|
|
1325
1715
|
}
|
|
1326
1716
|
const now = getNow(options);
|
|
@@ -1330,14 +1720,14 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
|
|
|
1330
1720
|
sessionId: createOpaqueId("sess"),
|
|
1331
1721
|
logicalSessionId,
|
|
1332
1722
|
subject,
|
|
1333
|
-
providerSessionId: typeof
|
|
1723
|
+
providerSessionId: freshClaims && typeof freshClaims.sid === "string" ? freshClaims.sid : currentSession.providerSessionId,
|
|
1334
1724
|
refreshToken: isString(tokenResponse.refresh_token) ? tokenResponse.refresh_token : currentSession.refreshToken,
|
|
1335
1725
|
idToken,
|
|
1336
1726
|
accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : currentSession.accessToken,
|
|
1337
1727
|
scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : currentSession.scope,
|
|
1338
1728
|
expiresAt: currentSession.expiresAt,
|
|
1339
1729
|
updatedAt: now,
|
|
1340
|
-
user:
|
|
1730
|
+
user: composeRefreshedUserProfile(subject, currentSession.user, freshClaims, userInfo)
|
|
1341
1731
|
};
|
|
1342
1732
|
let rotatedSession;
|
|
1343
1733
|
try {
|
|
@@ -1389,6 +1779,7 @@ var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandle
|
|
|
1389
1779
|
res.status(200).json({ loggedOut: true });
|
|
1390
1780
|
return;
|
|
1391
1781
|
}
|
|
1782
|
+
assertSessionIdentity(session, config);
|
|
1392
1783
|
await callHook("logout", options.hooks?.onBeforeLogout, req, res, session, void 0);
|
|
1393
1784
|
await options.storeProvider.deleteSessionsByLogicalSessionId({
|
|
1394
1785
|
logicalSessionId: session.logicalSessionId ?? session.sessionId
|
|
@@ -1396,8 +1787,24 @@ var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandle
|
|
|
1396
1787
|
if (usesCookieTransport(options)) {
|
|
1397
1788
|
clearSessionCookie(res, options);
|
|
1398
1789
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1790
|
+
if (!redirect) {
|
|
1791
|
+
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, session);
|
|
1792
|
+
res.status(200).json({ loggedOut: true });
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
let upstreamLogoutUrl;
|
|
1796
|
+
try {
|
|
1797
|
+
const metadata = await resolveProviderMetadata(config, options);
|
|
1798
|
+
upstreamLogoutUrl = metadata.endSessionEndpoint ? buildLogoutUrl(metadata.endSessionEndpoint, session.idToken, options.postLogoutRedirectUri) : void 0;
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
try {
|
|
1801
|
+
await options.hooks?.onError?.({
|
|
1802
|
+
...createHookContext("logout", req, res, session),
|
|
1803
|
+
error
|
|
1804
|
+
});
|
|
1805
|
+
} catch {
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1401
1808
|
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, session);
|
|
1402
1809
|
if (redirect && upstreamLogoutUrl) {
|
|
1403
1810
|
res.redirect(302, upstreamLogoutUrl);
|
|
@@ -1411,26 +1818,40 @@ var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("ba
|
|
|
1411
1818
|
const metadata = await resolveProviderMetadata(config, options);
|
|
1412
1819
|
const logoutToken = getLogoutTokenFromRequest(req);
|
|
1413
1820
|
const claims = await verifyBackchannelLogoutToken(metadata, logoutToken, options);
|
|
1821
|
+
const replayKey = buildBackchannelLogoutReplayKey({
|
|
1822
|
+
issuer: metadata.issuer,
|
|
1823
|
+
clientId: metadata.clientId,
|
|
1824
|
+
jti: claims.jti
|
|
1825
|
+
});
|
|
1414
1826
|
const firstUse = await options.storeProvider.consumeBackchannelLogoutTokenJti({
|
|
1415
|
-
jti:
|
|
1827
|
+
jti: replayKey,
|
|
1416
1828
|
expiresAt: claims.exp * 1e3
|
|
1417
1829
|
});
|
|
1418
|
-
|
|
1419
|
-
res.status(200).json({
|
|
1420
|
-
loggedOut: true,
|
|
1421
|
-
revokedSessions: 0
|
|
1422
|
-
});
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
const revokedSessions = isString(claims.sid) ? await options.storeProvider.deleteSessionsByProviderSessionId({
|
|
1830
|
+
const revokeMatchingSessions = () => isString(claims.sid) ? options.storeProvider.deleteSessionsByProviderSessionId({
|
|
1426
1831
|
providerSessionId: claims.sid,
|
|
1427
1832
|
issuer: metadata.issuer,
|
|
1428
1833
|
clientId: metadata.clientId
|
|
1429
|
-
}) :
|
|
1834
|
+
}) : options.storeProvider.deleteSessionsBySubject({
|
|
1430
1835
|
subject: String(claims.sub),
|
|
1431
1836
|
issuer: metadata.issuer,
|
|
1432
1837
|
clientId: metadata.clientId
|
|
1433
1838
|
});
|
|
1839
|
+
if (!firstUse) {
|
|
1840
|
+
const catchUpRevokedSessions = await revokeMatchingSessions();
|
|
1841
|
+
if (catchUpRevokedSessions > 0) {
|
|
1842
|
+
await callPostCommitHook("backchannel-logout", options, options.hooks?.onLogout, req, res, void 0, {
|
|
1843
|
+
providerSessionId: claims.sid,
|
|
1844
|
+
subject: claims.sub,
|
|
1845
|
+
revokedSessions: catchUpRevokedSessions
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
res.status(200).json({
|
|
1849
|
+
loggedOut: true,
|
|
1850
|
+
revokedSessions: catchUpRevokedSessions
|
|
1851
|
+
});
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
const revokedSessions = await revokeMatchingSessions();
|
|
1434
1855
|
await callPostCommitHook("backchannel-logout", options, options.hooks?.onLogout, req, res, void 0, {
|
|
1435
1856
|
providerSessionId: claims.sid,
|
|
1436
1857
|
subject: claims.sub,
|
|
@@ -1444,17 +1865,21 @@ var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("ba
|
|
|
1444
1865
|
function registerRoutes(router, options, config, trustedOrigins, backendOrigin, basePath) {
|
|
1445
1866
|
router.get(OIDC_VAULT_ROUTE_PATHS.login, createLoginHandler(options, config, backendOrigin, basePath));
|
|
1446
1867
|
router.get(OIDC_VAULT_ROUTE_PATHS.callback, createCallbackHandler(options, config, backendOrigin, basePath));
|
|
1447
|
-
router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options));
|
|
1868
|
+
router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options, config));
|
|
1448
1869
|
router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins));
|
|
1449
1870
|
router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins));
|
|
1450
1871
|
router.post(OIDC_VAULT_ROUTE_PATHS["backchannel-logout"], createBackchannelLogoutHandler(options, config));
|
|
1451
1872
|
}
|
|
1452
1873
|
function createOidcVaultMiddleware(options) {
|
|
1453
|
-
const { backendOrigin, config, trustedOrigins } = validateOidcVaultOptions(options);
|
|
1874
|
+
const { backendOrigin, config, trustedOrigins, resolvedOptions } = validateOidcVaultOptions(options);
|
|
1454
1875
|
const rootRouter = express.Router();
|
|
1455
1876
|
const baseRouter = express.Router();
|
|
1456
|
-
const basePath = normalizeOidcVaultBasePath(
|
|
1457
|
-
const requestBodyLimit =
|
|
1877
|
+
const basePath = normalizeOidcVaultBasePath(resolvedOptions.basePath);
|
|
1878
|
+
const requestBodyLimit = resolvedOptions.requestBodyLimit ?? DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT;
|
|
1879
|
+
baseRouter.use((_req, res, next) => {
|
|
1880
|
+
applyCredentialResponseCachePolicy(res);
|
|
1881
|
+
next();
|
|
1882
|
+
});
|
|
1458
1883
|
baseRouter.use(express.json({ limit: requestBodyLimit }));
|
|
1459
1884
|
baseRouter.use(
|
|
1460
1885
|
express.urlencoded({
|
|
@@ -1464,7 +1889,7 @@ function createOidcVaultMiddleware(options) {
|
|
|
1464
1889
|
})
|
|
1465
1890
|
);
|
|
1466
1891
|
baseRouter.use(createBodyParserErrorHandler());
|
|
1467
|
-
registerRoutes(baseRouter,
|
|
1892
|
+
registerRoutes(baseRouter, resolvedOptions, config, trustedOrigins, backendOrigin, basePath);
|
|
1468
1893
|
rootRouter.use(basePath, baseRouter);
|
|
1469
1894
|
return rootRouter;
|
|
1470
1895
|
}
|