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