@web-ts-toolkit/express-oidc-vault 0.47.1 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +994 -75
- package/index.d.mts +423 -32
- package/index.d.ts +423 -32
- package/index.js +1972 -390
- package/index.mjs +1965 -384
- package/package.json +5 -3
package/index.mjs
CHANGED
|
@@ -1,139 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createHash, randomBytes } from "crypto";
|
|
2
|
+
import { createHash as createHash6, randomBytes as randomBytes3 } from "crypto";
|
|
3
3
|
import express from "express";
|
|
4
4
|
|
|
5
|
-
// src/config.ts
|
|
6
|
-
var DEFAULT_OIDC_SCOPES = "openid email profile";
|
|
7
|
-
var MANUAL_CONFIG_REQUIREMENTS = ["issuer", "authorizationEndpoint", "tokenEndpoint", "jwksUri"];
|
|
8
|
-
var MANUAL_CONFIG_SELECTORS = ["authorizationEndpoint", "tokenEndpoint", "jwksUri", "userInfoEndpoint", "endSessionEndpoint"];
|
|
9
|
-
var normalizeOptionalString = (value) => {
|
|
10
|
-
const trimmed = value?.trim();
|
|
11
|
-
return trimmed ? trimmed : void 0;
|
|
12
|
-
};
|
|
13
|
-
var normalizeHttpUrl = (value, optionName) => {
|
|
14
|
-
if (value === void 0) {
|
|
15
|
-
return void 0;
|
|
16
|
-
}
|
|
17
|
-
let url;
|
|
18
|
-
try {
|
|
19
|
-
url = new URL(value);
|
|
20
|
-
} catch {
|
|
21
|
-
throw new Error(`${optionName} must be an absolute HTTP(S) URL.`);
|
|
22
|
-
}
|
|
23
|
-
if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
|
|
24
|
-
throw new Error(`${optionName} must use http or https.`);
|
|
25
|
-
}
|
|
26
|
-
return url.toString();
|
|
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
|
-
};
|
|
49
|
-
var withOptionalValue = (target, key, value) => {
|
|
50
|
-
if (value !== void 0) {
|
|
51
|
-
target[key] = value;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
function resolveOidcVaultConfig(config = {}) {
|
|
55
|
-
const issuer = normalizeIssuerIdentifier(normalizeOptionalString(config.issuer), "config.issuer");
|
|
56
|
-
const clientId = normalizeOptionalString(config.clientId);
|
|
57
|
-
const clientSecret = normalizeOptionalString(config.clientSecret);
|
|
58
|
-
const scopes = normalizeOptionalString(config.scopes) ?? DEFAULT_OIDC_SCOPES;
|
|
59
|
-
const hasManualEndpoints = MANUAL_CONFIG_SELECTORS.some((key) => normalizeOptionalString(config[key]));
|
|
60
|
-
if (!clientId) {
|
|
61
|
-
throw new Error("OIDC clientId is required.");
|
|
62
|
-
}
|
|
63
|
-
if (issuer && !hasManualEndpoints) {
|
|
64
|
-
const resolved2 = {
|
|
65
|
-
mode: "issuer",
|
|
66
|
-
issuer,
|
|
67
|
-
scopes
|
|
68
|
-
};
|
|
69
|
-
withOptionalValue(resolved2, "clientId", clientId);
|
|
70
|
-
withOptionalValue(resolved2, "clientSecret", clientSecret);
|
|
71
|
-
return resolved2;
|
|
72
|
-
}
|
|
73
|
-
const resolved = {
|
|
74
|
-
mode: "manual",
|
|
75
|
-
scopes
|
|
76
|
-
};
|
|
77
|
-
withOptionalValue(resolved, "issuer", issuer);
|
|
78
|
-
withOptionalValue(
|
|
79
|
-
resolved,
|
|
80
|
-
"authorizationEndpoint",
|
|
81
|
-
normalizeHttpUrl(normalizeOptionalString(config.authorizationEndpoint), "config.authorizationEndpoint")
|
|
82
|
-
);
|
|
83
|
-
withOptionalValue(
|
|
84
|
-
resolved,
|
|
85
|
-
"tokenEndpoint",
|
|
86
|
-
normalizeHttpUrl(normalizeOptionalString(config.tokenEndpoint), "config.tokenEndpoint")
|
|
87
|
-
);
|
|
88
|
-
withOptionalValue(
|
|
89
|
-
resolved,
|
|
90
|
-
"userInfoEndpoint",
|
|
91
|
-
normalizeHttpUrl(normalizeOptionalString(config.userInfoEndpoint), "config.userInfoEndpoint")
|
|
92
|
-
);
|
|
93
|
-
withOptionalValue(resolved, "jwksUri", normalizeHttpUrl(normalizeOptionalString(config.jwksUri), "config.jwksUri"));
|
|
94
|
-
withOptionalValue(
|
|
95
|
-
resolved,
|
|
96
|
-
"endSessionEndpoint",
|
|
97
|
-
normalizeHttpUrl(normalizeOptionalString(config.endSessionEndpoint), "config.endSessionEndpoint")
|
|
98
|
-
);
|
|
99
|
-
withOptionalValue(resolved, "clientId", clientId);
|
|
100
|
-
withOptionalValue(resolved, "clientSecret", clientSecret);
|
|
101
|
-
const missing = MANUAL_CONFIG_REQUIREMENTS.filter((key) => !resolved[key]);
|
|
102
|
-
if (missing.length > 0) {
|
|
103
|
-
throw new Error(`Missing required OIDC configuration for manual mode: ${missing.join(", ")}`);
|
|
104
|
-
}
|
|
105
|
-
return resolved;
|
|
106
|
-
}
|
|
107
|
-
function resolveOidcVaultConfigFromEnv(env = process.env) {
|
|
108
|
-
return resolveOidcVaultConfig({
|
|
109
|
-
issuer: env.OIDC_ISSUER,
|
|
110
|
-
authorizationEndpoint: env.OIDC_AUTHORIZATION_ENDPOINT,
|
|
111
|
-
tokenEndpoint: env.OIDC_TOKEN_ENDPOINT,
|
|
112
|
-
userInfoEndpoint: env.OIDC_USERINFO_ENDPOINT,
|
|
113
|
-
jwksUri: env.OIDC_JWKS_URI,
|
|
114
|
-
endSessionEndpoint: env.OIDC_END_SESSION_ENDPOINT,
|
|
115
|
-
clientId: env.OIDC_CLIENT_ID,
|
|
116
|
-
clientSecret: env.OIDC_CLIENT_SECRET,
|
|
117
|
-
scopes: env.OIDC_SCOPES
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// src/constants.ts
|
|
122
|
-
var DEFAULT_OIDC_VAULT_BASE_PATH = "/auth/oidc";
|
|
123
|
-
var DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
|
|
124
|
-
var DEFAULT_EXCHANGE_CODE_TTL_MS = 30 * 1e3;
|
|
125
|
-
var DEFAULT_SESSION_COOKIE_NAME = "oidc_vault_session";
|
|
126
|
-
var DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT = "16kb";
|
|
127
|
-
var OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT = 16;
|
|
128
|
-
var OIDC_VAULT_ROUTE_PATHS = {
|
|
129
|
-
login: "/login",
|
|
130
|
-
callback: "/callback",
|
|
131
|
-
exchange: "/exchange",
|
|
132
|
-
refresh: "/refresh",
|
|
133
|
-
logout: "/logout",
|
|
134
|
-
"backchannel-logout": "/backchannel-logout"
|
|
135
|
-
};
|
|
136
|
-
|
|
137
5
|
// src/errors.ts
|
|
138
6
|
var OidcVaultHttpError = class extends Error {
|
|
139
7
|
constructor(status, code, message, clientMessage = message) {
|
|
@@ -213,6 +81,25 @@ var getRequiredFiniteNonNegativeInteger = (value, message, code, status = 502) =
|
|
|
213
81
|
return value;
|
|
214
82
|
};
|
|
215
83
|
|
|
84
|
+
// src/fingerprint-recognition.ts
|
|
85
|
+
import { createHash as createHash3 } from "crypto";
|
|
86
|
+
|
|
87
|
+
// src/constants.ts
|
|
88
|
+
var DEFAULT_OIDC_VAULT_BASE_PATH = "/auth/oidc";
|
|
89
|
+
var DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
|
|
90
|
+
var DEFAULT_EXCHANGE_CODE_TTL_MS = 30 * 1e3;
|
|
91
|
+
var DEFAULT_SESSION_COOKIE_NAME = "oidc_vault_session";
|
|
92
|
+
var DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT = "16kb";
|
|
93
|
+
var OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT = 16;
|
|
94
|
+
var OIDC_VAULT_ROUTE_PATHS = {
|
|
95
|
+
login: "/login",
|
|
96
|
+
callback: "/callback",
|
|
97
|
+
exchange: "/exchange",
|
|
98
|
+
refresh: "/refresh",
|
|
99
|
+
logout: "/logout",
|
|
100
|
+
"backchannel-logout": "/backchannel-logout"
|
|
101
|
+
};
|
|
102
|
+
|
|
216
103
|
// src/cookies.ts
|
|
217
104
|
var getSessionTransport = (options) => options.sessionTransport ?? "body";
|
|
218
105
|
var usesCookieTransport = (options) => getSessionTransport(options) === "cookie";
|
|
@@ -243,6 +130,7 @@ var isHttpsBackendOrigin = (options) => {
|
|
|
243
130
|
}
|
|
244
131
|
};
|
|
245
132
|
var COOKIE_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
133
|
+
var isValidCookieName = (value) => typeof value === "string" && COOKIE_NAME_PATTERN.test(value) && !hasUnsafeCookieValueCharacters(value);
|
|
246
134
|
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])?)*$/;
|
|
247
135
|
var hasUnsafeCookieValueCharacters = (value) => {
|
|
248
136
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -258,7 +146,7 @@ var validateCookieOptions = (options) => {
|
|
|
258
146
|
return;
|
|
259
147
|
}
|
|
260
148
|
const cookieOptions = resolveCookieOptions(options);
|
|
261
|
-
if (!
|
|
149
|
+
if (!isValidCookieName(cookieOptions.name)) {
|
|
262
150
|
throw new Error("cookie.name must be a valid HTTP cookie name.");
|
|
263
151
|
}
|
|
264
152
|
if (hasUnsafeCookieValueCharacters(cookieOptions.path) || !cookieOptions.path.startsWith("/")) {
|
|
@@ -352,6 +240,527 @@ var parseSelectedCookieValue = (headerValue, selectedName) => {
|
|
|
352
240
|
return void 0;
|
|
353
241
|
};
|
|
354
242
|
|
|
243
|
+
// src/device-binding-policy.ts
|
|
244
|
+
import { isIP } from "net";
|
|
245
|
+
var isPlainRecord = (value) => {
|
|
246
|
+
if (typeof value !== "object" || value === null) return false;
|
|
247
|
+
const prototype = Object.getPrototypeOf(value);
|
|
248
|
+
return prototype === Object.prototype || prototype === null;
|
|
249
|
+
};
|
|
250
|
+
var isDpopAlgorithm = (value) => value === "ES256" || value === "PS256" || value === "RS256";
|
|
251
|
+
var resolveInteger = (value, fallback, min, max, name) => {
|
|
252
|
+
const resolved = value === void 0 ? fallback : value;
|
|
253
|
+
if (typeof resolved !== "number" || !Number.isInteger(resolved) || resolved < min || resolved > max) {
|
|
254
|
+
throw new Error(`${name} must be an integer from ${min} through ${max}.`);
|
|
255
|
+
}
|
|
256
|
+
return resolved;
|
|
257
|
+
};
|
|
258
|
+
var resolveDeviceBindingOptions = (value) => {
|
|
259
|
+
if (value === void 0) return void 0;
|
|
260
|
+
if (!isPlainRecord(value)) throw new Error("deviceBinding must be a plain options object.");
|
|
261
|
+
const { mode, algorithms, proofMaxAgeSeconds, clockSkewSeconds, nonce } = value;
|
|
262
|
+
if (mode !== void 0 && mode !== "optional" && mode !== "required") {
|
|
263
|
+
throw new Error("deviceBinding.mode must be optional or required.");
|
|
264
|
+
}
|
|
265
|
+
const configuredAlgorithms = algorithms === void 0 ? ["ES256"] : algorithms;
|
|
266
|
+
if (!Array.isArray(configuredAlgorithms) || configuredAlgorithms.length === 0) {
|
|
267
|
+
throw new Error("deviceBinding.algorithms must be a nonempty allowlist of ES256, PS256, or RS256.");
|
|
268
|
+
}
|
|
269
|
+
const allowlist = Array.from(configuredAlgorithms);
|
|
270
|
+
if (!allowlist.every(isDpopAlgorithm)) {
|
|
271
|
+
throw new Error("deviceBinding.algorithms must be a nonempty allowlist of ES256, PS256, or RS256.");
|
|
272
|
+
}
|
|
273
|
+
let resolvedNonce = false;
|
|
274
|
+
if (nonce !== void 0 && nonce !== false) {
|
|
275
|
+
if (!isPlainRecord(nonce)) throw new Error("deviceBinding.nonce must be false or a plain nonce options object.");
|
|
276
|
+
const { secret, lifetimeSeconds } = nonce;
|
|
277
|
+
if (!(secret instanceof Uint8Array) || secret.byteLength < 32) {
|
|
278
|
+
throw new Error("deviceBinding.nonce.secret must be a Uint8Array of at least 32 bytes.");
|
|
279
|
+
}
|
|
280
|
+
resolvedNonce = Object.freeze({
|
|
281
|
+
// Uint8Array.from also copies Buffer/subarray inputs instead of retaining
|
|
282
|
+
// a shared view. Nonempty typed arrays cannot be Object.freeze'd; these
|
|
283
|
+
// owned bytes stay private while the plain nonce container is frozen.
|
|
284
|
+
secret: Uint8Array.from(secret),
|
|
285
|
+
lifetimeSeconds: resolveInteger(lifetimeSeconds, 60, 1, 300, "deviceBinding.nonce.lifetimeSeconds")
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return Object.freeze({
|
|
289
|
+
mode: mode ?? "optional",
|
|
290
|
+
algorithms: Object.freeze([...new Set(allowlist)]),
|
|
291
|
+
proofMaxAgeSeconds: resolveInteger(proofMaxAgeSeconds, 60, 1, 300, "deviceBinding.proofMaxAgeSeconds"),
|
|
292
|
+
clockSkewSeconds: resolveInteger(clockSkewSeconds, 5, 0, 30, "deviceBinding.clockSkewSeconds"),
|
|
293
|
+
nonce: resolvedNonce
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
var DEVICE_BINDING_STORE_CAPABILITIES = [
|
|
297
|
+
"getAuthorizationTransaction",
|
|
298
|
+
"consumeAuthorizationTransactionIfMatches",
|
|
299
|
+
"getExchangeCode",
|
|
300
|
+
"consumeExchangeCodeIfMatches",
|
|
301
|
+
"getSessionRevocationContext",
|
|
302
|
+
"reserveDpopProof"
|
|
303
|
+
];
|
|
304
|
+
function assertDeviceBindingStoreCapabilities(store, feature = "deviceBinding") {
|
|
305
|
+
for (const name of DEVICE_BINDING_STORE_CAPABILITIES) {
|
|
306
|
+
if (typeof store?.[name] !== "function") {
|
|
307
|
+
throw new Error(`${feature} requires storeProvider.${name}.`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
var resolveDeviceBindingOrigin = (value, name = "backendOrigin") => {
|
|
312
|
+
if (typeof value !== "string") throw new Error(`${name} must be a static HTTP(S) origin for deviceBinding.`);
|
|
313
|
+
const raw = value.trim();
|
|
314
|
+
let url;
|
|
315
|
+
try {
|
|
316
|
+
url = new URL(raw);
|
|
317
|
+
} catch {
|
|
318
|
+
throw new Error(`${name} must be a static HTTP(S) origin for deviceBinding.`);
|
|
319
|
+
}
|
|
320
|
+
if (!/^https?:\/\/[^/\\?#]+\/?$/i.test(raw) || /\s/.test(raw) || raw.includes("@") || url.username !== "" || url.password !== "" || url.pathname !== "/" || url.origin === "null") {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`${name} must be a static HTTP(S) origin without userinfo, path, query, or fragment for deviceBinding.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || isIP(url.hostname) === 4 && url.hostname.startsWith("127.");
|
|
326
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
327
|
+
throw new Error(`${name} must use HTTPS except for loopback HTTP development when deviceBinding is enabled.`);
|
|
328
|
+
}
|
|
329
|
+
return url.origin;
|
|
330
|
+
};
|
|
331
|
+
var isCanonicalDpopJkt = (value) => typeof value === "string" && value.length === 43 && /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/.test(value);
|
|
332
|
+
var invalidBinding = (diagnostic) => new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_DPOP_PROOF", diagnostic, "DPoP proof validation failed.");
|
|
333
|
+
var snapshotDpopBinding = (value) => {
|
|
334
|
+
if (value === void 0) return void 0;
|
|
335
|
+
if (!isPlainRecord(value)) throw invalidBinding("Stored device binding must be a plain dpop binding object.");
|
|
336
|
+
const { type, jkt } = value;
|
|
337
|
+
if (type !== "dpop" || !isCanonicalDpopJkt(jkt)) {
|
|
338
|
+
throw invalidBinding("Stored device binding must contain type dpop and a canonical SHA-256 JWK thumbprint.");
|
|
339
|
+
}
|
|
340
|
+
return Object.freeze({ type, jkt });
|
|
341
|
+
};
|
|
342
|
+
var assertDeviceBindingPolicy = (policy, binding) => {
|
|
343
|
+
const stored = snapshotDpopBinding(binding);
|
|
344
|
+
if (stored !== void 0 && policy === void 0) {
|
|
345
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
346
|
+
}
|
|
347
|
+
if (stored === void 0 && policy?.mode === "required") {
|
|
348
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_DEVICE_BINDING_REQUIRED", "A device-bound login is required.");
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
var resolveVerifiedDeviceBinding = (policy, binding, verified) => {
|
|
352
|
+
const stored = snapshotDpopBinding(binding);
|
|
353
|
+
assertDeviceBindingPolicy(policy, stored);
|
|
354
|
+
if (verified === void 0) {
|
|
355
|
+
if (stored !== void 0) {
|
|
356
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
357
|
+
}
|
|
358
|
+
return void 0;
|
|
359
|
+
}
|
|
360
|
+
const proofBinding = snapshotDpopBinding(verified);
|
|
361
|
+
const alg = verified.alg;
|
|
362
|
+
if (proofBinding === void 0 || !isDpopAlgorithm(alg) || !policy?.algorithms.includes(alg)) {
|
|
363
|
+
throw invalidBinding("Verified DPoP context must use a currently allowed asymmetric proof algorithm.");
|
|
364
|
+
}
|
|
365
|
+
if (stored === void 0) return void 0;
|
|
366
|
+
if (stored.jkt !== proofBinding.jkt) throw invalidBinding("Verified DPoP key does not match the original binding.");
|
|
367
|
+
return Object.freeze({ type: "dpop", jkt: stored.jkt, alg });
|
|
368
|
+
};
|
|
369
|
+
var withSessionDeviceBinding = (session, binding) => {
|
|
370
|
+
const restored = {};
|
|
371
|
+
for (const key of Reflect.ownKeys(session)) {
|
|
372
|
+
if (key !== "deviceBinding" && Object.prototype.propertyIsEnumerable.call(session, key))
|
|
373
|
+
Object.defineProperty(restored, key, {
|
|
374
|
+
value: Reflect.get(session, key),
|
|
375
|
+
enumerable: true,
|
|
376
|
+
configurable: true,
|
|
377
|
+
writable: true
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const stored = snapshotDpopBinding(binding);
|
|
381
|
+
if (stored !== void 0) restored.deviceBinding = stored;
|
|
382
|
+
return restored;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// src/dpop-proof.ts
|
|
386
|
+
import { createHash as createHash2 } from "crypto";
|
|
387
|
+
import { calculateJwkThumbprint, compactVerify, importJWK } from "jose";
|
|
388
|
+
|
|
389
|
+
// src/dpop-nonce.ts
|
|
390
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
391
|
+
var NONCE_VERSION = "v1";
|
|
392
|
+
var HMAC_DOMAIN = "@web-ts-toolkit/express-oidc-vault/dpop-nonce/v1\0";
|
|
393
|
+
var DPOP_NONCE_MAX_BYTES = 512;
|
|
394
|
+
var isEpochMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
395
|
+
var decodeBase64url = (value, byteLength) => {
|
|
396
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) return null;
|
|
397
|
+
const bytes = Buffer.from(value, "base64url");
|
|
398
|
+
if (bytes.toString("base64url") !== value || byteLength !== void 0 && bytes.length !== byteLength) return null;
|
|
399
|
+
return bytes;
|
|
400
|
+
};
|
|
401
|
+
var createDpopNoncePolicy = (options) => {
|
|
402
|
+
const { secret: configuredSecret, lifetimeSeconds: configuredLifetime, namespace } = options;
|
|
403
|
+
if (!(configuredSecret instanceof Uint8Array) || configuredSecret.byteLength < 32) {
|
|
404
|
+
throw new TypeError("DPoP nonce secret must be a Uint8Array of at least 32 bytes.");
|
|
405
|
+
}
|
|
406
|
+
const lifetimeSeconds = configuredLifetime === void 0 ? 60 : configuredLifetime;
|
|
407
|
+
if (!Number.isInteger(lifetimeSeconds) || lifetimeSeconds < 1 || lifetimeSeconds > 300) {
|
|
408
|
+
throw new TypeError("DPoP nonce lifetime must be an integer from 1 through 300 seconds.");
|
|
409
|
+
}
|
|
410
|
+
if (typeof namespace !== "string" || namespace.length === 0) {
|
|
411
|
+
throw new TypeError("DPoP nonce requires a resolved protection-space namespace.");
|
|
412
|
+
}
|
|
413
|
+
const secret = Uint8Array.from(configuredSecret);
|
|
414
|
+
const namespaceDigest = createHash("sha256").update(namespace, "utf8").digest("base64url");
|
|
415
|
+
const lifetimeMs = lifetimeSeconds * 1e3;
|
|
416
|
+
const sign = (encodedPayload) => createHmac("sha256", secret).update(HMAC_DOMAIN, "utf8").update(`${NONCE_VERSION}.${encodedPayload}`, "ascii").digest();
|
|
417
|
+
return Object.freeze({
|
|
418
|
+
issue(jkt, now) {
|
|
419
|
+
const expiresAt = now + lifetimeMs;
|
|
420
|
+
if (!isCanonicalDpopJkt(jkt) || !isEpochMs(now) || !isEpochMs(expiresAt) || expiresAt <= now) {
|
|
421
|
+
throw new TypeError("DPoP nonce issuance requires a canonical key thumbprint and a safe live epoch window.");
|
|
422
|
+
}
|
|
423
|
+
const payload = JSON.stringify([randomBytes(16).toString("base64url"), now, expiresAt, namespaceDigest, jkt]);
|
|
424
|
+
const encodedPayload = Buffer.from(payload, "utf8").toString("base64url");
|
|
425
|
+
const nonce = `${NONCE_VERSION}.${encodedPayload}.${sign(encodedPayload).toString("base64url")}`;
|
|
426
|
+
if (Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES) {
|
|
427
|
+
throw new TypeError("DPoP nonce issuance exceeded its fixed wire bound.");
|
|
428
|
+
}
|
|
429
|
+
return nonce;
|
|
430
|
+
},
|
|
431
|
+
verify(nonce, jkt, now) {
|
|
432
|
+
if (!isCanonicalDpopJkt(jkt) || !isEpochMs(now) || typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)
|
|
433
|
+
return false;
|
|
434
|
+
const [version, encodedPayload, encodedMac, extra] = nonce.split(".");
|
|
435
|
+
if (version !== NONCE_VERSION || !encodedPayload || !encodedMac || extra !== void 0) return false;
|
|
436
|
+
const mac = decodeBase64url(encodedMac, 32);
|
|
437
|
+
const bytes = decodeBase64url(encodedPayload);
|
|
438
|
+
if (!mac || !bytes || !timingSafeEqual(mac, sign(encodedPayload))) return false;
|
|
439
|
+
let payload;
|
|
440
|
+
try {
|
|
441
|
+
payload = JSON.parse(bytes.toString("utf8"));
|
|
442
|
+
} catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
if (!Array.isArray(payload) || payload.length !== 5 || !bytes.equals(Buffer.from(JSON.stringify(payload), "utf8")))
|
|
446
|
+
return false;
|
|
447
|
+
const [random, issuedAt, expiresAt, context, key] = payload;
|
|
448
|
+
if (typeof random !== "string" || !decodeBase64url(random, 16) || !isEpochMs(issuedAt) || !isEpochMs(expiresAt) || context !== namespaceDigest || key !== jkt)
|
|
449
|
+
return false;
|
|
450
|
+
const declaredLifetime = expiresAt - issuedAt;
|
|
451
|
+
return declaredLifetime >= 1e3 && declaredLifetime <= lifetimeMs && declaredLifetime % 1e3 === 0 && issuedAt <= now && now < expiresAt;
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/dpop-target.ts
|
|
457
|
+
var invalidTarget = () => new OidcVaultHttpError(
|
|
458
|
+
401,
|
|
459
|
+
"OIDC_VAULT_INVALID_DPOP_PROOF",
|
|
460
|
+
"DPoP target must be a valid pinned HTTP(S) URL/path.",
|
|
461
|
+
"DPoP proof validation failed."
|
|
462
|
+
);
|
|
463
|
+
var hasControl = (value) => {
|
|
464
|
+
for (let index = 0; index < value.length; index++) {
|
|
465
|
+
const code = value.charCodeAt(index);
|
|
466
|
+
if (code < 32 || code === 127) return true;
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
};
|
|
470
|
+
var normalizePath = (origin, path) => {
|
|
471
|
+
if (!path.startsWith("/") || /[\s\\]/.test(path) || hasControl(path) || /%(?![0-9a-f]{2})/i.test(path))
|
|
472
|
+
throw invalidTarget();
|
|
473
|
+
const decoded = path.replace(/%([0-9a-f]{2})/gi, (_, hex) => {
|
|
474
|
+
const character = String.fromCharCode(Number.parseInt(hex, 16));
|
|
475
|
+
return /^[A-Za-z0-9._~-]$/.test(character) ? character : `%${hex.toUpperCase()}`;
|
|
476
|
+
});
|
|
477
|
+
return new URL(`${origin}${decoded}`).pathname;
|
|
478
|
+
};
|
|
479
|
+
var createDpopRequestTargetResolver = (options) => {
|
|
480
|
+
const { publicOrigin, publicPathPrefix } = options;
|
|
481
|
+
const origin = resolveDeviceBindingOrigin(publicOrigin, "publicOrigin");
|
|
482
|
+
let prefix = "";
|
|
483
|
+
if (publicPathPrefix !== void 0 && publicPathPrefix !== "") {
|
|
484
|
+
if (typeof publicPathPrefix !== "string" || /[?#]/.test(publicPathPrefix)) {
|
|
485
|
+
throw new TypeError("deviceBinding.publicPathPrefix must be an absolute path prefix without query or fragment.");
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
prefix = normalizePath(origin, publicPathPrefix).replace(/\/+$/g, "");
|
|
489
|
+
} catch {
|
|
490
|
+
throw new TypeError("deviceBinding.publicPathPrefix must be a valid absolute path prefix.");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return (originalUrl) => {
|
|
494
|
+
if (typeof originalUrl !== "string") throw invalidTarget();
|
|
495
|
+
const path = originalUrl.split(/[?#]/, 1)[0];
|
|
496
|
+
if (!path.startsWith("/")) throw invalidTarget();
|
|
497
|
+
return `${origin}${normalizePath(origin, `${prefix}${path}`)}`;
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
var normalizeDpopProofTarget = (value) => {
|
|
501
|
+
if (typeof value !== "string" || /[\s\\]/.test(value) || hasControl(value) || !/^https?:\/\/[^/\\?#]+/i.test(value))
|
|
502
|
+
throw invalidTarget();
|
|
503
|
+
let url;
|
|
504
|
+
try {
|
|
505
|
+
url = new URL(value);
|
|
506
|
+
} catch {
|
|
507
|
+
throw invalidTarget();
|
|
508
|
+
}
|
|
509
|
+
const queryFree = value.split(/[?#]/, 1)[0];
|
|
510
|
+
const authority = queryFree.slice(queryFree.indexOf("://") + 3).split("/", 1)[0];
|
|
511
|
+
if (url.username || url.password || url.origin === "null" || authority.includes("@")) throw invalidTarget();
|
|
512
|
+
const authorityEnd = queryFree.indexOf("/", queryFree.indexOf("://") + 3);
|
|
513
|
+
const path = authorityEnd === -1 ? "/" : queryFree.slice(authorityEnd);
|
|
514
|
+
return `${url.origin}${normalizePath(url.origin, path)}`;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// src/dpop-proof.ts
|
|
518
|
+
var DPOP_PROOF_MAX_BYTES = 8192;
|
|
519
|
+
var DPOP_PROTECTED_HEADER_MAX_BYTES = 2048;
|
|
520
|
+
var readDpopRawHeader = (req, name) => {
|
|
521
|
+
const normalizedName = name.toLowerCase();
|
|
522
|
+
let count = 0;
|
|
523
|
+
let value;
|
|
524
|
+
for (let index = 0; index < req.rawHeaders.length; index += 2) {
|
|
525
|
+
if (req.rawHeaders[index].toLowerCase() !== normalizedName) continue;
|
|
526
|
+
if (++count === 1) value = req.rawHeaders[index + 1];
|
|
527
|
+
if (count === 2) break;
|
|
528
|
+
}
|
|
529
|
+
return Object.freeze({ count, value });
|
|
530
|
+
};
|
|
531
|
+
var invalidProof = (diagnostic, cause) => {
|
|
532
|
+
const error = new OidcVaultHttpError(
|
|
533
|
+
401,
|
|
534
|
+
"OIDC_VAULT_INVALID_DPOP_PROOF",
|
|
535
|
+
diagnostic,
|
|
536
|
+
"DPoP proof validation failed."
|
|
537
|
+
);
|
|
538
|
+
if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause, configurable: true });
|
|
539
|
+
return error;
|
|
540
|
+
};
|
|
541
|
+
var extractDpopProof = (header) => {
|
|
542
|
+
if (header.count === 0) return void 0;
|
|
543
|
+
const { value } = header;
|
|
544
|
+
if (header.count !== 1 || typeof value !== "string" || value.length === 0 || value.length > DPOP_PROOF_MAX_BYTES || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value)) {
|
|
545
|
+
throw invalidProof("DPoP header must contain one bounded compact signed proof.");
|
|
546
|
+
}
|
|
547
|
+
return value;
|
|
548
|
+
};
|
|
549
|
+
var decodeBase64url2 = (value, maxBytes) => {
|
|
550
|
+
if (!value || value.length > Math.ceil(maxBytes * 4 / 3) || !/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
551
|
+
throw invalidProof("DPoP base64url input exceeds its bound or has invalid encoding.");
|
|
552
|
+
}
|
|
553
|
+
const bytes = Buffer.from(value, "base64url");
|
|
554
|
+
if (bytes.length > maxBytes || bytes.toString("base64url") !== value)
|
|
555
|
+
throw invalidProof("DPoP base64url input is not canonical.");
|
|
556
|
+
return bytes;
|
|
557
|
+
};
|
|
558
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
559
|
+
var parseObject = (bytes) => {
|
|
560
|
+
try {
|
|
561
|
+
const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
562
|
+
const value = JSON.parse(json);
|
|
563
|
+
if (!isPlainObject(value)) throw new TypeError("DPoP JSON must be an object.");
|
|
564
|
+
const stack = [];
|
|
565
|
+
for (let index = 0; index < json.length; index++) {
|
|
566
|
+
const character = json[index];
|
|
567
|
+
if (character === "{" || character === "[") {
|
|
568
|
+
stack.push({ keys: character === "{" ? /* @__PURE__ */ new Set() : null, expectsKey: character === "{" });
|
|
569
|
+
} else if (character === "}" || character === "]") {
|
|
570
|
+
stack.pop();
|
|
571
|
+
} else if (character === ",") {
|
|
572
|
+
const container = stack[stack.length - 1];
|
|
573
|
+
if (container?.keys) container.expectsKey = true;
|
|
574
|
+
} else if (character === '"') {
|
|
575
|
+
const start = index++;
|
|
576
|
+
for (; index < json.length; index++) {
|
|
577
|
+
if (json[index] === "\\") index++;
|
|
578
|
+
else if (json[index] === '"') break;
|
|
579
|
+
}
|
|
580
|
+
const container = stack[stack.length - 1];
|
|
581
|
+
if (container?.keys && container.expectsKey) {
|
|
582
|
+
const key = JSON.parse(json.slice(start, index + 1));
|
|
583
|
+
if (container.keys.has(key)) throw new TypeError("DPoP JSON has duplicate member names.");
|
|
584
|
+
container.keys.add(key);
|
|
585
|
+
container.expectsKey = false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return value;
|
|
590
|
+
} catch (cause) {
|
|
591
|
+
throw invalidProof("DPoP JSON encoding or object shape is invalid.", cause);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
var publicJwk = (value, alg) => {
|
|
595
|
+
if (!isPlainObject(value) || Buffer.byteLength(JSON.stringify(value), "utf8") > DPOP_PROTECTED_HEADER_MAX_BYTES) {
|
|
596
|
+
throw invalidProof("DPoP JWK must be a bounded public object.");
|
|
597
|
+
}
|
|
598
|
+
const allowed = alg === "ES256" ? ["kty", "crv", "x", "y", "alg", "use", "key_ops", "kid"] : ["kty", "n", "e", "alg", "use", "key_ops", "kid"];
|
|
599
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)) || value.alg !== void 0 && value.alg !== alg || value.use !== void 0 && value.use !== "sig" || value.kid !== void 0 && typeof value.kid !== "string" || value.key_ops !== void 0 && (!Array.isArray(value.key_ops) || value.key_ops.length !== 1 || value.key_ops[0] !== "verify")) {
|
|
600
|
+
throw invalidProof("DPoP JWK contains private, remote, unsupported or incompatible key parameters.");
|
|
601
|
+
}
|
|
602
|
+
if (alg === "ES256") {
|
|
603
|
+
if (value.kty !== "EC" || value.crv !== "P-256" || typeof value.x !== "string" || typeof value.y !== "string" || decodeBase64url2(value.x, 32).length !== 32 || decodeBase64url2(value.y, 32).length !== 32) {
|
|
604
|
+
throw invalidProof("DPoP ES256 requires a public P-256 key with canonical coordinates.");
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
if (value.kty !== "RSA" || typeof value.n !== "string" || typeof value.e !== "string") {
|
|
608
|
+
throw invalidProof("DPoP RSA requires public modulus and exponent.");
|
|
609
|
+
}
|
|
610
|
+
const modulus = decodeBase64url2(value.n, 512);
|
|
611
|
+
const exponent = decodeBase64url2(value.e, 512);
|
|
612
|
+
const bits = (modulus.length - 1) * 8 + 32 - Math.clz32(modulus[0]);
|
|
613
|
+
if (modulus[0] === 0 || bits < 2048 || bits > 4096 || exponent[0] === 0 || (exponent[exponent.length - 1] & 1) === 0 || exponent.length === 1 && exponent[0] < 3 || exponent.length > modulus.length || exponent.length === modulus.length && Buffer.compare(exponent, modulus) >= 0) {
|
|
614
|
+
throw invalidProof(
|
|
615
|
+
"DPoP RSA public parameters must use a 2048\u20134096-bit modulus and a valid bounded odd exponent."
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return value;
|
|
620
|
+
};
|
|
621
|
+
var verifyDpopProof = async (input) => {
|
|
622
|
+
const { proof, method, targetUrl, proofOptions, expectedJkt, accessToken } = input;
|
|
623
|
+
const algorithms = [...proofOptions.algorithms];
|
|
624
|
+
extractDpopProof({ count: 1, value: proof });
|
|
625
|
+
const [encodedHeader, encodedPayload, encodedSignature] = proof.split(".");
|
|
626
|
+
const header = parseObject(decodeBase64url2(encodedHeader, DPOP_PROTECTED_HEADER_MAX_BYTES));
|
|
627
|
+
decodeBase64url2(encodedPayload, DPOP_PROOF_MAX_BYTES);
|
|
628
|
+
const signature = decodeBase64url2(encodedSignature, 512);
|
|
629
|
+
const { alg } = header;
|
|
630
|
+
if (header.typ !== "dpop+jwt" || !isDpopAlgorithm(alg) || !algorithms.includes(alg) || ["crit", "b64", "jku", "x5u", "x5c", "x5t", "x5t#S256"].some((key) => Object.hasOwn(header, key))) {
|
|
631
|
+
throw invalidProof("DPoP protected header has an unsupported type, algorithm or key/critical extension.");
|
|
632
|
+
}
|
|
633
|
+
const jwk = publicJwk(header.jwk, alg);
|
|
634
|
+
const signatureBytes = alg === "ES256" ? 64 : decodeBase64url2(jwk.n, 512).length;
|
|
635
|
+
if (signature.length !== signatureBytes)
|
|
636
|
+
throw invalidProof("DPoP signature length does not match the allowed key profile.");
|
|
637
|
+
let payload;
|
|
638
|
+
try {
|
|
639
|
+
const key = await importJWK(jwk, alg);
|
|
640
|
+
({ payload } = await compactVerify(proof, key, { algorithms }));
|
|
641
|
+
} catch (cause) {
|
|
642
|
+
throw invalidProof("DPoP public-key import or signature verification failed.", cause);
|
|
643
|
+
}
|
|
644
|
+
const claims = parseObject(payload);
|
|
645
|
+
const { htm, htu, iat, jti, nonce, ath } = claims;
|
|
646
|
+
if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || typeof jti !== "string" || jti.length < 1 || jti.length > 128 || /[^\x20-\x7e]/.test(jti)) {
|
|
647
|
+
throw invalidProof("DPoP signed iat/JTI must be nonnegative integer time and bounded printable ASCII.");
|
|
648
|
+
}
|
|
649
|
+
if (htm !== method || typeof method !== "string" || method.length === 0 || normalizeDpopProofTarget(htu) !== targetUrl) {
|
|
650
|
+
throw invalidProof("DPoP signed method or normalized public target does not match this request.");
|
|
651
|
+
}
|
|
652
|
+
if (typeof nonce === "string" && (nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
653
|
+
throw invalidProof("DPoP signed nonce exceeds its byte bound.");
|
|
654
|
+
}
|
|
655
|
+
if (accessToken !== void 0 && (typeof accessToken !== "string" || !/^[\x21-\x7e]+$/.test(accessToken) || !isCanonicalDpopJkt(ath) || ath !== createHash2("sha256").update(accessToken, "ascii").digest("base64url"))) {
|
|
656
|
+
throw invalidProof("DPoP signed access-token hash does not match the presented API token.");
|
|
657
|
+
}
|
|
658
|
+
const jkt = await calculateJwkThumbprint(jwk, "sha256");
|
|
659
|
+
if (expectedJkt !== void 0 && (!isCanonicalDpopJkt(expectedJkt) || jkt !== expectedJkt)) {
|
|
660
|
+
throw invalidProof("DPoP verified public key does not match the original credential confirmation.");
|
|
661
|
+
}
|
|
662
|
+
return Object.freeze({
|
|
663
|
+
binding: Object.freeze({ type: "dpop", jkt, alg }),
|
|
664
|
+
iat,
|
|
665
|
+
jti,
|
|
666
|
+
...nonce === void 0 ? {} : { nonce }
|
|
667
|
+
});
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/fingerprint-recognition.ts
|
|
671
|
+
var FINGERPRINT_RECOGNITION_METADATA_KEY = "oidcVaultFingerprintRecognition";
|
|
672
|
+
var isPlainRecord2 = (value) => typeof value === "object" && value !== null && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
673
|
+
var COLLIDING_HEADERS = /* @__PURE__ */ new Set([
|
|
674
|
+
"authorization",
|
|
675
|
+
"proxy-authorization",
|
|
676
|
+
"www-authenticate",
|
|
677
|
+
"proxy-authenticate",
|
|
678
|
+
"authentication-info",
|
|
679
|
+
"proxy-authentication-info",
|
|
680
|
+
"dpop",
|
|
681
|
+
"dpop-nonce",
|
|
682
|
+
"cookie",
|
|
683
|
+
"set-cookie",
|
|
684
|
+
"origin",
|
|
685
|
+
"referer",
|
|
686
|
+
"host",
|
|
687
|
+
"forwarded",
|
|
688
|
+
"accept",
|
|
689
|
+
"accept-charset",
|
|
690
|
+
"accept-encoding",
|
|
691
|
+
"accept-language",
|
|
692
|
+
"connection",
|
|
693
|
+
"keep-alive",
|
|
694
|
+
"te",
|
|
695
|
+
"trailer",
|
|
696
|
+
"transfer-encoding",
|
|
697
|
+
"upgrade"
|
|
698
|
+
]);
|
|
699
|
+
var resolveFingerprintRecognitionOptions = (value) => {
|
|
700
|
+
if (value === void 0) return void 0;
|
|
701
|
+
if (!isPlainRecord2(value)) throw new Error("fingerprintRecognition must be a plain options object.");
|
|
702
|
+
const { headerName } = value;
|
|
703
|
+
const name = headerName === void 0 ? "X-Device-Fingerprint" : headerName;
|
|
704
|
+
if (!isValidCookieName(name)) throw new Error("fingerprintRecognition.headerName must be a valid HTTP field name.");
|
|
705
|
+
const normalized = name.toLowerCase();
|
|
706
|
+
if (COLLIDING_HEADERS.has(normalized) || ["content-", "x-forwarded-", "access-control-", "sec-fetch-"].some((prefix) => normalized.startsWith(prefix))) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
"fingerprintRecognition.headerName must not collide with authentication, cookie, origin, content, or transport headers."
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return Object.freeze({ headerName: name });
|
|
712
|
+
};
|
|
713
|
+
var invalidFingerprint = () => new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_FINGERPRINT", "Fingerprint signal is invalid.");
|
|
714
|
+
var captureFingerprintRecognition = (req, policy) => {
|
|
715
|
+
if (policy === void 0) return void 0;
|
|
716
|
+
const { count, value } = readDpopRawHeader(req, policy.headerName);
|
|
717
|
+
if (count === 0) return void 0;
|
|
718
|
+
if (count !== 1 || typeof value !== "string" || value.length === 0 || value.length > 256) throw invalidFingerprint();
|
|
719
|
+
for (let index = 0; index < value.length; index++) {
|
|
720
|
+
const byte = value.charCodeAt(index);
|
|
721
|
+
if (byte < 32 || byte > 126) throw invalidFingerprint();
|
|
722
|
+
}
|
|
723
|
+
return Object.freeze({ version: 1, hash: createHash3("sha256").update(value, "ascii").digest("base64url") });
|
|
724
|
+
};
|
|
725
|
+
var snapshotFingerprintRecognition = (metadata) => {
|
|
726
|
+
if (metadata === void 0 || !Object.hasOwn(metadata, FINGERPRINT_RECOGNITION_METADATA_KEY)) return void 0;
|
|
727
|
+
const value = metadata?.[FINGERPRINT_RECOGNITION_METADATA_KEY];
|
|
728
|
+
if (value === void 0) return void 0;
|
|
729
|
+
if (!isPlainRecord2(value)) throw new TypeError("Stored fingerprint recognition metadata is invalid.");
|
|
730
|
+
const { version, hash } = value;
|
|
731
|
+
if (Object.keys(value).length !== 2 || !Object.hasOwn(value, "version") || !Object.hasOwn(value, "hash") || version !== 1 || !isCanonicalDpopJkt(hash)) {
|
|
732
|
+
throw new TypeError("Stored fingerprint recognition metadata is invalid.");
|
|
733
|
+
}
|
|
734
|
+
return Object.freeze({ version, hash });
|
|
735
|
+
};
|
|
736
|
+
var fingerprintRecognitionMatches = (actual, original) => actual?.hash === original?.hash;
|
|
737
|
+
var assertFingerprintRecognition = (policy, original, supplied) => {
|
|
738
|
+
if (policy !== void 0 && original !== void 0 && !fingerprintRecognitionMatches(supplied, original)) {
|
|
739
|
+
throw new OidcVaultHttpError(
|
|
740
|
+
403,
|
|
741
|
+
"OIDC_VAULT_FINGERPRINT_REAUTH_REQUIRED",
|
|
742
|
+
"Browser recognition changed; sign in again."
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
var withFingerprintRecognitionMetadata = (metadata, original) => {
|
|
747
|
+
if (metadata === void 0 && original === void 0) return void 0;
|
|
748
|
+
if (original === void 0 && metadata !== void 0 && !Object.hasOwn(metadata, FINGERPRINT_RECOGNITION_METADATA_KEY))
|
|
749
|
+
return metadata;
|
|
750
|
+
const restored = {};
|
|
751
|
+
for (const name of Object.keys(metadata ?? {})) {
|
|
752
|
+
if (name !== FINGERPRINT_RECOGNITION_METADATA_KEY)
|
|
753
|
+
Object.defineProperty(restored, name, {
|
|
754
|
+
value: Reflect.get(metadata, name),
|
|
755
|
+
enumerable: true,
|
|
756
|
+
configurable: true,
|
|
757
|
+
writable: true
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (original !== void 0) restored[FINGERPRINT_RECOGNITION_METADATA_KEY] = { version: 1, hash: original.hash };
|
|
761
|
+
return restored;
|
|
762
|
+
};
|
|
763
|
+
|
|
355
764
|
// src/lifetime-policy.ts
|
|
356
765
|
var MAX_EPOCH_MS = 864e13;
|
|
357
766
|
var isUsableEpochMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && Math.abs(value) <= MAX_EPOCH_MS;
|
|
@@ -381,6 +790,283 @@ var validateLifetimeOptions = (options, now) => {
|
|
|
381
790
|
}
|
|
382
791
|
};
|
|
383
792
|
|
|
793
|
+
// src/transaction-cookie.ts
|
|
794
|
+
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
795
|
+
var invalidBrowserBinding = () => new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_BROWSER_BINDING", "Login browser binding validation failed.");
|
|
796
|
+
var resolveTransactionCookieOptions = (options, backendOrigin) => {
|
|
797
|
+
const value = options.transactionCookie;
|
|
798
|
+
if (value !== void 0 && (typeof value !== "object" || value === null || ![Object.prototype, null].includes(Object.getPrototypeOf(value)))) {
|
|
799
|
+
throw new Error("transactionCookie must be a plain options object.");
|
|
800
|
+
}
|
|
801
|
+
if (value !== void 0 && Object.keys(value).some((key) => key !== "name" && key !== "sameSite")) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
"transactionCookie supports only name and sameSite; browser-binding cookie security cannot be overridden."
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const { name, sameSite } = value ?? {};
|
|
807
|
+
const secure = new URL(backendOrigin).protocol === "https:";
|
|
808
|
+
const resolvedName = name === void 0 ? secure ? "__Host-oidc_vault_transaction" : "oidc_vault_transaction" : name;
|
|
809
|
+
if (!isValidCookieName(resolvedName)) throw new Error("transactionCookie.name must be a valid HTTP cookie name.");
|
|
810
|
+
if (sameSite !== void 0 && sameSite !== "lax" && sameSite !== "none") {
|
|
811
|
+
throw new Error("transactionCookie.sameSite must be lax or none.");
|
|
812
|
+
}
|
|
813
|
+
if (!secure && sameSite === "none") throw new Error("transactionCookie.sameSite none requires HTTPS.");
|
|
814
|
+
if (!secure && (resolvedName.startsWith("__Host-") || resolvedName.startsWith("__Secure-"))) {
|
|
815
|
+
throw new Error("transactionCookie.name with the __Host- or __Secure- prefix requires HTTPS.");
|
|
816
|
+
}
|
|
817
|
+
if ((options.deviceBinding !== void 0 || options.fingerprintRecognition !== void 0 || value !== void 0) && resolvedName === resolveCookieOptions(options).name) {
|
|
818
|
+
throw new Error("transactionCookie.name must be distinct from cookie.name.");
|
|
819
|
+
}
|
|
820
|
+
return Object.freeze({ name: resolvedName, sameSite: sameSite ?? "lax", secure, path: "/", httpOnly: true });
|
|
821
|
+
};
|
|
822
|
+
var hashSecret = (secret) => createHash4("sha256").update(secret, "ascii").digest("base64url");
|
|
823
|
+
var createTransactionBrowserBinding = () => {
|
|
824
|
+
const secret = randomBytes2(32).toString("base64url");
|
|
825
|
+
return Object.freeze({ secret, browserBindingHash: hashSecret(secret) });
|
|
826
|
+
};
|
|
827
|
+
var parseTransactionCookie = (header, selectedName) => {
|
|
828
|
+
if (header === void 0) return void 0;
|
|
829
|
+
if (typeof header !== "string") throw invalidBrowserBinding();
|
|
830
|
+
let selected;
|
|
831
|
+
for (let start = 0; start < header.length; ) {
|
|
832
|
+
const delimiter = header.indexOf(";", start);
|
|
833
|
+
const end = delimiter === -1 ? header.length : delimiter;
|
|
834
|
+
let separator = start;
|
|
835
|
+
while (separator < end && header[separator] !== "=") separator += 1;
|
|
836
|
+
const hasValue = separator < end;
|
|
837
|
+
let nameStart = start;
|
|
838
|
+
let nameEnd = hasValue ? separator : end;
|
|
839
|
+
while (nameStart < nameEnd && (header[nameStart] === " " || header[nameStart] === " ")) nameStart += 1;
|
|
840
|
+
while (nameEnd > nameStart && (header[nameEnd - 1] === " " || header[nameEnd - 1] === " ")) nameEnd -= 1;
|
|
841
|
+
if (nameEnd - nameStart === selectedName.length && header.startsWith(selectedName, nameStart)) {
|
|
842
|
+
if (selected !== void 0 || !hasValue) throw invalidBrowserBinding();
|
|
843
|
+
if (end - separator - 1 !== 43) throw invalidBrowserBinding();
|
|
844
|
+
const value = header.slice(separator + 1, end);
|
|
845
|
+
if (!isCanonicalDpopJkt(value)) throw invalidBrowserBinding();
|
|
846
|
+
selected = value;
|
|
847
|
+
}
|
|
848
|
+
start = end + 1;
|
|
849
|
+
}
|
|
850
|
+
return selected;
|
|
851
|
+
};
|
|
852
|
+
var snapshotRecordBindingMatch = (record) => {
|
|
853
|
+
const { deviceBinding, browserBindingHash } = record;
|
|
854
|
+
const binding = snapshotDpopBinding(deviceBinding);
|
|
855
|
+
if (binding !== void 0 && Object.keys(deviceBinding).length !== 2) {
|
|
856
|
+
throw new OidcVaultHttpError(
|
|
857
|
+
401,
|
|
858
|
+
"OIDC_VAULT_INVALID_DPOP_PROOF",
|
|
859
|
+
"Stored device binding contains unsupported fields.",
|
|
860
|
+
"DPoP proof validation failed."
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (browserBindingHash !== void 0 && !isCanonicalDpopJkt(browserBindingHash) || binding !== void 0 && browserBindingHash === void 0)
|
|
864
|
+
throw invalidBrowserBinding();
|
|
865
|
+
return Object.freeze({ deviceBinding: binding ?? null, browserBindingHash: browserBindingHash ?? null });
|
|
866
|
+
};
|
|
867
|
+
var recordBindingMatches = (actual, expected) => actual.deviceBinding?.jkt === expected.deviceBinding?.jkt && actual.browserBindingHash === expected.browserBindingHash;
|
|
868
|
+
var authenticateTransactionCookie = (header, cookie, match) => {
|
|
869
|
+
if (match.browserBindingHash === null) return void 0;
|
|
870
|
+
const secret = parseTransactionCookie(header, cookie.name);
|
|
871
|
+
if (secret === void 0 || hashSecret(secret) !== match.browserBindingHash) throw invalidBrowserBinding();
|
|
872
|
+
return secret;
|
|
873
|
+
};
|
|
874
|
+
var setTransactionCookie = (res, cookie, secret, expiresAt, now) => {
|
|
875
|
+
if (!isCanonicalDpopJkt(secret) || !isUsableEpochMs(expiresAt) || !isUsableEpochMs(now)) {
|
|
876
|
+
throw new TypeError("OIDC vault transaction cookie requires a canonical secret and usable deadline/clock.");
|
|
877
|
+
}
|
|
878
|
+
res.append(
|
|
879
|
+
"Set-Cookie",
|
|
880
|
+
serializeCookie(cookie.name, secret, cookie, {
|
|
881
|
+
maxAge: Math.max(0, Math.floor((expiresAt - now) / 1e3)),
|
|
882
|
+
expires: new Date(Math.floor(expiresAt / 1e3) * 1e3)
|
|
883
|
+
})
|
|
884
|
+
);
|
|
885
|
+
};
|
|
886
|
+
var clearTransactionCookie = (res, cookie) => {
|
|
887
|
+
res.append("Set-Cookie", serializeCookie(cookie.name, "", cookie, { maxAge: 0, expires: /* @__PURE__ */ new Date(0) }));
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// src/authorization-transaction.ts
|
|
891
|
+
var PROVIDER_METADATA_KEY = "oidcVaultTransactionProvider";
|
|
892
|
+
var invalidAuthorizationState = () => new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_STATE", "OIDC state is invalid or expired.");
|
|
893
|
+
var createTransactionProviderMetadata = (config) => ({
|
|
894
|
+
[PROVIDER_METADATA_KEY]: { issuer: config.issuer, clientId: config.clientId }
|
|
895
|
+
});
|
|
896
|
+
var snapshotAuthorizationTransaction = (record, state, now) => {
|
|
897
|
+
const { state: storedState, nonce, pkceVerifier, codeChallenge, returnTo, createdAt, expiresAt, metadata } = record;
|
|
898
|
+
if (storedState !== state || typeof nonce !== "string" || typeof pkceVerifier !== "string" || typeof codeChallenge !== "string" || returnTo !== void 0 && typeof returnTo !== "string" || !isUsableEpochMs(createdAt) || !isUsableEpochMs(expiresAt) || !isUsableEpochMs(now) || expiresAt <= now) {
|
|
899
|
+
throw invalidAuthorizationState();
|
|
900
|
+
}
|
|
901
|
+
const match = snapshotRecordBindingMatch(record);
|
|
902
|
+
let fingerprintRecognition;
|
|
903
|
+
try {
|
|
904
|
+
fingerprintRecognition = snapshotFingerprintRecognition(metadata);
|
|
905
|
+
} catch {
|
|
906
|
+
throw invalidAuthorizationState();
|
|
907
|
+
}
|
|
908
|
+
if (fingerprintRecognition !== void 0 && match.browserBindingHash === null) throw invalidAuthorizationState();
|
|
909
|
+
const value = metadata?.[PROVIDER_METADATA_KEY];
|
|
910
|
+
let provider;
|
|
911
|
+
if (value !== void 0) {
|
|
912
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidAuthorizationState();
|
|
913
|
+
const { issuer, clientId } = value;
|
|
914
|
+
if (issuer !== void 0 && typeof issuer !== "string" || clientId !== void 0 && typeof clientId !== "string") {
|
|
915
|
+
throw invalidAuthorizationState();
|
|
916
|
+
}
|
|
917
|
+
provider = Object.freeze({
|
|
918
|
+
...issuer === void 0 ? {} : { issuer },
|
|
919
|
+
...clientId === void 0 ? {} : { clientId }
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return Object.freeze({
|
|
923
|
+
state,
|
|
924
|
+
nonce,
|
|
925
|
+
pkceVerifier,
|
|
926
|
+
codeChallenge,
|
|
927
|
+
returnTo,
|
|
928
|
+
createdAt,
|
|
929
|
+
expiresAt,
|
|
930
|
+
...metadata === void 0 ? {} : {
|
|
931
|
+
metadata: Object.freeze({
|
|
932
|
+
...withFingerprintRecognitionMetadata(metadata, fingerprintRecognition),
|
|
933
|
+
...provider === void 0 ? {} : { [PROVIDER_METADATA_KEY]: provider },
|
|
934
|
+
...fingerprintRecognition === void 0 ? {} : { [FINGERPRINT_RECOGNITION_METADATA_KEY]: fingerprintRecognition }
|
|
935
|
+
})
|
|
936
|
+
},
|
|
937
|
+
...match.deviceBinding === null ? {} : { deviceBinding: match.deviceBinding },
|
|
938
|
+
...match.browserBindingHash === null ? {} : { browserBindingHash: match.browserBindingHash },
|
|
939
|
+
match,
|
|
940
|
+
...provider === void 0 ? {} : { provider },
|
|
941
|
+
...fingerprintRecognition === void 0 ? {} : { fingerprintRecognition }
|
|
942
|
+
});
|
|
943
|
+
};
|
|
944
|
+
var assertAuthorizationTransactionIdentity = (transaction, config) => {
|
|
945
|
+
if (transaction.provider?.issuer !== void 0 && transaction.provider.issuer !== config.issuer || transaction.provider?.clientId !== void 0 && transaction.provider.clientId !== config.clientId) {
|
|
946
|
+
throw invalidAuthorizationState();
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
var assertAuthorizationTransactionMatches = (actual, original) => {
|
|
950
|
+
if (actual.state !== original.state || actual.nonce !== original.nonce || actual.pkceVerifier !== original.pkceVerifier || actual.codeChallenge !== original.codeChallenge || actual.returnTo !== original.returnTo || actual.createdAt !== original.createdAt || actual.expiresAt !== original.expiresAt || actual.provider?.issuer !== original.provider?.issuer || actual.provider?.clientId !== original.provider?.clientId || !fingerprintRecognitionMatches(actual.fingerprintRecognition, original.fingerprintRecognition) || !recordBindingMatches(actual.match, original.match))
|
|
951
|
+
throw invalidAuthorizationState();
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
// src/config.ts
|
|
955
|
+
var DEFAULT_OIDC_SCOPES = "openid email profile";
|
|
956
|
+
var MANUAL_CONFIG_REQUIREMENTS = ["issuer", "authorizationEndpoint", "tokenEndpoint", "jwksUri"];
|
|
957
|
+
var MANUAL_CONFIG_SELECTORS = ["authorizationEndpoint", "tokenEndpoint", "jwksUri", "userInfoEndpoint", "endSessionEndpoint"];
|
|
958
|
+
var normalizeOptionalString = (value) => {
|
|
959
|
+
const trimmed = value?.trim();
|
|
960
|
+
return trimmed ? trimmed : void 0;
|
|
961
|
+
};
|
|
962
|
+
var normalizeHttpUrl = (value, optionName) => {
|
|
963
|
+
if (value === void 0) {
|
|
964
|
+
return void 0;
|
|
965
|
+
}
|
|
966
|
+
let url;
|
|
967
|
+
try {
|
|
968
|
+
url = new URL(value);
|
|
969
|
+
} catch {
|
|
970
|
+
throw new Error(`${optionName} must be an absolute HTTP(S) URL.`);
|
|
971
|
+
}
|
|
972
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
|
|
973
|
+
throw new Error(`${optionName} must use http or https.`);
|
|
974
|
+
}
|
|
975
|
+
return url.toString();
|
|
976
|
+
};
|
|
977
|
+
var normalizeIssuerIdentifier = (value, optionName) => {
|
|
978
|
+
if (value === void 0) {
|
|
979
|
+
return void 0;
|
|
980
|
+
}
|
|
981
|
+
let url;
|
|
982
|
+
try {
|
|
983
|
+
url = new URL(value);
|
|
984
|
+
} catch {
|
|
985
|
+
throw new Error(`${optionName} must be an absolute HTTP(S) URL.`);
|
|
986
|
+
}
|
|
987
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin === "null") {
|
|
988
|
+
throw new Error(`${optionName} must use http or https.`);
|
|
989
|
+
}
|
|
990
|
+
if (url.username !== "" || url.password !== "") {
|
|
991
|
+
throw new Error(`${optionName} must not include userinfo.`);
|
|
992
|
+
}
|
|
993
|
+
if (url.search !== "" || url.hash !== "") {
|
|
994
|
+
throw new Error(`${optionName} must not include query or fragment.`);
|
|
995
|
+
}
|
|
996
|
+
return value;
|
|
997
|
+
};
|
|
998
|
+
var withOptionalValue = (target, key, value) => {
|
|
999
|
+
if (value !== void 0) {
|
|
1000
|
+
target[key] = value;
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
function resolveOidcVaultConfig(config = {}) {
|
|
1004
|
+
const issuer = normalizeIssuerIdentifier(normalizeOptionalString(config.issuer), "config.issuer");
|
|
1005
|
+
const clientId = normalizeOptionalString(config.clientId);
|
|
1006
|
+
const clientSecret = normalizeOptionalString(config.clientSecret);
|
|
1007
|
+
const scopes = normalizeOptionalString(config.scopes) ?? DEFAULT_OIDC_SCOPES;
|
|
1008
|
+
const hasManualEndpoints = MANUAL_CONFIG_SELECTORS.some((key) => normalizeOptionalString(config[key]));
|
|
1009
|
+
if (!clientId) {
|
|
1010
|
+
throw new Error("OIDC clientId is required.");
|
|
1011
|
+
}
|
|
1012
|
+
if (issuer && !hasManualEndpoints) {
|
|
1013
|
+
const resolved2 = {
|
|
1014
|
+
mode: "issuer",
|
|
1015
|
+
issuer,
|
|
1016
|
+
scopes
|
|
1017
|
+
};
|
|
1018
|
+
withOptionalValue(resolved2, "clientId", clientId);
|
|
1019
|
+
withOptionalValue(resolved2, "clientSecret", clientSecret);
|
|
1020
|
+
return resolved2;
|
|
1021
|
+
}
|
|
1022
|
+
const resolved = {
|
|
1023
|
+
mode: "manual",
|
|
1024
|
+
scopes
|
|
1025
|
+
};
|
|
1026
|
+
withOptionalValue(resolved, "issuer", issuer);
|
|
1027
|
+
withOptionalValue(
|
|
1028
|
+
resolved,
|
|
1029
|
+
"authorizationEndpoint",
|
|
1030
|
+
normalizeHttpUrl(normalizeOptionalString(config.authorizationEndpoint), "config.authorizationEndpoint")
|
|
1031
|
+
);
|
|
1032
|
+
withOptionalValue(
|
|
1033
|
+
resolved,
|
|
1034
|
+
"tokenEndpoint",
|
|
1035
|
+
normalizeHttpUrl(normalizeOptionalString(config.tokenEndpoint), "config.tokenEndpoint")
|
|
1036
|
+
);
|
|
1037
|
+
withOptionalValue(
|
|
1038
|
+
resolved,
|
|
1039
|
+
"userInfoEndpoint",
|
|
1040
|
+
normalizeHttpUrl(normalizeOptionalString(config.userInfoEndpoint), "config.userInfoEndpoint")
|
|
1041
|
+
);
|
|
1042
|
+
withOptionalValue(resolved, "jwksUri", normalizeHttpUrl(normalizeOptionalString(config.jwksUri), "config.jwksUri"));
|
|
1043
|
+
withOptionalValue(
|
|
1044
|
+
resolved,
|
|
1045
|
+
"endSessionEndpoint",
|
|
1046
|
+
normalizeHttpUrl(normalizeOptionalString(config.endSessionEndpoint), "config.endSessionEndpoint")
|
|
1047
|
+
);
|
|
1048
|
+
withOptionalValue(resolved, "clientId", clientId);
|
|
1049
|
+
withOptionalValue(resolved, "clientSecret", clientSecret);
|
|
1050
|
+
const missing = MANUAL_CONFIG_REQUIREMENTS.filter((key) => !resolved[key]);
|
|
1051
|
+
if (missing.length > 0) {
|
|
1052
|
+
throw new Error(`Missing required OIDC configuration for manual mode: ${missing.join(", ")}`);
|
|
1053
|
+
}
|
|
1054
|
+
return resolved;
|
|
1055
|
+
}
|
|
1056
|
+
function resolveOidcVaultConfigFromEnv(env = process.env) {
|
|
1057
|
+
return resolveOidcVaultConfig({
|
|
1058
|
+
issuer: env.OIDC_ISSUER,
|
|
1059
|
+
authorizationEndpoint: env.OIDC_AUTHORIZATION_ENDPOINT,
|
|
1060
|
+
tokenEndpoint: env.OIDC_TOKEN_ENDPOINT,
|
|
1061
|
+
userInfoEndpoint: env.OIDC_USERINFO_ENDPOINT,
|
|
1062
|
+
jwksUri: env.OIDC_JWKS_URI,
|
|
1063
|
+
endSessionEndpoint: env.OIDC_END_SESSION_ENDPOINT,
|
|
1064
|
+
clientId: env.OIDC_CLIENT_ID,
|
|
1065
|
+
clientSecret: env.OIDC_CLIENT_SECRET,
|
|
1066
|
+
scopes: env.OIDC_SCOPES
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
384
1070
|
// src/utils.ts
|
|
385
1071
|
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
386
1072
|
var isString = (value) => typeof value === "string" && value.length > 0;
|
|
@@ -463,16 +1149,36 @@ var getRequestSourceOrigin = (req) => {
|
|
|
463
1149
|
}
|
|
464
1150
|
};
|
|
465
1151
|
var assertTrustedOrigin = (req, options, trustedOrigins, action) => {
|
|
466
|
-
|
|
467
|
-
|
|
1152
|
+
captureTrustedOriginGuard(req, options, trustedOrigins, action)();
|
|
1153
|
+
};
|
|
1154
|
+
var captureTrustedOriginGuard = (req, options, trustedOrigins, action) => {
|
|
1155
|
+
const guarded = action === "login" || action === "exchange";
|
|
1156
|
+
if (!guarded && !usesCookieTransport(options)) {
|
|
1157
|
+
return () => {
|
|
1158
|
+
};
|
|
468
1159
|
}
|
|
469
|
-
const origin = getRequestSourceOrigin(req);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
1160
|
+
const origin = guarded ? getGuardedRequestSourceOrigin(req) : getRequestSourceOrigin(req);
|
|
1161
|
+
return () => {
|
|
1162
|
+
if (!origin || !trustedOrigins.has(origin)) {
|
|
1163
|
+
throw new OidcVaultHttpError(
|
|
1164
|
+
403,
|
|
1165
|
+
"OIDC_VAULT_UNTRUSTED_ORIGIN",
|
|
1166
|
+
`${action.charAt(0).toUpperCase()}${action.slice(1)} request origin is not trusted.`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
};
|
|
1171
|
+
var getGuardedRequestSourceOrigin = (req) => {
|
|
1172
|
+
const origin = readDpopRawHeader(req, "origin");
|
|
1173
|
+
if (origin.count !== 0) return origin.count === 1 && isString(origin.value) ? origin.value : void 0;
|
|
1174
|
+
const referer = readDpopRawHeader(req, "referer");
|
|
1175
|
+
if (referer.count !== 1 || !isString(referer.value)) return void 0;
|
|
1176
|
+
if (!/^https?:\/\//i.test(referer.value) || /[\s\\]/.test(referer.value)) return void 0;
|
|
1177
|
+
try {
|
|
1178
|
+
const url = new URL(referer.value);
|
|
1179
|
+
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password ? url.origin : void 0;
|
|
1180
|
+
} catch {
|
|
1181
|
+
return void 0;
|
|
476
1182
|
}
|
|
477
1183
|
};
|
|
478
1184
|
|
|
@@ -1038,8 +1744,243 @@ async function fetchUserInfo(metadata, accessToken, options = {}) {
|
|
|
1038
1744
|
}
|
|
1039
1745
|
}
|
|
1040
1746
|
|
|
1747
|
+
// src/token-issuance.ts
|
|
1748
|
+
import { decodeJwt, decodeProtectedHeader } from "jose";
|
|
1749
|
+
|
|
1750
|
+
// src/vault-session.ts
|
|
1751
|
+
var invalidVaultSession = () => new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
1752
|
+
var invalidExchangeCode = () => new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_EXCHANGE_CODE", "Exchange code is invalid or expired.");
|
|
1753
|
+
var isPlainObject2 = (value) => typeof value === "object" && value !== null && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
1754
|
+
var copyPortableData = (value, omitRecognition = false) => {
|
|
1755
|
+
const copies = /* @__PURE__ */ new Map();
|
|
1756
|
+
const pending = [];
|
|
1757
|
+
const copy = (item) => {
|
|
1758
|
+
if (!Array.isArray(item) && !isPlainObject2(item)) return item;
|
|
1759
|
+
const existing = copies.get(item);
|
|
1760
|
+
if (existing) return existing;
|
|
1761
|
+
const target = Array.isArray(item) ? new Array(item.length) : Object.create(Object.getPrototypeOf(item));
|
|
1762
|
+
copies.set(item, target);
|
|
1763
|
+
pending.push({ source: item, target });
|
|
1764
|
+
return target;
|
|
1765
|
+
};
|
|
1766
|
+
const result = copy(value);
|
|
1767
|
+
for (let index = 0; index < pending.length; index++) {
|
|
1768
|
+
const { source, target } = pending[index];
|
|
1769
|
+
for (const key of Object.keys(source)) {
|
|
1770
|
+
if (omitRecognition && key === FINGERPRINT_RECOGNITION_METADATA_KEY) continue;
|
|
1771
|
+
Object.defineProperty(target, key, {
|
|
1772
|
+
value: copy(Reflect.get(source, key)),
|
|
1773
|
+
enumerable: true,
|
|
1774
|
+
configurable: true,
|
|
1775
|
+
writable: true
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return result;
|
|
1780
|
+
};
|
|
1781
|
+
var copyVaultSession = (session, omitRecognition = false) => copyPortableData(session, omitRecognition);
|
|
1782
|
+
var copyVaultUserProfile = (user) => copyPortableData(user, true);
|
|
1783
|
+
var snapshotProvider = (value) => {
|
|
1784
|
+
if (value === void 0) return void 0;
|
|
1785
|
+
if (!isPlainObject2(value)) throw invalidVaultSession();
|
|
1786
|
+
const { issuer, clientId } = value;
|
|
1787
|
+
if (issuer !== void 0 && typeof issuer !== "string" || clientId !== void 0 && typeof clientId !== "string") {
|
|
1788
|
+
throw invalidVaultSession();
|
|
1789
|
+
}
|
|
1790
|
+
return Object.freeze({
|
|
1791
|
+
...issuer === void 0 ? {} : { issuer },
|
|
1792
|
+
...clientId === void 0 ? {} : { clientId }
|
|
1793
|
+
});
|
|
1794
|
+
};
|
|
1795
|
+
var assertVaultSessionIdentity = (session, config) => {
|
|
1796
|
+
if (session.provider?.issuer !== void 0 && session.provider.issuer !== config.issuer || session.provider?.clientId !== void 0 && session.provider.clientId !== config.clientId)
|
|
1797
|
+
throw invalidVaultSession();
|
|
1798
|
+
};
|
|
1799
|
+
var snapshotVaultSession = (record, expectedSessionId, now) => {
|
|
1800
|
+
const {
|
|
1801
|
+
sessionId,
|
|
1802
|
+
logicalSessionId,
|
|
1803
|
+
subject,
|
|
1804
|
+
providerSessionId,
|
|
1805
|
+
provider: rawProvider,
|
|
1806
|
+
deviceBinding: rawBinding,
|
|
1807
|
+
refreshToken,
|
|
1808
|
+
idToken,
|
|
1809
|
+
accessToken,
|
|
1810
|
+
scope,
|
|
1811
|
+
expiresAt,
|
|
1812
|
+
createdAt,
|
|
1813
|
+
updatedAt,
|
|
1814
|
+
user,
|
|
1815
|
+
metadata
|
|
1816
|
+
} = record;
|
|
1817
|
+
if (sessionId !== expectedSessionId || typeof sessionId !== "string" || !sessionId || typeof subject !== "string" || !subject || logicalSessionId !== void 0 && (typeof logicalSessionId !== "string" || !logicalSessionId) || providerSessionId !== void 0 && typeof providerSessionId !== "string" || typeof refreshToken !== "string" || typeof idToken !== "string" || accessToken !== void 0 && typeof accessToken !== "string" || scope !== void 0 && typeof scope !== "string" || !isUsableEpochMs(createdAt) || !isUsableEpochMs(updatedAt) || !isUsableEpochMs(now) || expiresAt !== void 0 && (!isUsableEpochMs(expiresAt) || expiresAt <= now))
|
|
1818
|
+
throw invalidVaultSession();
|
|
1819
|
+
const provider = snapshotProvider(rawProvider);
|
|
1820
|
+
const binding = snapshotDpopBinding(rawBinding);
|
|
1821
|
+
if (binding !== void 0 && Object.keys(rawBinding).length !== 2) throw invalidVaultSession();
|
|
1822
|
+
return Object.freeze({
|
|
1823
|
+
sessionId,
|
|
1824
|
+
logicalSessionId,
|
|
1825
|
+
subject,
|
|
1826
|
+
providerSessionId,
|
|
1827
|
+
provider,
|
|
1828
|
+
...binding === void 0 ? {} : { deviceBinding: binding },
|
|
1829
|
+
refreshToken,
|
|
1830
|
+
idToken,
|
|
1831
|
+
accessToken,
|
|
1832
|
+
scope,
|
|
1833
|
+
expiresAt,
|
|
1834
|
+
createdAt,
|
|
1835
|
+
updatedAt,
|
|
1836
|
+
...user === void 0 ? {} : { user: copyPortableData(user) },
|
|
1837
|
+
...metadata === void 0 ? {} : { metadata: copyPortableData(metadata) }
|
|
1838
|
+
});
|
|
1839
|
+
};
|
|
1840
|
+
var assertVaultSessionMatches = (actual, original) => {
|
|
1841
|
+
if (actual.sessionId !== original.sessionId || actual.logicalSessionId !== original.logicalSessionId || actual.subject !== original.subject || actual.providerSessionId !== original.providerSessionId || actual.provider?.issuer !== original.provider?.issuer || actual.provider?.clientId !== original.provider?.clientId || actual.deviceBinding?.jkt !== original.deviceBinding?.jkt || actual.expiresAt !== original.expiresAt || actual.createdAt !== original.createdAt || actual.updatedAt !== original.updatedAt || actual.refreshToken !== original.refreshToken || actual.idToken !== original.idToken || actual.accessToken !== original.accessToken || actual.scope !== original.scope)
|
|
1842
|
+
throw invalidVaultSession();
|
|
1843
|
+
};
|
|
1844
|
+
var snapshotVaultExchangeCode = (record, expectedCode, now) => {
|
|
1845
|
+
const { code, sessionId, returnTo, createdAt, expiresAt } = record;
|
|
1846
|
+
if (code !== expectedCode || typeof sessionId !== "string" || !sessionId || returnTo !== void 0 && typeof returnTo !== "string" || !isUsableEpochMs(createdAt) || !isUsableEpochMs(expiresAt) || !isUsableEpochMs(now) || expiresAt <= now)
|
|
1847
|
+
throw invalidExchangeCode();
|
|
1848
|
+
const match = snapshotRecordBindingMatch(record);
|
|
1849
|
+
return Object.freeze({
|
|
1850
|
+
code,
|
|
1851
|
+
sessionId,
|
|
1852
|
+
returnTo,
|
|
1853
|
+
createdAt,
|
|
1854
|
+
expiresAt,
|
|
1855
|
+
match,
|
|
1856
|
+
...match.deviceBinding === null ? {} : { deviceBinding: match.deviceBinding },
|
|
1857
|
+
...match.browserBindingHash === null ? {} : { browserBindingHash: match.browserBindingHash }
|
|
1858
|
+
});
|
|
1859
|
+
};
|
|
1860
|
+
var assertVaultExchangeCodeMatches = (actual, original) => {
|
|
1861
|
+
if (actual.code !== original.code || actual.sessionId !== original.sessionId || actual.returnTo !== original.returnTo || actual.createdAt !== original.createdAt || actual.expiresAt !== original.expiresAt || !recordBindingMatches(actual.match, original.match))
|
|
1862
|
+
throw invalidExchangeCode();
|
|
1863
|
+
};
|
|
1864
|
+
var assertVaultExchangeSessionBinding = (code, session) => {
|
|
1865
|
+
if (code.sessionId !== session.sessionId || code.deviceBinding?.jkt !== session.deviceBinding?.jkt)
|
|
1866
|
+
throw invalidVaultSession();
|
|
1867
|
+
};
|
|
1868
|
+
var snapshotVaultRevocationContext = (context) => {
|
|
1869
|
+
const { logicalSessionId, provider, deviceBinding } = context;
|
|
1870
|
+
if (typeof logicalSessionId !== "string" || !logicalSessionId) throw invalidVaultSession();
|
|
1871
|
+
const identity = snapshotProvider(provider);
|
|
1872
|
+
const binding = snapshotDpopBinding(deviceBinding);
|
|
1873
|
+
if (binding !== void 0 && Object.keys(deviceBinding).length !== 2) throw invalidVaultSession();
|
|
1874
|
+
return Object.freeze({
|
|
1875
|
+
logicalSessionId,
|
|
1876
|
+
...identity === void 0 ? {} : { provider: identity },
|
|
1877
|
+
...binding === void 0 ? {} : { deviceBinding: binding }
|
|
1878
|
+
});
|
|
1879
|
+
};
|
|
1880
|
+
var assertVaultRevocationContextMatches = (actual, original) => {
|
|
1881
|
+
if (actual.logicalSessionId !== original.logicalSessionId || actual.provider?.issuer !== original.provider?.issuer || actual.provider?.clientId !== original.provider?.clientId || actual.deviceBinding?.jkt !== original.deviceBinding?.jkt) {
|
|
1882
|
+
throw invalidVaultSession();
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1886
|
+
// src/token-issuance.ts
|
|
1887
|
+
var createExchangeResponse = (options, session, issuedToken) => ({
|
|
1888
|
+
sessionId: usesCookieTransport(options) ? void 0 : session.sessionId,
|
|
1889
|
+
user: copyVaultUserProfile(session.user),
|
|
1890
|
+
accessToken: issuedToken.accessToken,
|
|
1891
|
+
expiresIn: issuedToken.expiresIn,
|
|
1892
|
+
tokenType: issuedToken.tokenType
|
|
1893
|
+
});
|
|
1894
|
+
var isCompactSignedJwt = (token) => {
|
|
1895
|
+
const parts = token.split(".");
|
|
1896
|
+
return parts.length === 3 && parts.every(
|
|
1897
|
+
(part) => /^[A-Za-z0-9_-]+$/.test(part) ? Buffer.from(part, "base64url").toString("base64url") === part : false
|
|
1898
|
+
);
|
|
1899
|
+
};
|
|
1900
|
+
var assertIssuedTokenConfirmation = (accessToken, jkt) => {
|
|
1901
|
+
if (!isCompactSignedJwt(accessToken)) {
|
|
1902
|
+
throw new TypeError("tokenIssuer.issue accessToken must be a compact signed JWT for bound issuance.");
|
|
1903
|
+
}
|
|
1904
|
+
let header;
|
|
1905
|
+
let claims;
|
|
1906
|
+
try {
|
|
1907
|
+
header = decodeProtectedHeader(accessToken);
|
|
1908
|
+
claims = decodeJwt(accessToken);
|
|
1909
|
+
} catch {
|
|
1910
|
+
throw new TypeError("tokenIssuer.issue accessToken must be a compact signed JWT for bound issuance.");
|
|
1911
|
+
}
|
|
1912
|
+
if (typeof header.alg !== "string" || header.alg.length === 0 || header.alg === "none" || header.b64 === false) {
|
|
1913
|
+
throw new TypeError("tokenIssuer.issue accessToken must use a signed JWT algorithm for bound issuance.");
|
|
1914
|
+
}
|
|
1915
|
+
const cnf = claims.cnf;
|
|
1916
|
+
if (typeof cnf !== "object" || cnf === null || Array.isArray(cnf) || cnf.jkt !== jkt) {
|
|
1917
|
+
throw new TypeError("tokenIssuer.issue accessToken cnf.jkt must match the verified device binding.");
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
var withIssuedToken = async (req, res, options, session, verifiedBinding) => {
|
|
1921
|
+
const binding = resolveVerifiedDeviceBinding(options.deviceBinding, session.deviceBinding, verifiedBinding);
|
|
1922
|
+
if (!options.tokenIssuer) return {};
|
|
1923
|
+
const logicalSessionId = session.logicalSessionId ?? session.sessionId;
|
|
1924
|
+
try {
|
|
1925
|
+
const issuerSession = withSessionDeviceBinding(copyVaultSession(session, true), binding);
|
|
1926
|
+
const result = await options.tokenIssuer.issue({
|
|
1927
|
+
req,
|
|
1928
|
+
res,
|
|
1929
|
+
session: issuerSession,
|
|
1930
|
+
...binding === void 0 ? {} : { deviceBinding: Object.freeze({ ...binding }) }
|
|
1931
|
+
});
|
|
1932
|
+
if (typeof result !== "object" || result === null || Array.isArray(result)) {
|
|
1933
|
+
throw new TypeError("tokenIssuer.issue must return a token result object.");
|
|
1934
|
+
}
|
|
1935
|
+
const { accessToken, expiresIn, tokenType } = result;
|
|
1936
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
1937
|
+
throw new TypeError("tokenIssuer.issue accessToken must be a nonempty string.");
|
|
1938
|
+
}
|
|
1939
|
+
if (typeof expiresIn !== "number" || !Number.isSafeInteger(expiresIn) || expiresIn < 0) {
|
|
1940
|
+
throw new TypeError("tokenIssuer.issue expiresIn must be a finite nonnegative safe integer.");
|
|
1941
|
+
}
|
|
1942
|
+
if (tokenType !== void 0 && tokenType !== "Bearer" && tokenType !== "DPoP") {
|
|
1943
|
+
throw new TypeError("tokenIssuer.issue tokenType must be the exact Bearer or DPoP literal when provided.");
|
|
1944
|
+
}
|
|
1945
|
+
if (binding === void 0) {
|
|
1946
|
+
if (tokenType !== void 0 && tokenType !== "Bearer") {
|
|
1947
|
+
throw new TypeError("tokenIssuer.issue tokenType must be Bearer when provided for unbound issuance.");
|
|
1948
|
+
}
|
|
1949
|
+
} else {
|
|
1950
|
+
if (tokenType !== "DPoP") throw new TypeError("tokenIssuer.issue tokenType must be DPoP for bound issuance.");
|
|
1951
|
+
assertIssuedTokenConfirmation(accessToken, binding.jkt);
|
|
1952
|
+
}
|
|
1953
|
+
return { accessToken, expiresIn, ...tokenType === void 0 ? {} : { tokenType } };
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
await options.storeProvider.deleteSessionsByLogicalSessionId({ logicalSessionId });
|
|
1956
|
+
if (usesCookieTransport(options)) clearSessionCookie(res, options);
|
|
1957
|
+
throw error;
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1041
1961
|
// src/token-validation.ts
|
|
1042
1962
|
import { jwtVerify } from "jose";
|
|
1963
|
+
|
|
1964
|
+
// src/access-token-confirmation.ts
|
|
1965
|
+
var invalidAccessToken = (diagnostic) => new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_ACCESS_TOKEN", diagnostic, "Access token validation failed.");
|
|
1966
|
+
var snapshotJkt = (value) => {
|
|
1967
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1968
|
+
throw invalidAccessToken("Access token confirmation must be a plain jkt object.");
|
|
1969
|
+
}
|
|
1970
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1971
|
+
const keys = Reflect.ownKeys(value);
|
|
1972
|
+
if (prototype !== Object.prototype && prototype !== null || keys.length !== 1 || keys[0] !== "jkt") {
|
|
1973
|
+
throw invalidAccessToken("Access token confirmation contains unsupported fields.");
|
|
1974
|
+
}
|
|
1975
|
+
const { jkt } = value;
|
|
1976
|
+
if (!isCanonicalDpopJkt(jkt))
|
|
1977
|
+
throw invalidAccessToken("Access token confirmation is not a canonical SHA-256 thumbprint.");
|
|
1978
|
+
return Object.freeze({ jkt });
|
|
1979
|
+
};
|
|
1980
|
+
var snapshotVerifiedJwtConfirmation = (claims) => Object.hasOwn(claims, "cnf") ? snapshotJkt(claims.cnf) : null;
|
|
1981
|
+
var snapshotAccessTokenConfirmation = (value) => value === null ? null : snapshotJkt(value);
|
|
1982
|
+
|
|
1983
|
+
// src/token-validation.ts
|
|
1043
1984
|
var BACKCHANNEL_LOGOUT_EVENT_CLAIM = "http://schemas.openid.net/event/backchannel-logout";
|
|
1044
1985
|
var validateBackchannelLogoutTokenTyp = (typ) => {
|
|
1045
1986
|
if (typ !== void 0 && (typeof typ !== "string" || typ.toLowerCase() !== "logout+jwt")) {
|
|
@@ -1187,17 +2128,35 @@ async function verifyBackchannelLogoutToken(metadata, logoutToken, options = {})
|
|
|
1187
2128
|
return claims;
|
|
1188
2129
|
}
|
|
1189
2130
|
function createOidcVaultJwtAccessTokenValidator(options) {
|
|
1190
|
-
|
|
2131
|
+
const {
|
|
2132
|
+
key: configuredKey,
|
|
2133
|
+
issuer,
|
|
2134
|
+
audience: configuredAudience,
|
|
2135
|
+
algorithms: configuredAlgorithms,
|
|
2136
|
+
mapClaims
|
|
2137
|
+
} = options;
|
|
2138
|
+
const key = configuredKey instanceof Uint8Array ? Uint8Array.from(configuredKey) : "kty" in configuredKey ? { ...configuredKey, ...configuredKey.key_ops ? { key_ops: [...configuredKey.key_ops] } : {} } : configuredKey;
|
|
2139
|
+
const audience = Array.isArray(configuredAudience) ? [...configuredAudience] : configuredAudience;
|
|
2140
|
+
const algorithms = configuredAlgorithms === void 0 ? void 0 : [...configuredAlgorithms];
|
|
2141
|
+
const validateJwt = async (token) => {
|
|
2142
|
+
const result = await jwtVerify(token, key, { issuer, audience, algorithms });
|
|
2143
|
+
const claims = result.payload;
|
|
2144
|
+
const confirmation = snapshotVerifiedJwtConfirmation(claims);
|
|
2145
|
+
const mapped = mapClaims ? mapClaims(claims) : defaultJwtClaimsMapper(claims);
|
|
2146
|
+
const { subject, sessionId, scope, claims: mappedClaims } = mapped;
|
|
2147
|
+
return { subject, sessionId, scope, claims: mappedClaims, confirmation };
|
|
2148
|
+
};
|
|
2149
|
+
return Object.freeze({
|
|
1191
2150
|
async validate(token) {
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
return
|
|
2151
|
+
const { confirmation, ...mapped } = await validateJwt(token);
|
|
2152
|
+
return confirmation === null ? mapped : { ...mapped, confirmation };
|
|
2153
|
+
},
|
|
2154
|
+
async validateWithRequest({
|
|
2155
|
+
token
|
|
2156
|
+
}) {
|
|
2157
|
+
return validateJwt(token);
|
|
1199
2158
|
}
|
|
1200
|
-
};
|
|
2159
|
+
});
|
|
1201
2160
|
}
|
|
1202
2161
|
|
|
1203
2162
|
// src/types.ts
|
|
@@ -1207,6 +2166,235 @@ var OidcVaultStoreConflictError = class extends Error {
|
|
|
1207
2166
|
this.name = "OidcVaultStoreConflictError";
|
|
1208
2167
|
}
|
|
1209
2168
|
};
|
|
2169
|
+
var OidcVaultDpopReplayCapacityError = class extends Error {
|
|
2170
|
+
constructor(message = "OIDC vault DPoP replay store capacity is exhausted.") {
|
|
2171
|
+
super(message);
|
|
2172
|
+
this.name = "OidcVaultDpopReplayCapacityError";
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
|
|
2176
|
+
// src/dpop-replay.ts
|
|
2177
|
+
import { createHash as createHash5 } from "crypto";
|
|
2178
|
+
var isPrintableIdentifier = (value) => typeof value === "string" && /^[\x20-\x7e]{1,128}$/.test(value);
|
|
2179
|
+
var normalizeNamespaceBasePath = (origin, value) => {
|
|
2180
|
+
if (value !== void 0 && typeof value !== "string")
|
|
2181
|
+
throw new TypeError("DPoP vault basePath must be a static path.");
|
|
2182
|
+
const mounted = !value || value === "/" ? DEFAULT_OIDC_VAULT_BASE_PATH : `/${value.replace(/^\/+|\/+$/g, "")}`;
|
|
2183
|
+
if (/[\s\\?#]/.test(mounted) || /%(?![0-9a-f]{2})/i.test(mounted)) {
|
|
2184
|
+
throw new TypeError("DPoP vault basePath must be a static path without query, fragment or malformed escapes.");
|
|
2185
|
+
}
|
|
2186
|
+
const path = mounted.replace(/%([0-9a-f]{2})/gi, (_, hex) => {
|
|
2187
|
+
const character = String.fromCharCode(Number.parseInt(hex, 16));
|
|
2188
|
+
return /^[A-Za-z0-9._~-]$/.test(character) ? character : `%${hex.toUpperCase()}`;
|
|
2189
|
+
});
|
|
2190
|
+
const normalized = new URL(`${origin}${path}`).pathname;
|
|
2191
|
+
return normalized.replace(/\/+$/g, "") || "/";
|
|
2192
|
+
};
|
|
2193
|
+
var resolveDpopEffectiveNamespace = (space) => {
|
|
2194
|
+
if (space?.type === "vault") {
|
|
2195
|
+
const { backendOrigin, basePath, issuer, clientId } = space;
|
|
2196
|
+
const origin = resolveDeviceBindingOrigin(backendOrigin);
|
|
2197
|
+
if (typeof issuer !== "string" || issuer.length === 0 || typeof clientId !== "string" || clientId.length === 0) {
|
|
2198
|
+
throw new TypeError("DPoP vault namespace requires resolved issuer and clientId.");
|
|
2199
|
+
}
|
|
2200
|
+
return Object.freeze(["vault", origin, normalizeNamespaceBasePath(origin, basePath), issuer, clientId]);
|
|
2201
|
+
}
|
|
2202
|
+
if (space?.type === "api") {
|
|
2203
|
+
const { publicOrigin, replayNamespace } = space;
|
|
2204
|
+
if (!isPrintableIdentifier(replayNamespace)) {
|
|
2205
|
+
throw new TypeError("DPoP replayNamespace must contain 1 through 128 printable ASCII bytes.");
|
|
2206
|
+
}
|
|
2207
|
+
return Object.freeze(["api", resolveDeviceBindingOrigin(publicOrigin, "publicOrigin"), replayNamespace]);
|
|
2208
|
+
}
|
|
2209
|
+
throw new TypeError("DPoP replay policy requires a vault or API protection space.");
|
|
2210
|
+
};
|
|
2211
|
+
var createDpopReplayKey = (effectiveNamespace, jkt, jti) => {
|
|
2212
|
+
if (!isCanonicalDpopJkt(jkt) || !isPrintableIdentifier(jti)) {
|
|
2213
|
+
throw new TypeError("DPoP replay key requires a canonical thumbprint and a bounded printable JTI.");
|
|
2214
|
+
}
|
|
2215
|
+
return `dpop:v1:${createHash5("sha256").update(JSON.stringify([effectiveNamespace, jkt, jti]), "utf8").digest("base64url")}`;
|
|
2216
|
+
};
|
|
2217
|
+
var getDpopReplayExpiresAt = (input) => {
|
|
2218
|
+
const { iat, now, proofMaxAgeSeconds: age, clockSkewSeconds: skew } = input;
|
|
2219
|
+
if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || !Number.isSafeInteger(now) || now < 0 || !Number.isInteger(age) || age < 1 || age > 300 || !Number.isInteger(skew) || skew < 0 || skew > 30)
|
|
2220
|
+
return null;
|
|
2221
|
+
const issuedAt = iat * 1e3;
|
|
2222
|
+
const expiresAt = (iat + age + skew) * 1e3;
|
|
2223
|
+
if (!Number.isSafeInteger(issuedAt) || !Number.isSafeInteger(expiresAt) || issuedAt - now > skew * 1e3 || expiresAt <= now || expiresAt - now > 36e4)
|
|
2224
|
+
return null;
|
|
2225
|
+
return expiresAt;
|
|
2226
|
+
};
|
|
2227
|
+
var invalidProof2 = (diagnostic) => new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_DPOP_PROOF", diagnostic, "DPoP proof validation failed.");
|
|
2228
|
+
var replayUnavailable = (cause) => {
|
|
2229
|
+
const error = new OidcVaultHttpError(
|
|
2230
|
+
503,
|
|
2231
|
+
"OIDC_VAULT_DPOP_REPLAY_UNAVAILABLE",
|
|
2232
|
+
"DPoP replay admission failed.",
|
|
2233
|
+
"DPoP replay protection is unavailable."
|
|
2234
|
+
);
|
|
2235
|
+
Object.defineProperty(error, "cause", { value: cause, configurable: true });
|
|
2236
|
+
return error;
|
|
2237
|
+
};
|
|
2238
|
+
var createDpopReplayPolicy = (options) => {
|
|
2239
|
+
const { protectionSpace, proofOptions, replayStore, now: configuredNow } = options;
|
|
2240
|
+
const policy = resolveDeviceBindingOptions(proofOptions);
|
|
2241
|
+
if (!policy) throw new TypeError("DPoP replay policy requires enabled proof options.");
|
|
2242
|
+
if (typeof replayStore?.reserveDpopProof !== "function")
|
|
2243
|
+
throw new TypeError("DPoP replay policy requires a replay store.");
|
|
2244
|
+
if (configuredNow !== void 0 && typeof configuredNow !== "function")
|
|
2245
|
+
throw new TypeError("DPoP replay clock must be callable.");
|
|
2246
|
+
const now = configuredNow ?? Date.now;
|
|
2247
|
+
const effectiveNamespace = resolveDpopEffectiveNamespace(protectionSpace);
|
|
2248
|
+
const noncePolicy = policy.nonce === false ? void 0 : createDpopNoncePolicy({
|
|
2249
|
+
...policy.nonce,
|
|
2250
|
+
namespace: JSON.stringify(effectiveNamespace)
|
|
2251
|
+
});
|
|
2252
|
+
return Object.freeze({
|
|
2253
|
+
effectiveNamespace,
|
|
2254
|
+
async verifyAndReserve(verifyBeforeReplay) {
|
|
2255
|
+
const verified = await verifyBeforeReplay();
|
|
2256
|
+
if (typeof verified !== "object" || verified === null || Array.isArray(verified)) {
|
|
2257
|
+
throw invalidProof2("DPoP verifier did not return a proof verification result.");
|
|
2258
|
+
}
|
|
2259
|
+
const { binding: verifiedBinding, iat, jti, nonce } = verified;
|
|
2260
|
+
if (typeof verifiedBinding !== "object" || verifiedBinding === null || Array.isArray(verifiedBinding)) {
|
|
2261
|
+
throw invalidProof2("DPoP verifier did not return a verified key binding.");
|
|
2262
|
+
}
|
|
2263
|
+
const { type, jkt, alg } = verifiedBinding;
|
|
2264
|
+
if (type !== "dpop" || !isCanonicalDpopJkt(jkt) || !isDpopAlgorithm(alg) || !policy.algorithms.includes(alg) || !isPrintableIdentifier(jti)) {
|
|
2265
|
+
throw invalidProof2("DPoP verified key/algorithm or signed JTI shape is invalid.");
|
|
2266
|
+
}
|
|
2267
|
+
const binding = Object.freeze({ type, jkt, alg });
|
|
2268
|
+
let checkedNow;
|
|
2269
|
+
try {
|
|
2270
|
+
checkedNow = now();
|
|
2271
|
+
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0) throw new TypeError("DPoP replay clock is invalid.");
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
throw replayUnavailable(error);
|
|
2274
|
+
}
|
|
2275
|
+
const expiresAt = getDpopReplayExpiresAt({ ...policy, iat, now: checkedNow });
|
|
2276
|
+
if (expiresAt === null) throw invalidProof2("DPoP signed iat is outside the bounded replay window.");
|
|
2277
|
+
if (noncePolicy) {
|
|
2278
|
+
if (!noncePolicy.verify(nonce, jkt, checkedNow)) {
|
|
2279
|
+
return Object.freeze({ type: "nonce-challenge", nonce: noncePolicy.issue(jkt, checkedNow) });
|
|
2280
|
+
}
|
|
2281
|
+
} else if (nonce !== void 0 && (typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
2282
|
+
throw invalidProof2("DPoP signed nonce claim exceeds its string/byte bound.");
|
|
2283
|
+
}
|
|
2284
|
+
let reserved;
|
|
2285
|
+
try {
|
|
2286
|
+
reserved = await replayStore.reserveDpopProof({
|
|
2287
|
+
replayKey: createDpopReplayKey(effectiveNamespace, jkt, jti),
|
|
2288
|
+
expiresAt
|
|
2289
|
+
});
|
|
2290
|
+
if (typeof reserved !== "boolean")
|
|
2291
|
+
throw new TypeError("DPoP replay store returned a non-boolean admission result.");
|
|
2292
|
+
} catch (error) {
|
|
2293
|
+
throw replayUnavailable(error);
|
|
2294
|
+
}
|
|
2295
|
+
if (!reserved) throw invalidProof2("DPoP replay reservation was duplicate or no longer live.");
|
|
2296
|
+
return Object.freeze({ type: "accepted", binding, expiresAt });
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
};
|
|
2300
|
+
var challenge = (error, algorithms) => {
|
|
2301
|
+
if (algorithms.length === 0 || !algorithms.every(isDpopAlgorithm))
|
|
2302
|
+
throw new TypeError("DPoP challenge requires a resolved algorithm allowlist.");
|
|
2303
|
+
return `DPoP error="${error}", algs="${[...new Set(algorithms)].join(" ")}"`;
|
|
2304
|
+
};
|
|
2305
|
+
var toDpopReplayErrorResponse = (error, algorithms) => {
|
|
2306
|
+
const { status, code, message } = toErrorPayload(error);
|
|
2307
|
+
return Object.freeze({
|
|
2308
|
+
status,
|
|
2309
|
+
body: Object.freeze({ code, message }),
|
|
2310
|
+
headers: Object.freeze({
|
|
2311
|
+
"Cache-Control": "no-store",
|
|
2312
|
+
...status === 401 && (code === "OIDC_VAULT_INVALID_DPOP_PROOF" || code === "OIDC_VAULT_DPOP_REQUIRED") ? { "WWW-Authenticate": challenge("invalid_dpop_proof", algorithms) } : {}
|
|
2313
|
+
})
|
|
2314
|
+
});
|
|
2315
|
+
};
|
|
2316
|
+
var toDpopNonceChallengeResponse = (result, endpoint, algorithms) => {
|
|
2317
|
+
const nonce = result?.nonce;
|
|
2318
|
+
if (result?.type !== "nonce-challenge" || typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || !/^v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{43}$/.test(nonce))
|
|
2319
|
+
throw new TypeError("DPoP nonce response requires a bounded server-issued challenge.");
|
|
2320
|
+
if (endpoint !== "vault-post" && endpoint !== "api")
|
|
2321
|
+
throw new TypeError("DPoP nonce response requires an API or vault POST endpoint.");
|
|
2322
|
+
return Object.freeze({
|
|
2323
|
+
status: endpoint === "api" ? 401 : 400,
|
|
2324
|
+
body: Object.freeze({ code: "OIDC_VAULT_USE_DPOP_NONCE", message: "A fresh DPoP nonce is required." }),
|
|
2325
|
+
headers: Object.freeze({
|
|
2326
|
+
"Cache-Control": "no-store",
|
|
2327
|
+
"DPoP-Nonce": nonce,
|
|
2328
|
+
...endpoint === "api" ? { "WWW-Authenticate": challenge("use_dpop_nonce", algorithms) } : {}
|
|
2329
|
+
})
|
|
2330
|
+
});
|
|
2331
|
+
};
|
|
2332
|
+
|
|
2333
|
+
// src/vault-route-proof.ts
|
|
2334
|
+
var captureVaultRouteProof = (req) => Object.freeze({
|
|
2335
|
+
header: readDpopRawHeader(req, "dpop"),
|
|
2336
|
+
method: req.method,
|
|
2337
|
+
originalUrl: req.originalUrl
|
|
2338
|
+
});
|
|
2339
|
+
var VaultDpopNonceChallengeError = class extends OidcVaultHttpError {
|
|
2340
|
+
constructor(response) {
|
|
2341
|
+
super(400, "OIDC_VAULT_USE_DPOP_NONCE", "A fresh DPoP nonce is required.");
|
|
2342
|
+
this.response = response;
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) => {
|
|
2346
|
+
const policy = options.deviceBinding;
|
|
2347
|
+
if (policy === void 0) return void 0;
|
|
2348
|
+
assertDeviceBindingStoreCapabilities(options.storeProvider);
|
|
2349
|
+
const targetFor = createDpopRequestTargetResolver({ publicOrigin: backendOrigin });
|
|
2350
|
+
const replay = createDpopReplayPolicy({
|
|
2351
|
+
// The config resolver requires both identifiers in manual/discovery modes.
|
|
2352
|
+
protectionSpace: { type: "vault", backendOrigin, basePath, issuer: config.issuer, clientId: config.clientId },
|
|
2353
|
+
proofOptions: policy,
|
|
2354
|
+
replayStore: options.storeProvider,
|
|
2355
|
+
now: options.now
|
|
2356
|
+
});
|
|
2357
|
+
const admit = async (request, proof, expectedJkt) => {
|
|
2358
|
+
const targetUrl = targetFor(request.originalUrl);
|
|
2359
|
+
const result = await replay.verifyAndReserve(
|
|
2360
|
+
() => verifyDpopProof({
|
|
2361
|
+
proof,
|
|
2362
|
+
method: request.method,
|
|
2363
|
+
targetUrl,
|
|
2364
|
+
proofOptions: policy,
|
|
2365
|
+
expectedJkt
|
|
2366
|
+
})
|
|
2367
|
+
);
|
|
2368
|
+
if (result.type === "nonce-challenge") {
|
|
2369
|
+
throw new VaultDpopNonceChallengeError(toDpopNonceChallengeResponse(result, "vault-post", policy.algorithms));
|
|
2370
|
+
}
|
|
2371
|
+
return result.binding;
|
|
2372
|
+
};
|
|
2373
|
+
return Object.freeze({
|
|
2374
|
+
async initiate(request) {
|
|
2375
|
+
const proof = extractDpopProof(request.header);
|
|
2376
|
+
if (proof === void 0) {
|
|
2377
|
+
assertDeviceBindingPolicy(policy, void 0);
|
|
2378
|
+
return void 0;
|
|
2379
|
+
}
|
|
2380
|
+
return admit(request, proof);
|
|
2381
|
+
},
|
|
2382
|
+
async verify(request, originalBinding) {
|
|
2383
|
+
const binding = snapshotDpopBinding(originalBinding);
|
|
2384
|
+
assertDeviceBindingPolicy(policy, binding);
|
|
2385
|
+
const proof = extractDpopProof(request.header);
|
|
2386
|
+
const verified = proof === void 0 ? void 0 : await admit(request, proof, binding?.jkt);
|
|
2387
|
+
return resolveVerifiedDeviceBinding(policy, binding, verified);
|
|
2388
|
+
}
|
|
2389
|
+
});
|
|
2390
|
+
};
|
|
2391
|
+
var toVaultRouteErrorResponse = (error, policy) => error instanceof VaultDpopNonceChallengeError ? error.response : toDpopReplayErrorResponse(error, policy?.algorithms ?? ["ES256"]);
|
|
2392
|
+
var sendVaultRoutePolicyResponse = (res, response) => {
|
|
2393
|
+
res.removeHeader("WWW-Authenticate");
|
|
2394
|
+
res.removeHeader("DPoP-Nonce");
|
|
2395
|
+
for (const [name, value] of Object.entries(response.headers)) res.setHeader(name, value);
|
|
2396
|
+
res.status(response.status).json(response.body);
|
|
2397
|
+
};
|
|
1210
2398
|
|
|
1211
2399
|
// src/access-token-middleware.ts
|
|
1212
2400
|
var extractBearerToken = (authorizationHeader) => {
|
|
@@ -1223,72 +2411,212 @@ var extractBearerToken = (authorizationHeader) => {
|
|
|
1223
2411
|
}
|
|
1224
2412
|
return token;
|
|
1225
2413
|
};
|
|
1226
|
-
var
|
|
1227
|
-
res.setHeader("WWW-Authenticate", "Bearer");
|
|
1228
|
-
};
|
|
1229
|
-
var notifyBearerMiddlewareError = async (options, context) => {
|
|
2414
|
+
var notifyMiddlewareError = async (onError, context) => {
|
|
1230
2415
|
try {
|
|
1231
|
-
await
|
|
2416
|
+
await onError?.(context);
|
|
1232
2417
|
} catch {
|
|
1233
2418
|
}
|
|
1234
2419
|
};
|
|
2420
|
+
var schemeOf = (value) => {
|
|
2421
|
+
const scheme = value?.trim().split(/\s+/, 1)[0]?.toLowerCase();
|
|
2422
|
+
return scheme === "bearer" ? "Bearer" : scheme === "dpop" ? "DPoP" : void 0;
|
|
2423
|
+
};
|
|
2424
|
+
var mappedFields = (result) => {
|
|
2425
|
+
if (typeof result !== "object" || result === null || Array.isArray(result))
|
|
2426
|
+
throw invalidAccessToken("Access token validator result is invalid.");
|
|
2427
|
+
const { subject, sessionId, scope, claims } = result;
|
|
2428
|
+
if (typeof subject !== "string" || subject.length === 0)
|
|
2429
|
+
throw invalidAccessToken("Access token validator result is missing subject.");
|
|
2430
|
+
return { subject, sessionId, scope, claims };
|
|
2431
|
+
};
|
|
1235
2432
|
function createOidcVaultAccessTokenMiddleware(options) {
|
|
2433
|
+
const { validator, deviceBinding: configuredBinding, onAuthContext, onError } = options;
|
|
2434
|
+
const policy = resolveDeviceBindingOptions(configuredBinding);
|
|
2435
|
+
const validate = validator.validate.bind(validator);
|
|
2436
|
+
const requestAware = policy ? validator.validateWithRequest : void 0;
|
|
2437
|
+
if (policy && typeof requestAware !== "function")
|
|
2438
|
+
throw new TypeError("deviceBinding requires validator.validateWithRequest.");
|
|
2439
|
+
const validateWithRequest = requestAware?.bind(validator);
|
|
2440
|
+
const apiOptions = policy ? (() => {
|
|
2441
|
+
const { publicOrigin, publicPathPrefix, replayNamespace, replayStore, now } = configuredBinding;
|
|
2442
|
+
return {
|
|
2443
|
+
publicOrigin: resolveDeviceBindingOrigin(publicOrigin, "publicOrigin"),
|
|
2444
|
+
publicPathPrefix,
|
|
2445
|
+
replayNamespace,
|
|
2446
|
+
replayStore,
|
|
2447
|
+
now
|
|
2448
|
+
};
|
|
2449
|
+
})() : void 0;
|
|
2450
|
+
const targetFor = apiOptions ? createDpopRequestTargetResolver(apiOptions) : void 0;
|
|
2451
|
+
const replay = policy ? createDpopReplayPolicy({
|
|
2452
|
+
protectionSpace: {
|
|
2453
|
+
type: "api",
|
|
2454
|
+
publicOrigin: apiOptions.publicOrigin,
|
|
2455
|
+
replayNamespace: apiOptions.replayNamespace
|
|
2456
|
+
},
|
|
2457
|
+
proofOptions: policy,
|
|
2458
|
+
replayStore: apiOptions.replayStore,
|
|
2459
|
+
now: apiOptions.now
|
|
2460
|
+
}) : void 0;
|
|
2461
|
+
const algorithms = policy?.algorithms ?? ["ES256"];
|
|
2462
|
+
const algs = algorithms.join(" ");
|
|
2463
|
+
const dpopChallenge = (error) => `DPoP${error ? ` error="${error}",` : ""} algs="${algs}"`;
|
|
2464
|
+
const proofChallenge = dpopChallenge("invalid_dpop_proof");
|
|
1236
2465
|
return async (req, res, next) => {
|
|
1237
2466
|
let token;
|
|
2467
|
+
let scheme;
|
|
2468
|
+
let nonceResponse;
|
|
2469
|
+
const authenticatedRequest = req;
|
|
2470
|
+
delete authenticatedRequest.auth;
|
|
2471
|
+
if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
|
|
2472
|
+
const prepareError = (error, hookFailure = false) => {
|
|
2473
|
+
const response = nonceResponse ?? (error instanceof OidcVaultHttpError ? toDpopReplayErrorResponse(error, algorithms) : {
|
|
2474
|
+
status: hookFailure ? 500 : 401,
|
|
2475
|
+
body: hookFailure ? { code: "OIDC_VAULT_AUTH_CONTEXT_FAILED", message: "Auth context hook failed." } : { code: "OIDC_VAULT_INVALID_ACCESS_TOKEN", message: "Access token validation failed." },
|
|
2476
|
+
headers: { "Cache-Control": "no-store" }
|
|
2477
|
+
});
|
|
2478
|
+
const headers = { ...response.headers };
|
|
2479
|
+
if (response.status === 401 && headers["WWW-Authenticate"] === void 0) {
|
|
2480
|
+
const code = response.body.code;
|
|
2481
|
+
const challenge2 = code === "OIDC_VAULT_DPOP_REQUIRED" || code === "OIDC_VAULT_INVALID_DPOP_PROOF" ? proofChallenge : !policy ? "Bearer" : code === "OIDC_VAULT_MISSING_ACCESS_TOKEN" ? policy.mode === "required" ? dpopChallenge() : `Bearer, ${dpopChallenge()}` : scheme === "DPoP" || !scheme && policy.mode === "required" ? dpopChallenge("invalid_token") : 'Bearer error="invalid_token"';
|
|
2482
|
+
headers["WWW-Authenticate"] = challenge2;
|
|
2483
|
+
}
|
|
2484
|
+
return Object.freeze({
|
|
2485
|
+
status: response.status,
|
|
2486
|
+
body: Object.freeze({ ...response.body }),
|
|
2487
|
+
headers: Object.freeze(headers)
|
|
2488
|
+
});
|
|
2489
|
+
};
|
|
2490
|
+
const sendError = (response) => {
|
|
2491
|
+
res.removeHeader("WWW-Authenticate");
|
|
2492
|
+
res.removeHeader("DPoP-Nonce");
|
|
2493
|
+
for (const [name, value] of Object.entries(response.headers)) res.setHeader(name, value);
|
|
2494
|
+
res.status(response.status).json(response.body);
|
|
2495
|
+
};
|
|
1238
2496
|
try {
|
|
1239
|
-
|
|
1240
|
-
|
|
2497
|
+
let result;
|
|
2498
|
+
let confirmation;
|
|
2499
|
+
let deviceBinding;
|
|
2500
|
+
if (!policy) {
|
|
2501
|
+
token = extractBearerToken(req.get("authorization"));
|
|
2502
|
+
scheme = "Bearer";
|
|
2503
|
+
result = await validate(token);
|
|
2504
|
+
delete authenticatedRequest.auth;
|
|
2505
|
+
const reported = result?.confirmation;
|
|
2506
|
+
confirmation = reported === void 0 ? void 0 : snapshotAccessTokenConfirmation(reported);
|
|
2507
|
+
result = mappedFields(result);
|
|
2508
|
+
delete authenticatedRequest.auth;
|
|
2509
|
+
if (confirmation)
|
|
2510
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
2511
|
+
} else {
|
|
2512
|
+
const authorization = readDpopRawHeader(req, "authorization");
|
|
2513
|
+
scheme = schemeOf(authorization.value);
|
|
2514
|
+
if (authorization.count === 0 || authorization.count === 1 && !authorization.value?.trim()) {
|
|
2515
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_MISSING_ACCESS_TOKEN", "Missing access token.");
|
|
2516
|
+
}
|
|
2517
|
+
const credential = authorization.value?.trim().split(/\s+/);
|
|
2518
|
+
if (authorization.count !== 1 || !scheme || credential?.length !== 2 || !credential[1] || !/^[\x21-\x7e]+$/.test(credential[1]) || credential[1].includes(",")) {
|
|
2519
|
+
throw new OidcVaultHttpError(
|
|
2520
|
+
401,
|
|
2521
|
+
"OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
|
|
2522
|
+
"Authorization header must use one Bearer or DPoP credential."
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
token = credential[1];
|
|
2526
|
+
const proofHeader = readDpopRawHeader(req, "dpop");
|
|
2527
|
+
const { method, originalUrl } = req;
|
|
2528
|
+
result = await validateWithRequest(Object.freeze({ token, scheme, req }));
|
|
2529
|
+
delete authenticatedRequest.auth;
|
|
2530
|
+
confirmation = snapshotAccessTokenConfirmation(result?.confirmation);
|
|
2531
|
+
result = mappedFields(result);
|
|
2532
|
+
delete authenticatedRequest.auth;
|
|
2533
|
+
if (confirmation && scheme !== "DPoP")
|
|
2534
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
2535
|
+
if (!confirmation && scheme === "DPoP")
|
|
2536
|
+
throw invalidAccessToken("DPoP presentation of a verified unbound token is invalid.");
|
|
2537
|
+
if (!confirmation && policy.mode === "required") {
|
|
2538
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_DEVICE_BINDING_REQUIRED", "A device-bound login is required.");
|
|
2539
|
+
}
|
|
2540
|
+
const proof = extractDpopProof(proofHeader);
|
|
2541
|
+
if (!proof && confirmation)
|
|
2542
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
2543
|
+
if (proof) {
|
|
2544
|
+
const targetUrl = targetFor(originalUrl);
|
|
2545
|
+
const admission = await replay.verifyAndReserve(
|
|
2546
|
+
() => verifyDpopProof({
|
|
2547
|
+
proof,
|
|
2548
|
+
method,
|
|
2549
|
+
targetUrl,
|
|
2550
|
+
proofOptions: policy,
|
|
2551
|
+
accessToken: token,
|
|
2552
|
+
expectedJkt: confirmation?.jkt
|
|
2553
|
+
})
|
|
2554
|
+
);
|
|
2555
|
+
if (admission.type === "nonce-challenge") {
|
|
2556
|
+
nonceResponse = toDpopNonceChallengeResponse(admission, "api", policy.algorithms);
|
|
2557
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_USE_DPOP_NONCE", "A fresh DPoP nonce is required.");
|
|
2558
|
+
}
|
|
2559
|
+
if (confirmation) deviceBinding = admission.binding;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
const authenticatedToken = token;
|
|
1241
2563
|
const auth = {
|
|
1242
|
-
|
|
1243
|
-
|
|
2564
|
+
...result,
|
|
2565
|
+
token: authenticatedToken,
|
|
2566
|
+
...confirmation === void 0 ? {} : { confirmation },
|
|
2567
|
+
...deviceBinding ? { deviceBinding } : {}
|
|
2568
|
+
};
|
|
2569
|
+
const restoreAuth = () => {
|
|
2570
|
+
const { subject, sessionId, scope, claims } = auth;
|
|
2571
|
+
return {
|
|
2572
|
+
subject,
|
|
2573
|
+
sessionId,
|
|
2574
|
+
scope,
|
|
2575
|
+
claims,
|
|
2576
|
+
token: authenticatedToken,
|
|
2577
|
+
...confirmation === void 0 ? {} : { confirmation },
|
|
2578
|
+
...deviceBinding ? { deviceBinding } : {}
|
|
2579
|
+
};
|
|
1244
2580
|
};
|
|
1245
|
-
const authenticatedRequest = req;
|
|
1246
2581
|
authenticatedRequest.auth = auth;
|
|
1247
2582
|
try {
|
|
1248
|
-
await
|
|
2583
|
+
await onAuthContext?.({ req: authenticatedRequest, res, auth });
|
|
2584
|
+
authenticatedRequest.auth = restoreAuth();
|
|
1249
2585
|
} catch (hookError) {
|
|
2586
|
+
const response = prepareError(hookError, true);
|
|
2587
|
+
delete authenticatedRequest.auth;
|
|
2588
|
+
let errorAuth;
|
|
2589
|
+
try {
|
|
2590
|
+
errorAuth = restoreAuth();
|
|
2591
|
+
} catch {
|
|
2592
|
+
errorAuth = {
|
|
2593
|
+
...result,
|
|
2594
|
+
token: authenticatedToken,
|
|
2595
|
+
...confirmation === void 0 ? {} : { confirmation },
|
|
2596
|
+
...deviceBinding ? { deviceBinding } : {}
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
await notifyMiddlewareError(onError, { error: hookError, req, res, token, auth: errorAuth });
|
|
1250
2600
|
delete authenticatedRequest.auth;
|
|
1251
|
-
await notifyBearerMiddlewareError(options, { error: hookError, req, res, token, auth });
|
|
1252
2601
|
if (res.headersSent) {
|
|
1253
2602
|
next(hookError);
|
|
1254
2603
|
return;
|
|
1255
2604
|
}
|
|
1256
|
-
|
|
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
|
-
});
|
|
2605
|
+
sendError(response);
|
|
1270
2606
|
return;
|
|
1271
2607
|
}
|
|
2608
|
+
if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
|
|
1272
2609
|
next();
|
|
1273
2610
|
} catch (error) {
|
|
1274
|
-
|
|
2611
|
+
const response = prepareError(error);
|
|
2612
|
+
delete authenticatedRequest.auth;
|
|
2613
|
+
await notifyMiddlewareError(onError, { error, req, res, ...token ? { token } : {} });
|
|
2614
|
+
delete authenticatedRequest.auth;
|
|
1275
2615
|
if (res.headersSent) {
|
|
1276
2616
|
next(error);
|
|
1277
2617
|
return;
|
|
1278
2618
|
}
|
|
1279
|
-
|
|
1280
|
-
setBearerChallengeHeader(res);
|
|
1281
|
-
res.status(error.status).json({
|
|
1282
|
-
code: error.code,
|
|
1283
|
-
message: error.clientMessage
|
|
1284
|
-
});
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
setBearerChallengeHeader(res);
|
|
1288
|
-
res.status(401).json({
|
|
1289
|
-
code: "OIDC_VAULT_INVALID_ACCESS_TOKEN",
|
|
1290
|
-
message: "Access token validation failed."
|
|
1291
|
-
});
|
|
2619
|
+
sendError(response);
|
|
1292
2620
|
}
|
|
1293
2621
|
};
|
|
1294
2622
|
}
|
|
@@ -1305,39 +2633,58 @@ var CREDENTIAL_RESPONSE_CACHE_CONTROL = "no-store";
|
|
|
1305
2633
|
var applyCredentialResponseCachePolicy = (res) => {
|
|
1306
2634
|
res.setHeader("Cache-Control", CREDENTIAL_RESPONSE_CACHE_CONTROL);
|
|
1307
2635
|
};
|
|
1308
|
-
var createOpaqueId = (prefix) => `${prefix}_${
|
|
1309
|
-
var createPkceVerifier = () =>
|
|
1310
|
-
var createPkceChallenge = (verifier) =>
|
|
2636
|
+
var createOpaqueId = (prefix) => `${prefix}_${randomBytes3(16).toString("base64url")}`;
|
|
2637
|
+
var createPkceVerifier = () => randomBytes3(32).toString("base64url");
|
|
2638
|
+
var createPkceChallenge = (verifier) => createHash6("sha256").update(verifier).digest("base64url");
|
|
1311
2639
|
var getCallbackUri = (backendOrigin, basePath) => `${backendOrigin}${basePath}${OIDC_VAULT_ROUTE_PATHS.callback}`;
|
|
1312
2640
|
var validateOidcVaultOptions = (options) => {
|
|
2641
|
+
const {
|
|
2642
|
+
deviceBinding: configuredDeviceBinding,
|
|
2643
|
+
fingerprintRecognition: configuredFingerprintRecognition,
|
|
2644
|
+
transactionCookie: configuredTransactionCookie,
|
|
2645
|
+
...otherOptions
|
|
2646
|
+
} = options;
|
|
2647
|
+
const cookie = otherOptions.cookie ? { ...otherOptions.cookie } : void 0;
|
|
2648
|
+
const cookieOptions = { ...otherOptions, cookie };
|
|
1313
2649
|
validateLifetimeOptions(options, getNow(options));
|
|
1314
|
-
const
|
|
2650
|
+
const deviceBinding = resolveDeviceBindingOptions(configuredDeviceBinding);
|
|
2651
|
+
const fingerprintRecognition = resolveFingerprintRecognitionOptions(configuredFingerprintRecognition);
|
|
2652
|
+
const backendOrigin = deviceBinding === void 0 ? resolveBackendOrigin(options) : resolveDeviceBindingOrigin(options.backendOrigin);
|
|
2653
|
+
if (deviceBinding !== void 0) assertDeviceBindingStoreCapabilities(options.storeProvider);
|
|
2654
|
+
if (fingerprintRecognition !== void 0)
|
|
2655
|
+
assertDeviceBindingStoreCapabilities(options.storeProvider, "fingerprintRecognition");
|
|
1315
2656
|
const frontendRedirectUri = resolveFrontendRedirectUri(options);
|
|
1316
2657
|
validatePostLogoutRedirectUri(options);
|
|
1317
|
-
validateCookieOptions(
|
|
2658
|
+
validateCookieOptions(cookieOptions);
|
|
2659
|
+
const transactionCookie = resolveTransactionCookieOptions(
|
|
2660
|
+
{
|
|
2661
|
+
...cookieOptions,
|
|
2662
|
+
deviceBinding: configuredDeviceBinding,
|
|
2663
|
+
fingerprintRecognition,
|
|
2664
|
+
transactionCookie: configuredTransactionCookie
|
|
2665
|
+
},
|
|
2666
|
+
backendOrigin
|
|
2667
|
+
);
|
|
1318
2668
|
const config = resolveOidcVaultConfig(options.config ? { ...options.config } : void 0);
|
|
1319
2669
|
const configuredTrustedOrigins = resolveTrustedOrigins(options);
|
|
1320
|
-
if (usesCrossSiteCookieTransport(
|
|
2670
|
+
if (usesCrossSiteCookieTransport(cookieOptions) && configuredTrustedOrigins.size === 0) {
|
|
1321
2671
|
throw new Error("trustedOrigins is required when using cross-site cookie transport.");
|
|
1322
2672
|
}
|
|
1323
2673
|
const trustedOrigins = new Set(configuredTrustedOrigins);
|
|
1324
2674
|
trustedOrigins.add(backendOrigin);
|
|
1325
2675
|
const resolvedOptions = {
|
|
1326
|
-
...
|
|
2676
|
+
...otherOptions,
|
|
2677
|
+
deviceBinding,
|
|
2678
|
+
fingerprintRecognition,
|
|
2679
|
+
transactionCookie,
|
|
1327
2680
|
...frontendRedirectUri !== void 0 ? { frontendRedirectUri } : {},
|
|
1328
2681
|
...options.trustedOrigins ? { trustedOrigins: [...options.trustedOrigins] } : {},
|
|
1329
|
-
...
|
|
2682
|
+
...cookie === void 0 ? {} : { cookie },
|
|
1330
2683
|
...options.config ? { config: { ...options.config } } : {}
|
|
1331
2684
|
};
|
|
1332
2685
|
return { backendOrigin, config, trustedOrigins, resolvedOptions };
|
|
1333
2686
|
};
|
|
1334
2687
|
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
|
-
};
|
|
1341
2688
|
var getSessionIdFromRequest = (req, options, action) => {
|
|
1342
2689
|
if (usesCookieTransport(options)) {
|
|
1343
2690
|
const cookieSessionId = getSessionIdFromCookie(req, options);
|
|
@@ -1357,13 +2704,6 @@ var getSessionIdFromRequest = (req, options, action) => {
|
|
|
1357
2704
|
"OIDC_VAULT_MISSING_SESSION_ID"
|
|
1358
2705
|
);
|
|
1359
2706
|
};
|
|
1360
|
-
var createExchangeResponse = (options, session, issuedToken) => ({
|
|
1361
|
-
sessionId: usesCookieTransport(options) ? void 0 : session.sessionId,
|
|
1362
|
-
user: session.user,
|
|
1363
|
-
accessToken: issuedToken.accessToken,
|
|
1364
|
-
expiresIn: issuedToken.expiresIn,
|
|
1365
|
-
tokenType: issuedToken.tokenType
|
|
1366
|
-
});
|
|
1367
2707
|
var appendQueryParam = (url, name, value) => {
|
|
1368
2708
|
if (value) {
|
|
1369
2709
|
url.searchParams.set(name, value);
|
|
@@ -1400,8 +2740,7 @@ var buildBackchannelLogoutReplayKey = (input) => {
|
|
|
1400
2740
|
return `v2:${encode(input.issuer ?? "")}:${encode(input.clientId ?? "")}:${encode(input.jti)}`;
|
|
1401
2741
|
};
|
|
1402
2742
|
var createHookContext = (route, req, res, session, metadata) => ({ route, req, res, session, metadata });
|
|
1403
|
-
var
|
|
1404
|
-
const rawReturnTo = req.query.returnTo;
|
|
2743
|
+
var resolveLoginReturnTo = (rawReturnTo, options) => {
|
|
1405
2744
|
if (!isString(rawReturnTo)) {
|
|
1406
2745
|
return void 0;
|
|
1407
2746
|
}
|
|
@@ -1429,31 +2768,12 @@ var resolveReturnTo = (req, options) => {
|
|
|
1429
2768
|
}
|
|
1430
2769
|
return resolvedUrl.toString();
|
|
1431
2770
|
};
|
|
2771
|
+
var resolveReturnTo = (req, options) => resolveLoginReturnTo(req.query.returnTo, options);
|
|
1432
2772
|
var appendCodeToRedirectUri = (redirectUri, code) => {
|
|
1433
2773
|
const url = new URL(redirectUri);
|
|
1434
2774
|
url.searchParams.set("code", code);
|
|
1435
2775
|
return url.toString();
|
|
1436
2776
|
};
|
|
1437
|
-
var withIssuedToken = async (req, res, options, session) => {
|
|
1438
|
-
if (!options.tokenIssuer) {
|
|
1439
|
-
return {};
|
|
1440
|
-
}
|
|
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 } };
|
|
1456
|
-
};
|
|
1457
2777
|
var resolveFrontendRedirectUri2 = (transaction, options) => {
|
|
1458
2778
|
const redirectUri = transaction.returnTo ?? options.frontendRedirectUri;
|
|
1459
2779
|
if (!redirectUri) {
|
|
@@ -1489,6 +2809,7 @@ async function callPostCommitHook(route, options, hook, req, res, session, metad
|
|
|
1489
2809
|
}
|
|
1490
2810
|
}
|
|
1491
2811
|
async function handleRouteError(route, req, res, next, options, error) {
|
|
2812
|
+
const response = toVaultRouteErrorResponse(error, options.deviceBinding);
|
|
1492
2813
|
try {
|
|
1493
2814
|
await options.hooks?.onError?.({
|
|
1494
2815
|
...createHookContext(route, req, res),
|
|
@@ -1500,12 +2821,7 @@ async function handleRouteError(route, req, res, next, options, error) {
|
|
|
1500
2821
|
next(error);
|
|
1501
2822
|
return;
|
|
1502
2823
|
}
|
|
1503
|
-
|
|
1504
|
-
const payload = toErrorPayload(error);
|
|
1505
|
-
res.status(payload.status).json({
|
|
1506
|
-
code: payload.code,
|
|
1507
|
-
message: payload.message
|
|
1508
|
-
});
|
|
2824
|
+
sendVaultRoutePolicyResponse(res, response);
|
|
1509
2825
|
}
|
|
1510
2826
|
function createAsyncHandler(route, options, handler) {
|
|
1511
2827
|
return async (req, res, next) => {
|
|
@@ -1531,6 +2847,7 @@ function createBodyParserErrorHandler() {
|
|
|
1531
2847
|
};
|
|
1532
2848
|
}
|
|
1533
2849
|
var createLoginHandler = (options, config, backendOrigin, basePath) => createAsyncHandler("login", options, async (req, res) => {
|
|
2850
|
+
assertDeviceBindingPolicy(options.deviceBinding, void 0);
|
|
1534
2851
|
const metadata = await resolveProviderMetadata(config, options);
|
|
1535
2852
|
await callHook("login", options.hooks?.onLoginStart, req, res, void 0, { provider: metadata });
|
|
1536
2853
|
const now = getNow(options);
|
|
@@ -1556,143 +2873,330 @@ var createLoginHandler = (options, config, backendOrigin, basePath) => createAsy
|
|
|
1556
2873
|
});
|
|
1557
2874
|
res.redirect(302, authorizationUrl);
|
|
1558
2875
|
});
|
|
1559
|
-
var
|
|
1560
|
-
|
|
1561
|
-
|
|
2876
|
+
var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrigin, basePath, proofVerifier) => createAsyncHandler("login", options, async (req, res) => {
|
|
2877
|
+
const capturedProof = captureVaultRouteProof(req);
|
|
2878
|
+
const fingerprintRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
|
|
2879
|
+
assertLoginInitiationRequest(req, options, trustedOrigins);
|
|
2880
|
+
const body = getBody(req);
|
|
2881
|
+
if (Array.isArray(req.body) || body.returnTo !== void 0 && !isString(body.returnTo)) {
|
|
2882
|
+
throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_REQUEST_BODY", "Login initiation request body is invalid.");
|
|
1562
2883
|
}
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_STATE", "OIDC state is invalid or expired.");
|
|
2884
|
+
const returnTo = resolveLoginReturnTo(body.returnTo, options);
|
|
2885
|
+
if (proofVerifier === void 0 && capturedProof.header.count !== 0) {
|
|
2886
|
+
throw new OidcVaultHttpError(
|
|
2887
|
+
401,
|
|
2888
|
+
"OIDC_VAULT_INVALID_DPOP_PROOF",
|
|
2889
|
+
"DPoP login initiation is not enabled.",
|
|
2890
|
+
"DPoP proof validation failed."
|
|
2891
|
+
);
|
|
1572
2892
|
}
|
|
1573
|
-
const
|
|
2893
|
+
const verifiedBinding = await proofVerifier?.initiate(capturedProof);
|
|
2894
|
+
const binding = snapshotDpopBinding(verifiedBinding);
|
|
1574
2895
|
const metadata = await resolveProviderMetadata(config, options);
|
|
1575
|
-
|
|
1576
|
-
metadata,
|
|
1577
|
-
{
|
|
1578
|
-
grant_type: "authorization_code",
|
|
1579
|
-
code,
|
|
1580
|
-
code_verifier: transaction.pkceVerifier,
|
|
1581
|
-
redirect_uri: getCallbackUri(backendOrigin, basePath)
|
|
1582
|
-
},
|
|
1583
|
-
options
|
|
1584
|
-
);
|
|
1585
|
-
validateCallbackTokenResponse(tokenResponse);
|
|
1586
|
-
await callHook("callback", options.hooks?.onCallbackTokens, req, res, void 0, {
|
|
1587
|
-
hasAccessToken: Boolean(tokenResponse.access_token),
|
|
1588
|
-
hasRefreshToken: true,
|
|
1589
|
-
hasIdToken: true
|
|
1590
|
-
});
|
|
1591
|
-
const claims = await verifyIdToken(metadata, tokenResponse.id_token, transaction.nonce, options);
|
|
1592
|
-
const subject = getRequiredString(claims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502);
|
|
1593
|
-
const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
|
|
1594
|
-
if (userInfo !== void 0) {
|
|
1595
|
-
assertUserInfoSubject(userInfo, subject);
|
|
1596
|
-
}
|
|
1597
|
-
if (userInfo !== void 0) {
|
|
1598
|
-
await callHook("callback", options.hooks?.onUserInfo, req, res, void 0, { subject });
|
|
1599
|
-
}
|
|
2896
|
+
await callHook("login", options.hooks?.onLoginStart, req, res, void 0, { provider: { ...metadata } });
|
|
1600
2897
|
const now = getNow(options);
|
|
1601
|
-
const
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
"
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
subject,
|
|
1612
|
-
providerSessionId: typeof claims.sid === "string" ? claims.sid : void 0,
|
|
1613
|
-
provider: {
|
|
1614
|
-
issuer: metadata.issuer ?? (typeof claims.iss === "string" ? claims.iss : void 0),
|
|
1615
|
-
clientId: metadata.clientId
|
|
1616
|
-
},
|
|
1617
|
-
refreshToken: tokenResponse.refresh_token,
|
|
1618
|
-
idToken: tokenResponse.id_token,
|
|
1619
|
-
accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : void 0,
|
|
1620
|
-
scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : metadata.scopes,
|
|
2898
|
+
const browserBinding = createTransactionBrowserBinding();
|
|
2899
|
+
const pkceVerifier = createPkceVerifier();
|
|
2900
|
+
const transaction = {
|
|
2901
|
+
state: createOpaqueId("state"),
|
|
2902
|
+
nonce: createOpaqueId("nonce"),
|
|
2903
|
+
pkceVerifier,
|
|
2904
|
+
codeChallenge: createPkceChallenge(pkceVerifier),
|
|
2905
|
+
returnTo,
|
|
2906
|
+
...binding === void 0 ? {} : { deviceBinding: binding },
|
|
2907
|
+
browserBindingHash: browserBinding.browserBindingHash,
|
|
1621
2908
|
createdAt: now,
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
2909
|
+
expiresAt: computeExpiresAt(
|
|
2910
|
+
now,
|
|
2911
|
+
options.authorizationTransactionTtlMs ?? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS,
|
|
2912
|
+
"authorizationTransactionTtlMs"
|
|
2913
|
+
),
|
|
2914
|
+
metadata: Object.freeze({
|
|
2915
|
+
...createTransactionProviderMetadata(config),
|
|
2916
|
+
...fingerprintRecognition === void 0 ? {} : { [FINGERPRINT_RECOGNITION_METADATA_KEY]: fingerprintRecognition }
|
|
2917
|
+
})
|
|
1628
2918
|
};
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
2919
|
+
const authorizationUrl = buildAuthorizationUrl(metadata, transaction, getCallbackUri(backendOrigin, basePath));
|
|
2920
|
+
const { state, expiresAt } = transaction;
|
|
2921
|
+
await options.storeProvider.createAuthorizationTransaction(transaction);
|
|
2922
|
+
await callHook("login", options.hooks?.onAuthorizationUrl, req, res, void 0, { authorizationUrl, state });
|
|
2923
|
+
setTransactionCookie(res, options.transactionCookie, browserBinding.secret, expiresAt, getNow(options));
|
|
2924
|
+
applyCredentialResponseCachePolicy(res);
|
|
2925
|
+
res.status(200).json({ authorizationUrl });
|
|
2926
|
+
});
|
|
2927
|
+
var createCallbackHandler = (options, config, backendOrigin, basePath) => createAsyncHandler("callback", options, async (req, res) => {
|
|
2928
|
+
const providerError = isString(req.query.error) ? req.query.error : void 0;
|
|
2929
|
+
const code = providerError === void 0 ? getRequiredString(
|
|
2930
|
+
req.query.code,
|
|
2931
|
+
"OIDC callback is missing the authorization code.",
|
|
2932
|
+
"OIDC_VAULT_MISSING_CODE"
|
|
2933
|
+
) : void 0;
|
|
2934
|
+
const state = getRequiredString(req.query.state, "OIDC callback is missing state.", "OIDC_VAULT_MISSING_STATE");
|
|
2935
|
+
const cookieHeader = req.headers.cookie;
|
|
2936
|
+
const store = options.storeProvider;
|
|
2937
|
+
const preflight = typeof store.getAuthorizationTransaction === "function" ? await store.getAuthorizationTransaction(state) : await store.consumeAuthorizationTransaction(state);
|
|
2938
|
+
if (!preflight) throw invalidAuthorizationState();
|
|
2939
|
+
const transaction = snapshotAuthorizationTransaction(preflight, state, getNow(options));
|
|
2940
|
+
assertAuthorizationTransactionIdentity(transaction, config);
|
|
2941
|
+
assertDeviceBindingPolicy(options.deviceBinding, transaction.deviceBinding);
|
|
2942
|
+
const cookie = options.transactionCookie;
|
|
2943
|
+
const cookieSecret = authenticateTransactionCookie(cookieHeader, cookie, transaction.match);
|
|
2944
|
+
const frontendDestination = providerError === void 0 ? resolveFrontendRedirectUri2(transaction, options) : void 0;
|
|
2945
|
+
if (typeof store.getAuthorizationTransaction === "function") {
|
|
2946
|
+
let consumed;
|
|
2947
|
+
if (typeof store.consumeAuthorizationTransactionIfMatches === "function") {
|
|
2948
|
+
consumed = await store.consumeAuthorizationTransactionIfMatches({ state, match: transaction.match });
|
|
2949
|
+
} else {
|
|
2950
|
+
if (transaction.browserBindingHash !== void 0) {
|
|
2951
|
+
throw new TypeError("Guarded callbacks require storeProvider.consumeAuthorizationTransactionIfMatches.");
|
|
2952
|
+
}
|
|
2953
|
+
consumed = await store.consumeAuthorizationTransaction(state);
|
|
2954
|
+
}
|
|
2955
|
+
if (!consumed) throw invalidAuthorizationState();
|
|
2956
|
+
const returned = snapshotAuthorizationTransaction(consumed, state, getNow(options));
|
|
2957
|
+
assertAuthorizationTransactionIdentity(returned, config);
|
|
2958
|
+
assertDeviceBindingPolicy(options.deviceBinding, returned.deviceBinding);
|
|
2959
|
+
assertAuthorizationTransactionMatches(returned, transaction);
|
|
1632
2960
|
}
|
|
1633
|
-
const createdSession = await options.storeProvider.createSession(session);
|
|
1634
|
-
const exchangeCode = createOpaqueId("code");
|
|
1635
2961
|
try {
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2962
|
+
if (providerError !== void 0) {
|
|
2963
|
+
throw new OidcVaultHttpError(400, "OIDC_VAULT_CALLBACK_ERROR", providerError, "OIDC callback failed.");
|
|
2964
|
+
}
|
|
2965
|
+
const metadata = await resolveProviderMetadata(config, options);
|
|
2966
|
+
const tokenResponse = await requestToken(
|
|
2967
|
+
metadata,
|
|
2968
|
+
{
|
|
2969
|
+
grant_type: "authorization_code",
|
|
2970
|
+
code,
|
|
2971
|
+
code_verifier: transaction.pkceVerifier,
|
|
2972
|
+
redirect_uri: getCallbackUri(backendOrigin, basePath)
|
|
2973
|
+
},
|
|
2974
|
+
options
|
|
2975
|
+
);
|
|
2976
|
+
validateCallbackTokenResponse(tokenResponse);
|
|
2977
|
+
await callHook("callback", options.hooks?.onCallbackTokens, req, res, void 0, {
|
|
2978
|
+
hasAccessToken: Boolean(tokenResponse.access_token),
|
|
2979
|
+
hasRefreshToken: true,
|
|
2980
|
+
hasIdToken: true
|
|
1642
2981
|
});
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
2982
|
+
const claims = await verifyIdToken(metadata, tokenResponse.id_token, transaction.nonce, options);
|
|
2983
|
+
const subject = getRequiredString(
|
|
2984
|
+
claims.sub,
|
|
2985
|
+
"OIDC id_token is missing sub.",
|
|
2986
|
+
"OIDC_VAULT_INVALID_ID_TOKEN",
|
|
2987
|
+
502
|
|
2988
|
+
);
|
|
2989
|
+
const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token, options) : void 0;
|
|
2990
|
+
if (userInfo !== void 0) {
|
|
2991
|
+
assertUserInfoSubject(userInfo, subject);
|
|
2992
|
+
await callHook("callback", options.hooks?.onUserInfo, req, res, void 0, { subject });
|
|
2993
|
+
}
|
|
2994
|
+
const now = getNow(options);
|
|
2995
|
+
const sessionExpiresAt = options.sessionTtlMs === void 0 ? void 0 : computeExpiresAt(now, options.sessionTtlMs, "sessionTtlMs");
|
|
2996
|
+
const exchangeExpiresAt = computeExpiresAt(
|
|
2997
|
+
now,
|
|
2998
|
+
options.exchangeCodeTtlMs ?? DEFAULT_EXCHANGE_CODE_TTL_MS,
|
|
2999
|
+
"exchangeCodeTtlMs"
|
|
3000
|
+
);
|
|
3001
|
+
const sessionId = createOpaqueId("sess");
|
|
3002
|
+
const provider = Object.freeze({
|
|
3003
|
+
issuer: metadata.issuer ?? (typeof claims.iss === "string" ? claims.iss : void 0),
|
|
3004
|
+
clientId: metadata.clientId
|
|
1646
3005
|
});
|
|
3006
|
+
const sessionBinding = transaction.deviceBinding;
|
|
3007
|
+
const recognition = transaction.fingerprintRecognition;
|
|
3008
|
+
const session = {
|
|
3009
|
+
sessionId,
|
|
3010
|
+
logicalSessionId: sessionId,
|
|
3011
|
+
subject,
|
|
3012
|
+
providerSessionId: typeof claims.sid === "string" ? claims.sid : void 0,
|
|
3013
|
+
provider: { ...provider },
|
|
3014
|
+
...sessionBinding === void 0 ? {} : { deviceBinding: { ...sessionBinding } },
|
|
3015
|
+
refreshToken: tokenResponse.refresh_token,
|
|
3016
|
+
idToken: tokenResponse.id_token,
|
|
3017
|
+
accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : void 0,
|
|
3018
|
+
scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : metadata.scopes,
|
|
3019
|
+
createdAt: now,
|
|
3020
|
+
updatedAt: now,
|
|
3021
|
+
...sessionExpiresAt === void 0 ? {} : { expiresAt: sessionExpiresAt },
|
|
3022
|
+
user: copyVaultUserProfile(mergeUserProfile(subject, claims, userInfo)),
|
|
3023
|
+
metadata: withFingerprintRecognitionMetadata({ tokenType: tokenResponse.token_type }, recognition)
|
|
3024
|
+
};
|
|
3025
|
+
await callHook("callback", options.hooks?.onBeforeSessionCreate, req, res, session, { subject });
|
|
3026
|
+
if (sessionExpiresAt !== void 0) {
|
|
3027
|
+
session.expiresAt = isUsableEpochMs(session.expiresAt) ? Math.min(session.expiresAt, sessionExpiresAt) : sessionExpiresAt;
|
|
3028
|
+
}
|
|
3029
|
+
const guarded = transaction.browserBindingHash !== void 0;
|
|
3030
|
+
const restoredBinding = guarded ? withSessionDeviceBinding(
|
|
3031
|
+
{
|
|
3032
|
+
...copySessionApplicationFields(session),
|
|
3033
|
+
sessionId,
|
|
3034
|
+
logicalSessionId: sessionId,
|
|
3035
|
+
provider: { ...provider }
|
|
3036
|
+
},
|
|
3037
|
+
sessionBinding
|
|
3038
|
+
) : withSessionDeviceBinding(session, sessionBinding);
|
|
3039
|
+
const restored = {
|
|
3040
|
+
...restoredBinding,
|
|
3041
|
+
user: copyVaultUserProfile(restoredBinding.user),
|
|
3042
|
+
metadata: withFingerprintRecognitionMetadata(restoredBinding.metadata, recognition)
|
|
3043
|
+
};
|
|
3044
|
+
const createdSession = await store.createSession(restored);
|
|
3045
|
+
const createdSessionId = guarded ? sessionId : createdSession.sessionId;
|
|
3046
|
+
const logicalSessionId = guarded ? sessionId : createdSession.logicalSessionId ?? createdSessionId;
|
|
3047
|
+
const exchangeCode = createOpaqueId("code");
|
|
3048
|
+
try {
|
|
3049
|
+
const returnedBinding = snapshotDpopBinding(createdSession.deviceBinding);
|
|
3050
|
+
if (returnedBinding?.jkt !== sessionBinding?.jkt) throw invalidAuthorizationState();
|
|
3051
|
+
if (!fingerprintRecognitionMatches(snapshotFingerprintRecognition(createdSession.metadata), recognition))
|
|
3052
|
+
throw invalidAuthorizationState();
|
|
3053
|
+
if (guarded && (createdSession.sessionId !== sessionId || createdSession.logicalSessionId !== sessionId || createdSession.provider?.issuer !== provider.issuer || createdSession.provider?.clientId !== provider.clientId)) {
|
|
3054
|
+
throw invalidAuthorizationState();
|
|
3055
|
+
}
|
|
3056
|
+
await store.createExchangeCode({
|
|
3057
|
+
code: exchangeCode,
|
|
3058
|
+
sessionId: createdSessionId,
|
|
3059
|
+
returnTo: transaction.returnTo,
|
|
3060
|
+
...sessionBinding === void 0 ? {} : { deviceBinding: sessionBinding },
|
|
3061
|
+
...transaction.browserBindingHash === void 0 ? {} : { browserBindingHash: transaction.browserBindingHash },
|
|
3062
|
+
createdAt: now,
|
|
3063
|
+
expiresAt: exchangeExpiresAt
|
|
3064
|
+
});
|
|
3065
|
+
} catch (error) {
|
|
3066
|
+
await store.deleteSessionsByLogicalSessionId({ logicalSessionId });
|
|
3067
|
+
throw error;
|
|
3068
|
+
}
|
|
3069
|
+
await callPostCommitHook(
|
|
3070
|
+
"callback",
|
|
3071
|
+
options,
|
|
3072
|
+
options.hooks?.onSessionCreated,
|
|
3073
|
+
req,
|
|
3074
|
+
res,
|
|
3075
|
+
withSessionDeviceBinding(copyVaultSession(createdSession), sessionBinding),
|
|
3076
|
+
{ subject }
|
|
3077
|
+
);
|
|
3078
|
+
if (cookieSecret !== void 0) {
|
|
3079
|
+
setTransactionCookie(res, cookie, cookieSecret, exchangeExpiresAt, getNow(options));
|
|
3080
|
+
}
|
|
3081
|
+
applyCredentialResponseCachePolicy(res);
|
|
3082
|
+
res.redirect(302, appendCodeToRedirectUri(frontendDestination, exchangeCode));
|
|
3083
|
+
} catch (error) {
|
|
3084
|
+
if (cookieSecret !== void 0) clearTransactionCookie(res, cookie);
|
|
1647
3085
|
throw error;
|
|
1648
3086
|
}
|
|
1649
|
-
await callPostCommitHook("callback", options, options.hooks?.onSessionCreated, req, res, createdSession, {
|
|
1650
|
-
subject
|
|
1651
|
-
});
|
|
1652
|
-
res.redirect(302, appendCodeToRedirectUri(frontendDestination, exchangeCode));
|
|
1653
3087
|
});
|
|
1654
|
-
var
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
3088
|
+
var assertLoginInitiationRequest = (req, options, trustedOrigins) => {
|
|
3089
|
+
assertTrustedOrigin(req, options, trustedOrigins, "login");
|
|
3090
|
+
if (!req.is("application/json")) {
|
|
3091
|
+
throw new OidcVaultHttpError(
|
|
3092
|
+
415,
|
|
3093
|
+
"OIDC_VAULT_UNSUPPORTED_REQUEST_BODY_TYPE",
|
|
3094
|
+
"Login initiation requires a JSON request body."
|
|
3095
|
+
);
|
|
1660
3096
|
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
3097
|
+
};
|
|
3098
|
+
var copySessionApplicationFields = (session) => {
|
|
3099
|
+
const result = {};
|
|
3100
|
+
for (const key of Object.keys(session)) {
|
|
3101
|
+
if (["deviceBinding", "browserBindingHash", "sessionId", "logicalSessionId", "provider"].includes(key)) continue;
|
|
3102
|
+
Object.defineProperty(result, key, {
|
|
3103
|
+
value: Reflect.get(session, key),
|
|
3104
|
+
enumerable: true,
|
|
3105
|
+
configurable: true,
|
|
3106
|
+
writable: true
|
|
3107
|
+
});
|
|
1664
3108
|
}
|
|
1665
|
-
|
|
1666
|
-
|
|
3109
|
+
return result;
|
|
3110
|
+
};
|
|
3111
|
+
var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("exchange", options, async (req, res) => {
|
|
3112
|
+
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
|
|
3113
|
+
const capturedRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
|
|
3114
|
+
const assertExchangeOrigin = captureTrustedOriginGuard(req, options, trustedOrigins, "exchange");
|
|
3115
|
+
const cookieHeader = req.headers.cookie;
|
|
3116
|
+
const body = getBody(req);
|
|
3117
|
+
const code = getRequiredString(body.code, "Exchange request is missing code.", "OIDC_VAULT_MISSING_EXCHANGE_CODE");
|
|
3118
|
+
const store = options.storeProvider;
|
|
3119
|
+
const canPreflight = typeof store.getExchangeCode === "function";
|
|
3120
|
+
if ((proofVerifier !== void 0 || options.fingerprintRecognition !== void 0) && (!canPreflight || typeof store.consumeExchangeCodeIfMatches !== "function")) {
|
|
3121
|
+
throw new TypeError("Device-bound exchange requires guarded store reads and atomic consumption.");
|
|
3122
|
+
}
|
|
3123
|
+
const rawCode = canPreflight ? await store.getExchangeCode(code) : await store.consumeExchangeCode(code);
|
|
3124
|
+
if (!rawCode) throw invalidExchangeCode();
|
|
3125
|
+
const record = snapshotVaultExchangeCode(rawCode, code, getNow(options));
|
|
3126
|
+
const guarded = record.browserBindingHash !== void 0;
|
|
3127
|
+
if (guarded) assertExchangeOrigin();
|
|
3128
|
+
assertDeviceBindingPolicy(options.deviceBinding, record.deviceBinding);
|
|
3129
|
+
const rawSession = await store.getSession(record.sessionId);
|
|
3130
|
+
if (!rawSession) throw invalidVaultSession();
|
|
3131
|
+
const session = snapshotVaultSession(rawSession, record.sessionId, getNow(options));
|
|
3132
|
+
assertDeviceBindingPolicy(options.deviceBinding, session.deviceBinding);
|
|
3133
|
+
assertVaultExchangeSessionBinding(record, session);
|
|
3134
|
+
const protectedExchange = proofVerifier !== void 0 || options.fingerprintRecognition !== void 0 || guarded;
|
|
3135
|
+
if (protectedExchange && typeof store.consumeExchangeCodeIfMatches !== "function") {
|
|
3136
|
+
throw new TypeError("Guarded exchange requires storeProvider.consumeExchangeCodeIfMatches.");
|
|
3137
|
+
}
|
|
3138
|
+
if (protectedExchange) assertVaultSessionIdentity(session, config);
|
|
3139
|
+
const cookieSecret = authenticateTransactionCookie(cookieHeader, options.transactionCookie, record.match);
|
|
3140
|
+
const recognition = options.fingerprintRecognition === void 0 ? void 0 : snapshotFingerprintRecognition(session.metadata);
|
|
3141
|
+
assertFingerprintRecognition(options.fingerprintRecognition, recognition, capturedRecognition);
|
|
3142
|
+
const verifiedBinding = proofVerifier === void 0 ? void 0 : await proofVerifier.verify(capturedProof, session.deviceBinding);
|
|
3143
|
+
if (canPreflight) {
|
|
3144
|
+
const consumed = protectedExchange ? await store.consumeExchangeCodeIfMatches({
|
|
3145
|
+
code,
|
|
3146
|
+
expectedSessionId: record.sessionId,
|
|
3147
|
+
match: record.match
|
|
3148
|
+
}) : await store.consumeExchangeCode(code);
|
|
3149
|
+
if (!consumed) throw invalidExchangeCode();
|
|
3150
|
+
const returned = snapshotVaultExchangeCode(consumed, code, getNow(options));
|
|
3151
|
+
assertVaultExchangeCodeMatches(returned, record);
|
|
3152
|
+
const returnedSession = await store.getSession(record.sessionId);
|
|
3153
|
+
if (!returnedSession) throw invalidVaultSession();
|
|
3154
|
+
const checkedSession = snapshotVaultSession(returnedSession, record.sessionId, getNow(options));
|
|
3155
|
+
assertVaultSessionIdentity(checkedSession, config);
|
|
3156
|
+
assertVaultExchangeSessionBinding(returned, checkedSession);
|
|
3157
|
+
assertVaultSessionMatches(checkedSession, session);
|
|
3158
|
+
if (options.fingerprintRecognition !== void 0 && !fingerprintRecognitionMatches(snapshotFingerprintRecognition(checkedSession.metadata), recognition))
|
|
3159
|
+
throw invalidVaultSession();
|
|
3160
|
+
}
|
|
3161
|
+
assertVaultSessionIdentity(session, config);
|
|
1667
3162
|
try {
|
|
1668
|
-
issuedToken = await withIssuedToken(req, res, options, session);
|
|
3163
|
+
const issuedToken = await withIssuedToken(req, res, options, session, verifiedBinding);
|
|
3164
|
+
if (usesCookieTransport(options)) setSessionCookie(res, options, session.sessionId);
|
|
3165
|
+
if (cookieSecret !== void 0) clearTransactionCookie(res, options.transactionCookie);
|
|
3166
|
+
applyCredentialResponseCachePolicy(res);
|
|
3167
|
+
res.status(200).json(createExchangeResponse(options, session, issuedToken));
|
|
1669
3168
|
} catch (error) {
|
|
1670
|
-
|
|
1671
|
-
logicalSessionId: session.logicalSessionId ?? session.sessionId
|
|
1672
|
-
});
|
|
1673
|
-
if (usesCookieTransport(options)) {
|
|
1674
|
-
clearSessionCookie(res, options);
|
|
1675
|
-
}
|
|
3169
|
+
if (cookieSecret !== void 0) clearTransactionCookie(res, options.transactionCookie);
|
|
1676
3170
|
throw error;
|
|
1677
3171
|
}
|
|
1678
|
-
if (usesCookieTransport(options)) {
|
|
1679
|
-
setSessionCookie(res, options, session.sessionId);
|
|
1680
|
-
}
|
|
1681
|
-
const response = createExchangeResponse(options, session, issuedToken);
|
|
1682
|
-
res.status(200).json(response);
|
|
1683
3172
|
});
|
|
1684
|
-
var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandler("refresh", options, async (req, res) => {
|
|
3173
|
+
var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("refresh", options, async (req, res) => {
|
|
3174
|
+
const capturedRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
|
|
1685
3175
|
assertTrustedOrigin(req, options, trustedOrigins, "refresh");
|
|
3176
|
+
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
|
|
1686
3177
|
const sessionId = getSessionIdFromRequest(req, options, "refresh");
|
|
1687
|
-
const
|
|
1688
|
-
if (!
|
|
3178
|
+
const currentRecord = await options.storeProvider.getSession(sessionId);
|
|
3179
|
+
if (!currentRecord) {
|
|
1689
3180
|
if (usesCookieTransport(options)) {
|
|
1690
3181
|
clearSessionCookie(res, options);
|
|
1691
3182
|
}
|
|
1692
3183
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
1693
3184
|
}
|
|
1694
|
-
|
|
3185
|
+
const currentSession = snapshotVaultSession(currentRecord, sessionId, getNow(options));
|
|
3186
|
+
assertVaultSessionIdentity(currentSession, config);
|
|
3187
|
+
assertDeviceBindingPolicy(options.deviceBinding, currentSession.deviceBinding);
|
|
3188
|
+
const recognition = options.fingerprintRecognition === void 0 ? void 0 : snapshotFingerprintRecognition(currentSession.metadata);
|
|
3189
|
+
assertFingerprintRecognition(options.fingerprintRecognition, recognition, capturedRecognition);
|
|
3190
|
+
const verifiedBinding = proofVerifier === void 0 ? void 0 : await proofVerifier.verify(capturedProof, currentSession.deviceBinding);
|
|
1695
3191
|
const metadata = await resolveProviderMetadata(config, options);
|
|
3192
|
+
if (proofVerifier !== void 0 || options.fingerprintRecognition !== void 0) {
|
|
3193
|
+
const latest = await options.storeProvider.getSession(sessionId);
|
|
3194
|
+
if (!latest) throw invalidVaultSession();
|
|
3195
|
+
const latestSession = snapshotVaultSession(latest, sessionId, getNow(options));
|
|
3196
|
+
assertVaultSessionMatches(latestSession, currentSession);
|
|
3197
|
+
if (options.fingerprintRecognition !== void 0 && !fingerprintRecognitionMatches(snapshotFingerprintRecognition(latestSession.metadata), recognition))
|
|
3198
|
+
throw invalidVaultSession();
|
|
3199
|
+
}
|
|
1696
3200
|
const tokenResponse = await requestToken(
|
|
1697
3201
|
metadata,
|
|
1698
3202
|
{
|
|
@@ -1716,7 +3220,7 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
|
|
|
1716
3220
|
const now = getNow(options);
|
|
1717
3221
|
const logicalSessionId = currentSession.logicalSessionId ?? currentSession.sessionId;
|
|
1718
3222
|
const nextSession = {
|
|
1719
|
-
...currentSession,
|
|
3223
|
+
...copyVaultSession(currentSession),
|
|
1720
3224
|
sessionId: createOpaqueId("sess"),
|
|
1721
3225
|
logicalSessionId,
|
|
1722
3226
|
subject,
|
|
@@ -1727,13 +3231,14 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
|
|
|
1727
3231
|
scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : currentSession.scope,
|
|
1728
3232
|
expiresAt: currentSession.expiresAt,
|
|
1729
3233
|
updatedAt: now,
|
|
1730
|
-
user: composeRefreshedUserProfile(subject, currentSession.user, freshClaims, userInfo)
|
|
3234
|
+
user: copyVaultUserProfile(composeRefreshedUserProfile(subject, currentSession.user, freshClaims, userInfo))
|
|
1731
3235
|
};
|
|
1732
|
-
|
|
3236
|
+
const expectedSession = snapshotVaultSession(nextSession, nextSession.sessionId, getNow(options));
|
|
3237
|
+
let rotatedRecord;
|
|
1733
3238
|
try {
|
|
1734
|
-
|
|
3239
|
+
rotatedRecord = await options.storeProvider.rotateSession({
|
|
1735
3240
|
sessionId: currentSession.sessionId,
|
|
1736
|
-
nextSession
|
|
3241
|
+
nextSession: copyVaultSession(expectedSession)
|
|
1737
3242
|
});
|
|
1738
3243
|
} catch (error) {
|
|
1739
3244
|
if (isStoreConflictError(error)) {
|
|
@@ -1744,51 +3249,109 @@ var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandl
|
|
|
1744
3249
|
}
|
|
1745
3250
|
throw error;
|
|
1746
3251
|
}
|
|
1747
|
-
let issuedToken;
|
|
1748
3252
|
try {
|
|
1749
|
-
|
|
3253
|
+
const returnedSession = snapshotVaultSession(rotatedRecord, expectedSession.sessionId, getNow(options));
|
|
3254
|
+
assertVaultSessionIdentity(returnedSession, config);
|
|
3255
|
+
assertVaultSessionMatches(returnedSession, expectedSession);
|
|
3256
|
+
if (options.fingerprintRecognition !== void 0 && !fingerprintRecognitionMatches(snapshotFingerprintRecognition(returnedSession.metadata), recognition))
|
|
3257
|
+
throw invalidVaultSession();
|
|
1750
3258
|
} catch (error) {
|
|
1751
|
-
await options.storeProvider.deleteSessionsByLogicalSessionId({
|
|
1752
|
-
|
|
1753
|
-
});
|
|
1754
|
-
if (usesCookieTransport(options)) {
|
|
1755
|
-
clearSessionCookie(res, options);
|
|
1756
|
-
}
|
|
3259
|
+
await options.storeProvider.deleteSessionsByLogicalSessionId({ logicalSessionId });
|
|
3260
|
+
if (usesCookieTransport(options)) clearSessionCookie(res, options);
|
|
1757
3261
|
throw error;
|
|
1758
3262
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
3263
|
+
const rotatedSession = expectedSession;
|
|
3264
|
+
const issuedToken = await withIssuedToken(req, res, options, rotatedSession, verifiedBinding);
|
|
3265
|
+
await callPostCommitHook(
|
|
3266
|
+
"refresh",
|
|
3267
|
+
options,
|
|
3268
|
+
options.hooks?.onSessionRefreshed,
|
|
3269
|
+
req,
|
|
3270
|
+
res,
|
|
3271
|
+
copyVaultSession(rotatedSession),
|
|
3272
|
+
{
|
|
3273
|
+
previousSessionId: currentSession.sessionId
|
|
3274
|
+
}
|
|
3275
|
+
);
|
|
1762
3276
|
if (usesCookieTransport(options)) {
|
|
1763
3277
|
setSessionCookie(res, options, rotatedSession.sessionId);
|
|
1764
3278
|
}
|
|
1765
3279
|
const response = createExchangeResponse(options, rotatedSession, issuedToken);
|
|
3280
|
+
applyCredentialResponseCachePolicy(res);
|
|
1766
3281
|
res.status(200).json(response);
|
|
1767
3282
|
});
|
|
1768
|
-
var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandler("logout", options, async (req, res) => {
|
|
3283
|
+
var createLogoutHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("logout", options, async (req, res) => {
|
|
1769
3284
|
assertTrustedOrigin(req, options, trustedOrigins, "logout");
|
|
3285
|
+
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
|
|
1770
3286
|
const body = getBody(req);
|
|
1771
3287
|
const sessionId = getSessionIdFromRequest(req, options, "logout");
|
|
1772
3288
|
const redirect = body.redirect === true;
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
3289
|
+
const store = options.storeProvider;
|
|
3290
|
+
const hasContext = typeof store.getSessionRevocationContext === "function";
|
|
3291
|
+
if (proofVerifier !== void 0 && !hasContext) {
|
|
3292
|
+
throw new TypeError("Device-bound logout requires storeProvider.getSessionRevocationContext.");
|
|
3293
|
+
}
|
|
3294
|
+
const rawContext = hasContext ? await store.getSessionRevocationContext(sessionId) : void 0;
|
|
3295
|
+
const context = rawContext ? snapshotVaultRevocationContext(rawContext) : void 0;
|
|
3296
|
+
const rawSession = await store.getSession(sessionId);
|
|
3297
|
+
const session = rawSession ? snapshotVaultSession(rawSession, sessionId, getNow(options)) : void 0;
|
|
3298
|
+
const finishLocalLogout = () => {
|
|
3299
|
+
if (usesCookieTransport(options)) clearSessionCookie(res, options);
|
|
3300
|
+
applyCredentialResponseCachePolicy(res);
|
|
1779
3301
|
res.status(200).json({ loggedOut: true });
|
|
3302
|
+
};
|
|
3303
|
+
if (hasContext && context === void 0) {
|
|
3304
|
+
if (session !== void 0) {
|
|
3305
|
+
assertDeviceBindingPolicy(options.deviceBinding, session.deviceBinding);
|
|
3306
|
+
throw invalidVaultSession();
|
|
3307
|
+
}
|
|
3308
|
+
finishLocalLogout();
|
|
1780
3309
|
return;
|
|
1781
3310
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
});
|
|
3311
|
+
const authority = context ?? (session === void 0 ? void 0 : snapshotVaultRevocationContext({
|
|
3312
|
+
logicalSessionId: session.logicalSessionId ?? session.sessionId,
|
|
3313
|
+
provider: session.provider,
|
|
3314
|
+
deviceBinding: session.deviceBinding
|
|
3315
|
+
}));
|
|
3316
|
+
if (authority === void 0) {
|
|
3317
|
+
await store.deleteSession(sessionId);
|
|
3318
|
+
finishLocalLogout();
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
assertVaultSessionIdentity(authority, config);
|
|
3322
|
+
assertDeviceBindingPolicy(options.deviceBinding, authority.deviceBinding);
|
|
3323
|
+
if (session !== void 0)
|
|
3324
|
+
assertVaultRevocationContextMatches(
|
|
3325
|
+
snapshotVaultRevocationContext({
|
|
3326
|
+
logicalSessionId: session.logicalSessionId ?? session.sessionId,
|
|
3327
|
+
provider: session.provider,
|
|
3328
|
+
deviceBinding: session.deviceBinding
|
|
3329
|
+
}),
|
|
3330
|
+
authority
|
|
3331
|
+
);
|
|
3332
|
+
if (proofVerifier !== void 0) await proofVerifier.verify(capturedProof, authority.deviceBinding);
|
|
3333
|
+
if (hasContext) {
|
|
3334
|
+
const latest = await store.getSessionRevocationContext(sessionId);
|
|
3335
|
+
if (!latest) {
|
|
3336
|
+
finishLocalLogout();
|
|
3337
|
+
return;
|
|
3338
|
+
}
|
|
3339
|
+
assertVaultRevocationContextMatches(snapshotVaultRevocationContext(latest), authority);
|
|
3340
|
+
}
|
|
3341
|
+
if (session !== void 0)
|
|
3342
|
+
await callHook("logout", options.hooks?.onBeforeLogout, req, res, copyVaultSession(session));
|
|
3343
|
+
await store.deleteSessionsByLogicalSessionId({ logicalSessionId: authority.logicalSessionId });
|
|
1787
3344
|
if (usesCookieTransport(options)) {
|
|
1788
3345
|
clearSessionCookie(res, options);
|
|
1789
3346
|
}
|
|
3347
|
+
if (session === void 0) {
|
|
3348
|
+
applyCredentialResponseCachePolicy(res);
|
|
3349
|
+
res.status(200).json({ loggedOut: true });
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
1790
3352
|
if (!redirect) {
|
|
1791
|
-
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, session);
|
|
3353
|
+
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, copyVaultSession(session));
|
|
3354
|
+
applyCredentialResponseCachePolicy(res);
|
|
1792
3355
|
res.status(200).json({ loggedOut: true });
|
|
1793
3356
|
return;
|
|
1794
3357
|
}
|
|
@@ -1799,13 +3362,14 @@ var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandle
|
|
|
1799
3362
|
} catch (error) {
|
|
1800
3363
|
try {
|
|
1801
3364
|
await options.hooks?.onError?.({
|
|
1802
|
-
...createHookContext("logout", req, res, session),
|
|
3365
|
+
...createHookContext("logout", req, res, copyVaultSession(session)),
|
|
1803
3366
|
error
|
|
1804
3367
|
});
|
|
1805
3368
|
} catch {
|
|
1806
3369
|
}
|
|
1807
3370
|
}
|
|
1808
|
-
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, session);
|
|
3371
|
+
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, copyVaultSession(session));
|
|
3372
|
+
applyCredentialResponseCachePolicy(res);
|
|
1809
3373
|
if (redirect && upstreamLogoutUrl) {
|
|
1810
3374
|
res.redirect(302, upstreamLogoutUrl);
|
|
1811
3375
|
return;
|
|
@@ -1863,11 +3427,18 @@ var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("ba
|
|
|
1863
3427
|
});
|
|
1864
3428
|
});
|
|
1865
3429
|
function registerRoutes(router, options, config, trustedOrigins, backendOrigin, basePath) {
|
|
3430
|
+
const proofVerifier = createVaultRouteProofVerifier(options, config, backendOrigin, basePath);
|
|
1866
3431
|
router.get(OIDC_VAULT_ROUTE_PATHS.login, createLoginHandler(options, config, backendOrigin, basePath));
|
|
3432
|
+
if (proofVerifier !== void 0 || options.fingerprintRecognition !== void 0) {
|
|
3433
|
+
router.post(
|
|
3434
|
+
OIDC_VAULT_ROUTE_PATHS.login,
|
|
3435
|
+
createLoginInitiationHandler(options, config, trustedOrigins, backendOrigin, basePath, proofVerifier)
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
1867
3438
|
router.get(OIDC_VAULT_ROUTE_PATHS.callback, createCallbackHandler(options, config, backendOrigin, basePath));
|
|
1868
|
-
router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options, config));
|
|
1869
|
-
router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins));
|
|
1870
|
-
router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins));
|
|
3439
|
+
router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options, config, trustedOrigins, proofVerifier));
|
|
3440
|
+
router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins, proofVerifier));
|
|
3441
|
+
router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins, proofVerifier));
|
|
1871
3442
|
router.post(OIDC_VAULT_ROUTE_PATHS["backchannel-logout"], createBackchannelLogoutHandler(options, config));
|
|
1872
3443
|
}
|
|
1873
3444
|
function createOidcVaultMiddleware(options) {
|
|
@@ -1880,6 +3451,15 @@ function createOidcVaultMiddleware(options) {
|
|
|
1880
3451
|
applyCredentialResponseCachePolicy(res);
|
|
1881
3452
|
next();
|
|
1882
3453
|
});
|
|
3454
|
+
if (resolvedOptions.deviceBinding !== void 0 || resolvedOptions.fingerprintRecognition !== void 0) {
|
|
3455
|
+
baseRouter.post(
|
|
3456
|
+
OIDC_VAULT_ROUTE_PATHS.login,
|
|
3457
|
+
createAsyncHandler("login", resolvedOptions, async (req, res, next) => {
|
|
3458
|
+
assertLoginInitiationRequest(req, resolvedOptions, trustedOrigins);
|
|
3459
|
+
next();
|
|
3460
|
+
})
|
|
3461
|
+
);
|
|
3462
|
+
}
|
|
1883
3463
|
baseRouter.use(express.json({ limit: requestBodyLimit }));
|
|
1884
3464
|
baseRouter.use(
|
|
1885
3465
|
express.urlencoded({
|
|
@@ -1901,6 +3481,7 @@ export {
|
|
|
1901
3481
|
DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT,
|
|
1902
3482
|
OIDC_VAULT_ROUTE_PATHS,
|
|
1903
3483
|
OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT,
|
|
3484
|
+
OidcVaultDpopReplayCapacityError,
|
|
1904
3485
|
OidcVaultStoreConflictError,
|
|
1905
3486
|
createOidcVaultAccessTokenMiddleware,
|
|
1906
3487
|
createOidcVaultJwtAccessTokenValidator,
|