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