@web-ts-toolkit/express-oidc-vault 0.28.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/LICENSE +201 -0
- package/README.md +892 -0
- package/index.d.mts +258 -0
- package/index.d.ts +258 -0
- package/index.js +1001 -0
- package/index.mjs +954 -0
- package/package.json +48 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createHash, randomBytes } from "crypto";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
var DEFAULT_OIDC_SCOPES = "openid email profile";
|
|
8
|
+
var MANUAL_CONFIG_REQUIREMENTS = ["authorizationEndpoint", "tokenEndpoint", "jwksUri"];
|
|
9
|
+
var normalizeOptionalString = (value) => {
|
|
10
|
+
const trimmed = value?.trim();
|
|
11
|
+
return trimmed ? trimmed : void 0;
|
|
12
|
+
};
|
|
13
|
+
var withOptionalValue = (target, key, value) => {
|
|
14
|
+
if (value !== void 0) {
|
|
15
|
+
target[key] = value;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function resolveOidcVaultConfig(config = {}) {
|
|
19
|
+
const issuer = normalizeOptionalString(config.issuer);
|
|
20
|
+
const clientId = normalizeOptionalString(config.clientId);
|
|
21
|
+
const clientSecret = normalizeOptionalString(config.clientSecret);
|
|
22
|
+
const scopes = normalizeOptionalString(config.scopes) ?? DEFAULT_OIDC_SCOPES;
|
|
23
|
+
if (!clientId) {
|
|
24
|
+
throw new Error("OIDC clientId is required.");
|
|
25
|
+
}
|
|
26
|
+
if (issuer) {
|
|
27
|
+
const resolved2 = {
|
|
28
|
+
mode: "issuer",
|
|
29
|
+
issuer,
|
|
30
|
+
scopes
|
|
31
|
+
};
|
|
32
|
+
withOptionalValue(resolved2, "clientId", clientId);
|
|
33
|
+
withOptionalValue(resolved2, "clientSecret", clientSecret);
|
|
34
|
+
return resolved2;
|
|
35
|
+
}
|
|
36
|
+
const resolved = {
|
|
37
|
+
mode: "manual",
|
|
38
|
+
scopes
|
|
39
|
+
};
|
|
40
|
+
withOptionalValue(resolved, "authorizationEndpoint", normalizeOptionalString(config.authorizationEndpoint));
|
|
41
|
+
withOptionalValue(resolved, "tokenEndpoint", normalizeOptionalString(config.tokenEndpoint));
|
|
42
|
+
withOptionalValue(resolved, "userInfoEndpoint", normalizeOptionalString(config.userInfoEndpoint));
|
|
43
|
+
withOptionalValue(resolved, "jwksUri", normalizeOptionalString(config.jwksUri));
|
|
44
|
+
withOptionalValue(resolved, "endSessionEndpoint", normalizeOptionalString(config.endSessionEndpoint));
|
|
45
|
+
withOptionalValue(resolved, "clientId", clientId);
|
|
46
|
+
withOptionalValue(resolved, "clientSecret", clientSecret);
|
|
47
|
+
const missing = MANUAL_CONFIG_REQUIREMENTS.filter((key) => !resolved[key]);
|
|
48
|
+
if (missing.length > 0) {
|
|
49
|
+
throw new Error(`Missing required OIDC configuration for manual mode: ${missing.join(", ")}`);
|
|
50
|
+
}
|
|
51
|
+
return resolved;
|
|
52
|
+
}
|
|
53
|
+
function resolveOidcVaultConfigFromEnv(env = process.env) {
|
|
54
|
+
return resolveOidcVaultConfig({
|
|
55
|
+
issuer: env.OIDC_ISSUER,
|
|
56
|
+
authorizationEndpoint: env.OIDC_AUTHORIZATION_ENDPOINT,
|
|
57
|
+
tokenEndpoint: env.OIDC_TOKEN_ENDPOINT,
|
|
58
|
+
userInfoEndpoint: env.OIDC_USERINFO_ENDPOINT,
|
|
59
|
+
jwksUri: env.OIDC_JWKS_URI,
|
|
60
|
+
endSessionEndpoint: env.OIDC_END_SESSION_ENDPOINT,
|
|
61
|
+
clientId: env.OIDC_CLIENT_ID,
|
|
62
|
+
clientSecret: env.OIDC_CLIENT_SECRET,
|
|
63
|
+
scopes: env.OIDC_SCOPES
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/types.ts
|
|
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
|
|
76
|
+
var DEFAULT_OIDC_VAULT_BASE_PATH = "/auth/oidc";
|
|
77
|
+
var DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
|
|
78
|
+
var DEFAULT_EXCHANGE_CODE_TTL_MS = 30 * 1e3;
|
|
79
|
+
var DEFAULT_SESSION_COOKIE_NAME = "oidc_vault_session";
|
|
80
|
+
var OIDC_VAULT_ROUTE_PATHS = {
|
|
81
|
+
login: "/login",
|
|
82
|
+
callback: "/callback",
|
|
83
|
+
exchange: "/exchange",
|
|
84
|
+
refresh: "/refresh",
|
|
85
|
+
logout: "/logout",
|
|
86
|
+
"backchannel-logout": "/backchannel-logout"
|
|
87
|
+
};
|
|
88
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
89
|
+
var jwksCache = /* @__PURE__ */ new Map();
|
|
90
|
+
var OidcVaultHttpError = class extends Error {
|
|
91
|
+
constructor(status, code, message) {
|
|
92
|
+
super(message);
|
|
93
|
+
this.status = status;
|
|
94
|
+
this.code = code;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
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
|
+
var toErrorPayload = (error) => {
|
|
112
|
+
if (error instanceof OidcVaultHttpError) {
|
|
113
|
+
return {
|
|
114
|
+
status: error.status,
|
|
115
|
+
code: error.code,
|
|
116
|
+
message: error.message
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
status: 500,
|
|
121
|
+
code: "OIDC_VAULT_INTERNAL_ERROR",
|
|
122
|
+
message: error instanceof Error ? error.message : "Unexpected OIDC vault error."
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
var getRequestOrigin = (req) => `${req.protocol}://${req.get("host") ?? "localhost"}`;
|
|
126
|
+
var getCallbackUri = (req, basePath) => `${getRequestOrigin(req)}${basePath}${OIDC_VAULT_ROUTE_PATHS.callback}`;
|
|
127
|
+
var getRequiredString = (value, message, code, status = 400) => {
|
|
128
|
+
if (!isString(value)) {
|
|
129
|
+
throw new OidcVaultHttpError(status, code, message);
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
};
|
|
133
|
+
var parseCookieHeader = (headerValue) => {
|
|
134
|
+
if (!headerValue) {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
return headerValue.split(";").reduce((cookies, part) => {
|
|
138
|
+
const separatorIndex = part.indexOf("=");
|
|
139
|
+
if (separatorIndex === -1) {
|
|
140
|
+
return cookies;
|
|
141
|
+
}
|
|
142
|
+
const name = part.slice(0, separatorIndex).trim();
|
|
143
|
+
const value = part.slice(separatorIndex + 1).trim();
|
|
144
|
+
if (!name) {
|
|
145
|
+
return cookies;
|
|
146
|
+
}
|
|
147
|
+
cookies[name] = decodeURIComponent(value);
|
|
148
|
+
return cookies;
|
|
149
|
+
}, {});
|
|
150
|
+
};
|
|
151
|
+
var resolveCookieOptions = (options) => {
|
|
152
|
+
const cookieOptions = options.cookie ?? {};
|
|
153
|
+
const deploymentMode = cookieOptions.deploymentMode ?? "same-origin";
|
|
154
|
+
const sameSite = cookieOptions.sameSite ?? (deploymentMode === "cross-site" ? "none" : "lax");
|
|
155
|
+
const secure = cookieOptions.secure ?? (sameSite === "none" || deploymentMode === "cross-site");
|
|
156
|
+
return {
|
|
157
|
+
name: cookieOptions.name ?? DEFAULT_SESSION_COOKIE_NAME,
|
|
158
|
+
sameSite,
|
|
159
|
+
secure,
|
|
160
|
+
domain: cookieOptions.domain,
|
|
161
|
+
path: cookieOptions.path ?? "/",
|
|
162
|
+
httpOnly: cookieOptions.httpOnly ?? true
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
var usesCrossSiteCookieTransport = (options) => usesCookieTransport(options) && resolveCookieOptions(options).sameSite === "none";
|
|
166
|
+
var resolveTrustedOrigins = (options) => new Set((options.trustedOrigins ?? []).map((value) => new URL(value).origin));
|
|
167
|
+
var getRequestSourceOrigin = (req) => {
|
|
168
|
+
const origin = req.get("origin");
|
|
169
|
+
if (isString(origin)) {
|
|
170
|
+
return origin;
|
|
171
|
+
}
|
|
172
|
+
const referer = req.get("referer");
|
|
173
|
+
if (!isString(referer)) {
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
return new URL(referer).origin;
|
|
178
|
+
} catch {
|
|
179
|
+
return void 0;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var assertTrustedOrigin = (req, options, trustedOrigins, action) => {
|
|
183
|
+
if (!usesCrossSiteCookieTransport(options)) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const origin = getRequestSourceOrigin(req);
|
|
187
|
+
if (!origin || !trustedOrigins.has(origin)) {
|
|
188
|
+
throw new OidcVaultHttpError(
|
|
189
|
+
403,
|
|
190
|
+
"OIDC_VAULT_UNTRUSTED_ORIGIN",
|
|
191
|
+
`${action === "refresh" ? "Refresh" : "Logout"} request origin is not trusted.`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var validateOidcVaultOptions = (options) => {
|
|
196
|
+
const config = resolveOidcVaultConfig(options.config);
|
|
197
|
+
const trustedOrigins = resolveTrustedOrigins(options);
|
|
198
|
+
if (usesCrossSiteCookieTransport(options) && trustedOrigins.size === 0) {
|
|
199
|
+
throw new Error("trustedOrigins is required when using cross-site cookie transport.");
|
|
200
|
+
}
|
|
201
|
+
return { config, trustedOrigins };
|
|
202
|
+
};
|
|
203
|
+
var serializeCookie = (name, value, options, overrides) => {
|
|
204
|
+
const sameSite = options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1);
|
|
205
|
+
const parts = [`${name}=${encodeURIComponent(value)}`, `Path=${options.path}`, `SameSite=${sameSite}`];
|
|
206
|
+
if (options.httpOnly) {
|
|
207
|
+
parts.push("HttpOnly");
|
|
208
|
+
}
|
|
209
|
+
if (options.secure || options.sameSite === "none") {
|
|
210
|
+
parts.push("Secure");
|
|
211
|
+
}
|
|
212
|
+
if (options.domain) {
|
|
213
|
+
parts.push(`Domain=${options.domain}`);
|
|
214
|
+
}
|
|
215
|
+
if (typeof overrides?.maxAge === "number") {
|
|
216
|
+
parts.push(`Max-Age=${overrides.maxAge}`);
|
|
217
|
+
}
|
|
218
|
+
if (overrides?.expires) {
|
|
219
|
+
parts.push(`Expires=${overrides.expires.toUTCString()}`);
|
|
220
|
+
}
|
|
221
|
+
return parts.join("; ");
|
|
222
|
+
};
|
|
223
|
+
var setSessionCookie = (res, options, sessionId) => {
|
|
224
|
+
const cookieOptions = resolveCookieOptions(options);
|
|
225
|
+
res.append("Set-Cookie", serializeCookie(cookieOptions.name, sessionId, cookieOptions));
|
|
226
|
+
};
|
|
227
|
+
var clearSessionCookie = (res, options) => {
|
|
228
|
+
const cookieOptions = resolveCookieOptions(options);
|
|
229
|
+
res.append(
|
|
230
|
+
"Set-Cookie",
|
|
231
|
+
serializeCookie(cookieOptions.name, "", cookieOptions, {
|
|
232
|
+
maxAge: 0,
|
|
233
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
var getSessionIdFromCookie = (req, options) => {
|
|
238
|
+
const cookies = parseCookieHeader(req.headers.cookie);
|
|
239
|
+
return cookies[resolveCookieOptions(options).name];
|
|
240
|
+
};
|
|
241
|
+
var getSessionIdFromRequest = (req, options, action) => {
|
|
242
|
+
const body = getBody(req);
|
|
243
|
+
if (usesCookieTransport(options)) {
|
|
244
|
+
const cookieSessionId = getSessionIdFromCookie(req, options);
|
|
245
|
+
if (isString(cookieSessionId)) {
|
|
246
|
+
return cookieSessionId;
|
|
247
|
+
}
|
|
248
|
+
if (isString(body.sessionId)) {
|
|
249
|
+
return body.sessionId;
|
|
250
|
+
}
|
|
251
|
+
throw new OidcVaultHttpError(
|
|
252
|
+
400,
|
|
253
|
+
"OIDC_VAULT_MISSING_SESSION_ID",
|
|
254
|
+
`${action === "refresh" ? "Refresh" : "Logout"} request is missing the session cookie.`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return getRequiredString(
|
|
258
|
+
body.sessionId,
|
|
259
|
+
`${action === "refresh" ? "Refresh" : "Logout"} request is missing sessionId.`,
|
|
260
|
+
"OIDC_VAULT_MISSING_SESSION_ID"
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
var createExchangeResponse = (options, session, issuedToken) => ({
|
|
264
|
+
sessionId: usesCookieTransport(options) ? void 0 : session.sessionId,
|
|
265
|
+
user: session.user,
|
|
266
|
+
...issuedToken
|
|
267
|
+
});
|
|
268
|
+
var extractBearerToken = (authorizationHeader) => {
|
|
269
|
+
if (!authorizationHeader) {
|
|
270
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_MISSING_BEARER_TOKEN", "Missing bearer token.");
|
|
271
|
+
}
|
|
272
|
+
const [scheme, token, extra] = authorizationHeader.trim().split(/\s+/);
|
|
273
|
+
if (scheme?.toLowerCase() !== "bearer" || !token || extra) {
|
|
274
|
+
throw new OidcVaultHttpError(
|
|
275
|
+
401,
|
|
276
|
+
"OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
|
|
277
|
+
"Authorization header must use the Bearer scheme."
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return token;
|
|
281
|
+
};
|
|
282
|
+
var setBearerChallengeHeader = (res) => {
|
|
283
|
+
res.setHeader("WWW-Authenticate", "Bearer");
|
|
284
|
+
};
|
|
285
|
+
var BACKCHANNEL_LOGOUT_EVENT_CLAIM = "http://schemas.openid.net/event/backchannel-logout";
|
|
286
|
+
var defaultJwtClaimsMapper = (claims) => {
|
|
287
|
+
const subject = getRequiredString(
|
|
288
|
+
claims.sub,
|
|
289
|
+
"JWT access token is missing sub.",
|
|
290
|
+
"OIDC_VAULT_INVALID_ACCESS_TOKEN",
|
|
291
|
+
401
|
|
292
|
+
);
|
|
293
|
+
return {
|
|
294
|
+
subject,
|
|
295
|
+
sessionId: typeof claims.sid === "string" ? claims.sid : void 0,
|
|
296
|
+
scope: typeof claims.scope === "string" ? claims.scope : void 0,
|
|
297
|
+
claims
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
var buildWellKnownUrl = (issuer) => {
|
|
301
|
+
const normalizedIssuer = issuer.endsWith("/") ? issuer : `${issuer}/`;
|
|
302
|
+
return new URL(".well-known/openid-configuration", normalizedIssuer);
|
|
303
|
+
};
|
|
304
|
+
var resolveJwks = (jwksUri) => {
|
|
305
|
+
let jwks = jwksCache.get(jwksUri);
|
|
306
|
+
if (!jwks) {
|
|
307
|
+
jwks = createRemoteJWKSet(new URL(jwksUri));
|
|
308
|
+
jwksCache.set(jwksUri, jwks);
|
|
309
|
+
}
|
|
310
|
+
return jwks;
|
|
311
|
+
};
|
|
312
|
+
var appendQueryParam = (url, name, value) => {
|
|
313
|
+
if (value) {
|
|
314
|
+
url.searchParams.set(name, value);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var buildAuthorizationUrl = (metadata, transaction, redirectUri) => {
|
|
318
|
+
const url = new URL(metadata.authorizationEndpoint);
|
|
319
|
+
appendQueryParam(url, "response_type", "code");
|
|
320
|
+
appendQueryParam(url, "client_id", metadata.clientId);
|
|
321
|
+
appendQueryParam(url, "redirect_uri", redirectUri);
|
|
322
|
+
appendQueryParam(url, "scope", metadata.scopes);
|
|
323
|
+
appendQueryParam(url, "state", transaction.state);
|
|
324
|
+
appendQueryParam(url, "nonce", transaction.nonce);
|
|
325
|
+
appendQueryParam(url, "code_challenge", transaction.codeChallenge);
|
|
326
|
+
appendQueryParam(url, "code_challenge_method", "S256");
|
|
327
|
+
return url.toString();
|
|
328
|
+
};
|
|
329
|
+
var buildLogoutUrl = (endSessionEndpoint, idTokenHint, postLogoutRedirectUri) => {
|
|
330
|
+
const url = new URL(endSessionEndpoint);
|
|
331
|
+
appendQueryParam(url, "id_token_hint", idTokenHint);
|
|
332
|
+
appendQueryParam(url, "post_logout_redirect_uri", postLogoutRedirectUri);
|
|
333
|
+
return url.toString();
|
|
334
|
+
};
|
|
335
|
+
var toBasicAuthorization = (clientId, clientSecret) => `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
|
|
336
|
+
var readJsonResponse = async (response, errorCode) => {
|
|
337
|
+
const text = await response.text();
|
|
338
|
+
if (!text) {
|
|
339
|
+
return {};
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
return JSON.parse(text);
|
|
343
|
+
} catch {
|
|
344
|
+
throw new OidcVaultHttpError(response.status, errorCode, text);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
var getLogoutTokenFromRequest = (req) => {
|
|
348
|
+
const body = getBody(req);
|
|
349
|
+
return getRequiredString(
|
|
350
|
+
body.logout_token,
|
|
351
|
+
"Backchannel logout request is missing logout_token.",
|
|
352
|
+
"OIDC_VAULT_MISSING_LOGOUT_TOKEN"
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
var createHookContext = (route, req, res, session, metadata) => ({ route, req, res, session, metadata });
|
|
356
|
+
var mergeUserProfile = (sub, idTokenClaims, userInfo) => ({
|
|
357
|
+
...idTokenClaims,
|
|
358
|
+
...userInfo ?? {},
|
|
359
|
+
sub
|
|
360
|
+
});
|
|
361
|
+
var resolveReturnTo = (req, options) => {
|
|
362
|
+
const rawReturnTo = req.query.returnTo;
|
|
363
|
+
if (!isString(rawReturnTo)) {
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
const configuredFrontendUri = options.frontendRedirectUri;
|
|
367
|
+
if (!configuredFrontendUri) {
|
|
368
|
+
throw new OidcVaultHttpError(
|
|
369
|
+
400,
|
|
370
|
+
"OIDC_VAULT_INVALID_RETURN_TO",
|
|
371
|
+
"Custom returnTo requires a configured frontendRedirectUri."
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const configuredUrl = new URL(configuredFrontendUri);
|
|
375
|
+
const resolvedUrl = rawReturnTo.startsWith("/") ? new URL(rawReturnTo, configuredUrl) : new URL(rawReturnTo);
|
|
376
|
+
if (resolvedUrl.origin !== configuredUrl.origin) {
|
|
377
|
+
throw new OidcVaultHttpError(
|
|
378
|
+
400,
|
|
379
|
+
"OIDC_VAULT_INVALID_RETURN_TO",
|
|
380
|
+
"returnTo must stay on the configured frontend origin."
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
return resolvedUrl.toString();
|
|
384
|
+
};
|
|
385
|
+
var appendCodeToRedirectUri = (redirectUri, code) => {
|
|
386
|
+
const url = new URL(redirectUri);
|
|
387
|
+
url.searchParams.set("code", code);
|
|
388
|
+
return url.toString();
|
|
389
|
+
};
|
|
390
|
+
var getBody = (req) => isRecord(req.body) ? req.body : {};
|
|
391
|
+
var withIssuedToken = async (req, res, options, session) => {
|
|
392
|
+
if (!options.tokenIssuer) {
|
|
393
|
+
return {};
|
|
394
|
+
}
|
|
395
|
+
return options.tokenIssuer.issue({ req, res, session });
|
|
396
|
+
};
|
|
397
|
+
var resolveFrontendRedirectUri = (transaction, options) => {
|
|
398
|
+
const redirectUri = transaction.returnTo ?? options.frontendRedirectUri;
|
|
399
|
+
if (!redirectUri) {
|
|
400
|
+
throw new OidcVaultHttpError(
|
|
401
|
+
500,
|
|
402
|
+
"OIDC_VAULT_MISSING_FRONTEND_REDIRECT_URI",
|
|
403
|
+
"A frontendRedirectUri or login returnTo is required to complete the callback flow."
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
return redirectUri;
|
|
407
|
+
};
|
|
408
|
+
var shouldFetchUserInfo = (options, metadata) => options.fetchUserInfo !== false && Boolean(metadata.userInfoEndpoint);
|
|
409
|
+
async function callHook(route, hook, req, res, session, metadata) {
|
|
410
|
+
if (!hook) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
await hook(createHookContext(route, req, res, session, metadata));
|
|
414
|
+
}
|
|
415
|
+
async function handleRouteError(route, req, res, next, options, error) {
|
|
416
|
+
try {
|
|
417
|
+
await options.hooks?.onError?.({
|
|
418
|
+
...createHookContext(route, req, res),
|
|
419
|
+
error
|
|
420
|
+
});
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
if (res.headersSent) {
|
|
424
|
+
next(error);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const payload = toErrorPayload(error);
|
|
428
|
+
res.status(payload.status).json({
|
|
429
|
+
code: payload.code,
|
|
430
|
+
message: payload.message
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function createAsyncHandler(route, options, handler) {
|
|
434
|
+
return async (req, res, next) => {
|
|
435
|
+
try {
|
|
436
|
+
await handler(req, res, next);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
await handleRouteError(route, req, res, next, options, error);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
async function discoverIssuerMetadata(issuer) {
|
|
443
|
+
let discoveryPromise = discoveryCache.get(issuer);
|
|
444
|
+
if (!discoveryPromise) {
|
|
445
|
+
discoveryPromise = (async () => {
|
|
446
|
+
const discoveryUrl = buildWellKnownUrl(issuer);
|
|
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);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const headers = {
|
|
535
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
536
|
+
};
|
|
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
|
+
}
|
|
642
|
+
var createLoginHandler = (options, config, basePath) => createAsyncHandler("login", options, async (req, res) => {
|
|
643
|
+
const metadata = await resolveProviderMetadata(config);
|
|
644
|
+
await callHook("login", options.hooks?.onLoginStart, req, res, void 0, { provider: metadata });
|
|
645
|
+
const now = getNow(options);
|
|
646
|
+
const pkceVerifier = createPkceVerifier();
|
|
647
|
+
const transaction = {
|
|
648
|
+
state: createOpaqueId("state"),
|
|
649
|
+
nonce: createOpaqueId("nonce"),
|
|
650
|
+
pkceVerifier,
|
|
651
|
+
codeChallenge: createPkceChallenge(pkceVerifier),
|
|
652
|
+
returnTo: resolveReturnTo(req, options),
|
|
653
|
+
createdAt: now,
|
|
654
|
+
expiresAt: now + (options.authorizationTransactionTtlMs ?? DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS)
|
|
655
|
+
};
|
|
656
|
+
await options.storeProvider.createAuthorizationTransaction(transaction);
|
|
657
|
+
const authorizationUrl = buildAuthorizationUrl(metadata, transaction, getCallbackUri(req, basePath));
|
|
658
|
+
await callHook("login", options.hooks?.onAuthorizationUrl, req, res, void 0, {
|
|
659
|
+
authorizationUrl,
|
|
660
|
+
state: transaction.state
|
|
661
|
+
});
|
|
662
|
+
res.redirect(302, authorizationUrl);
|
|
663
|
+
});
|
|
664
|
+
var createCallbackHandler = (options, config, basePath) => createAsyncHandler("callback", options, async (req, res) => {
|
|
665
|
+
if (isString(req.query.error)) {
|
|
666
|
+
throw new OidcVaultHttpError(400, "OIDC_VAULT_CALLBACK_ERROR", req.query.error);
|
|
667
|
+
}
|
|
668
|
+
const code = getRequiredString(
|
|
669
|
+
req.query.code,
|
|
670
|
+
"OIDC callback is missing the authorization code.",
|
|
671
|
+
"OIDC_VAULT_MISSING_CODE"
|
|
672
|
+
);
|
|
673
|
+
const state = getRequiredString(req.query.state, "OIDC callback is missing state.", "OIDC_VAULT_MISSING_STATE");
|
|
674
|
+
const transaction = await options.storeProvider.consumeAuthorizationTransaction(state);
|
|
675
|
+
if (!transaction) {
|
|
676
|
+
throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_STATE", "OIDC state is invalid or expired.");
|
|
677
|
+
}
|
|
678
|
+
const metadata = await resolveProviderMetadata(config);
|
|
679
|
+
const tokenResponse = await requestToken(metadata, {
|
|
680
|
+
grant_type: "authorization_code",
|
|
681
|
+
code,
|
|
682
|
+
code_verifier: transaction.pkceVerifier,
|
|
683
|
+
redirect_uri: getCallbackUri(req, basePath)
|
|
684
|
+
});
|
|
685
|
+
if (!isString(tokenResponse.id_token)) {
|
|
686
|
+
throw new OidcVaultHttpError(502, "OIDC_VAULT_MISSING_ID_TOKEN", "OIDC token response is missing id_token.");
|
|
687
|
+
}
|
|
688
|
+
if (!isString(tokenResponse.refresh_token)) {
|
|
689
|
+
throw new OidcVaultHttpError(
|
|
690
|
+
502,
|
|
691
|
+
"OIDC_VAULT_MISSING_REFRESH_TOKEN",
|
|
692
|
+
"OIDC token response is missing refresh_token."
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
await callHook("callback", options.hooks?.onCallbackTokens, req, res, void 0, {
|
|
696
|
+
hasAccessToken: Boolean(tokenResponse.access_token),
|
|
697
|
+
hasRefreshToken: true,
|
|
698
|
+
hasIdToken: true
|
|
699
|
+
});
|
|
700
|
+
const claims = await verifyIdToken(metadata, tokenResponse.id_token, transaction.nonce);
|
|
701
|
+
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;
|
|
703
|
+
if (userInfo) {
|
|
704
|
+
await callHook("callback", options.hooks?.onUserInfo, req, res, void 0, { subject });
|
|
705
|
+
}
|
|
706
|
+
const now = getNow(options);
|
|
707
|
+
const sessionId = createOpaqueId("sess");
|
|
708
|
+
const session = {
|
|
709
|
+
sessionId,
|
|
710
|
+
subject,
|
|
711
|
+
providerSessionId: typeof claims.sid === "string" ? claims.sid : void 0,
|
|
712
|
+
provider: {
|
|
713
|
+
issuer: metadata.issuer ?? (typeof claims.iss === "string" ? claims.iss : void 0),
|
|
714
|
+
clientId: metadata.clientId
|
|
715
|
+
},
|
|
716
|
+
refreshToken: tokenResponse.refresh_token,
|
|
717
|
+
idToken: tokenResponse.id_token,
|
|
718
|
+
accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : void 0,
|
|
719
|
+
scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : metadata.scopes,
|
|
720
|
+
expiresAt: typeof tokenResponse.expires_in === "number" ? now + tokenResponse.expires_in * 1e3 : void 0,
|
|
721
|
+
createdAt: now,
|
|
722
|
+
updatedAt: now,
|
|
723
|
+
user: mergeUserProfile(subject, claims, userInfo),
|
|
724
|
+
metadata: {
|
|
725
|
+
tokenType: tokenResponse.token_type
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
await callHook("callback", options.hooks?.onBeforeSessionCreate, req, res, session, { subject });
|
|
729
|
+
const createdSession = await options.storeProvider.createSession(session);
|
|
730
|
+
await callHook("callback", options.hooks?.onSessionCreated, req, res, createdSession, { subject });
|
|
731
|
+
const exchangeCode = createOpaqueId("code");
|
|
732
|
+
await options.storeProvider.createExchangeCode({
|
|
733
|
+
code: exchangeCode,
|
|
734
|
+
sessionId: createdSession.sessionId,
|
|
735
|
+
returnTo: transaction.returnTo,
|
|
736
|
+
createdAt: now,
|
|
737
|
+
expiresAt: now + (options.exchangeCodeTtlMs ?? DEFAULT_EXCHANGE_CODE_TTL_MS)
|
|
738
|
+
});
|
|
739
|
+
res.redirect(302, appendCodeToRedirectUri(resolveFrontendRedirectUri(transaction, options), exchangeCode));
|
|
740
|
+
});
|
|
741
|
+
var createExchangeHandler = (options) => createAsyncHandler("exchange", options, async (req, res) => {
|
|
742
|
+
const body = getBody(req);
|
|
743
|
+
const code = getRequiredString(body.code, "Exchange request is missing code.", "OIDC_VAULT_MISSING_EXCHANGE_CODE");
|
|
744
|
+
const exchangeRecord = await options.storeProvider.consumeExchangeCode(code);
|
|
745
|
+
if (!exchangeRecord) {
|
|
746
|
+
throw new OidcVaultHttpError(400, "OIDC_VAULT_INVALID_EXCHANGE_CODE", "Exchange code is invalid or expired.");
|
|
747
|
+
}
|
|
748
|
+
const session = await options.storeProvider.getSession(exchangeRecord.sessionId);
|
|
749
|
+
if (!session) {
|
|
750
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
751
|
+
}
|
|
752
|
+
const issuedToken = await withIssuedToken(req, res, options, session);
|
|
753
|
+
if (usesCookieTransport(options)) {
|
|
754
|
+
setSessionCookie(res, options, session.sessionId);
|
|
755
|
+
}
|
|
756
|
+
const response = createExchangeResponse(options, session, issuedToken);
|
|
757
|
+
res.status(200).json(response);
|
|
758
|
+
});
|
|
759
|
+
var createRefreshHandler = (options, config, trustedOrigins) => createAsyncHandler("refresh", options, async (req, res) => {
|
|
760
|
+
assertTrustedOrigin(req, options, trustedOrigins, "refresh");
|
|
761
|
+
const sessionId = getSessionIdFromRequest(req, options, "refresh");
|
|
762
|
+
const currentSession = await options.storeProvider.getSession(sessionId);
|
|
763
|
+
if (!currentSession) {
|
|
764
|
+
if (usesCookieTransport(options)) {
|
|
765
|
+
clearSessionCookie(res, options);
|
|
766
|
+
}
|
|
767
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
768
|
+
}
|
|
769
|
+
const metadata = await resolveProviderMetadata(config);
|
|
770
|
+
const tokenResponse = await requestToken(metadata, {
|
|
771
|
+
grant_type: "refresh_token",
|
|
772
|
+
refresh_token: currentSession.refreshToken
|
|
773
|
+
});
|
|
774
|
+
const idToken = isString(tokenResponse.id_token) ? tokenResponse.id_token : currentSession.idToken;
|
|
775
|
+
const claims = await verifyIdToken(metadata, idToken);
|
|
776
|
+
const subject = getRequiredString(claims.sub, "OIDC id_token is missing sub.", "OIDC_VAULT_INVALID_ID_TOKEN", 502);
|
|
777
|
+
const userInfo = shouldFetchUserInfo(options, metadata) && isString(tokenResponse.access_token) ? await fetchUserInfo(metadata, tokenResponse.access_token) : void 0;
|
|
778
|
+
const now = getNow(options);
|
|
779
|
+
const nextSession = {
|
|
780
|
+
...currentSession,
|
|
781
|
+
sessionId: createOpaqueId("sess"),
|
|
782
|
+
subject,
|
|
783
|
+
providerSessionId: typeof claims.sid === "string" ? claims.sid : currentSession.providerSessionId,
|
|
784
|
+
refreshToken: isString(tokenResponse.refresh_token) ? tokenResponse.refresh_token : currentSession.refreshToken,
|
|
785
|
+
idToken,
|
|
786
|
+
accessToken: isString(tokenResponse.access_token) ? tokenResponse.access_token : currentSession.accessToken,
|
|
787
|
+
scope: typeof tokenResponse.scope === "string" ? tokenResponse.scope : currentSession.scope,
|
|
788
|
+
expiresAt: typeof tokenResponse.expires_in === "number" ? now + tokenResponse.expires_in * 1e3 : currentSession.expiresAt,
|
|
789
|
+
updatedAt: now,
|
|
790
|
+
user: mergeUserProfile(subject, claims, userInfo ?? currentSession.user)
|
|
791
|
+
};
|
|
792
|
+
const issuedToken = await withIssuedToken(req, res, options, nextSession);
|
|
793
|
+
let rotatedSession;
|
|
794
|
+
try {
|
|
795
|
+
rotatedSession = await options.storeProvider.rotateSession({
|
|
796
|
+
sessionId: currentSession.sessionId,
|
|
797
|
+
nextSession
|
|
798
|
+
});
|
|
799
|
+
} catch (error) {
|
|
800
|
+
if (error instanceof OidcVaultStoreConflictError) {
|
|
801
|
+
if (usesCookieTransport(options)) {
|
|
802
|
+
clearSessionCookie(res, options);
|
|
803
|
+
}
|
|
804
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
805
|
+
}
|
|
806
|
+
throw error;
|
|
807
|
+
}
|
|
808
|
+
await callHook("refresh", options.hooks?.onSessionRefreshed, req, res, rotatedSession, {
|
|
809
|
+
previousSessionId: currentSession.sessionId
|
|
810
|
+
});
|
|
811
|
+
if (usesCookieTransport(options)) {
|
|
812
|
+
setSessionCookie(res, options, rotatedSession.sessionId);
|
|
813
|
+
}
|
|
814
|
+
const response = createExchangeResponse(options, rotatedSession, issuedToken);
|
|
815
|
+
res.status(200).json(response);
|
|
816
|
+
});
|
|
817
|
+
var createLogoutHandler = (options, config, trustedOrigins) => createAsyncHandler("logout", options, async (req, res) => {
|
|
818
|
+
assertTrustedOrigin(req, options, trustedOrigins, "logout");
|
|
819
|
+
const body = getBody(req);
|
|
820
|
+
const sessionId = getSessionIdFromRequest(req, options, "logout");
|
|
821
|
+
const redirect = body.redirect === true;
|
|
822
|
+
const session = await options.storeProvider.getSession(sessionId);
|
|
823
|
+
if (!session) {
|
|
824
|
+
if (usesCookieTransport(options)) {
|
|
825
|
+
clearSessionCookie(res, options);
|
|
826
|
+
}
|
|
827
|
+
res.status(200).json({ loggedOut: true });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
await callHook("logout", options.hooks?.onBeforeLogout, req, res, session, void 0);
|
|
831
|
+
await options.storeProvider.deleteSession(sessionId);
|
|
832
|
+
if (usesCookieTransport(options)) {
|
|
833
|
+
clearSessionCookie(res, options);
|
|
834
|
+
}
|
|
835
|
+
const metadata = await resolveProviderMetadata(config);
|
|
836
|
+
const upstreamLogoutUrl = metadata.endSessionEndpoint ? buildLogoutUrl(metadata.endSessionEndpoint, session.idToken, options.postLogoutRedirectUri) : void 0;
|
|
837
|
+
await callHook("logout", options.hooks?.onLogout, req, res, session, { upstreamLogoutUrl });
|
|
838
|
+
if (redirect && upstreamLogoutUrl) {
|
|
839
|
+
res.redirect(302, upstreamLogoutUrl);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
res.status(200).json({
|
|
843
|
+
loggedOut: true,
|
|
844
|
+
upstreamLogoutUrl
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("backchannel-logout", options, async (req, res) => {
|
|
848
|
+
const metadata = await resolveProviderMetadata(config);
|
|
849
|
+
const logoutToken = getLogoutTokenFromRequest(req);
|
|
850
|
+
const claims = await verifyBackchannelLogoutToken(metadata, logoutToken);
|
|
851
|
+
const revokedSessions = isString(claims.sid) ? await options.storeProvider.deleteSessionsByProviderSessionId({
|
|
852
|
+
providerSessionId: claims.sid,
|
|
853
|
+
issuer: metadata.issuer,
|
|
854
|
+
clientId: metadata.clientId
|
|
855
|
+
}) : await options.storeProvider.deleteSessionsBySubject({
|
|
856
|
+
subject: String(claims.sub),
|
|
857
|
+
issuer: metadata.issuer,
|
|
858
|
+
clientId: metadata.clientId
|
|
859
|
+
});
|
|
860
|
+
await callHook("backchannel-logout", options.hooks?.onLogout, req, res, void 0, {
|
|
861
|
+
providerSessionId: claims.sid,
|
|
862
|
+
subject: claims.sub,
|
|
863
|
+
revokedSessions
|
|
864
|
+
});
|
|
865
|
+
res.status(200).json({
|
|
866
|
+
loggedOut: true,
|
|
867
|
+
revokedSessions
|
|
868
|
+
});
|
|
869
|
+
});
|
|
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));
|
|
873
|
+
router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options));
|
|
874
|
+
router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins));
|
|
875
|
+
router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins));
|
|
876
|
+
router.post(OIDC_VAULT_ROUTE_PATHS["backchannel-logout"], createBackchannelLogoutHandler(options, config));
|
|
877
|
+
}
|
|
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
|
+
function createOidcVaultMiddleware(options) {
|
|
930
|
+
const { config, trustedOrigins } = validateOidcVaultOptions(options);
|
|
931
|
+
const rootRouter = express.Router();
|
|
932
|
+
const baseRouter = express.Router();
|
|
933
|
+
const basePath = normalizeOidcVaultBasePath(options.basePath);
|
|
934
|
+
baseRouter.use(express.json());
|
|
935
|
+
baseRouter.use(express.urlencoded({ extended: false }));
|
|
936
|
+
registerRoutes(baseRouter, options, config, trustedOrigins, basePath);
|
|
937
|
+
rootRouter.use(basePath, baseRouter);
|
|
938
|
+
return rootRouter;
|
|
939
|
+
}
|
|
940
|
+
export {
|
|
941
|
+
DEFAULT_AUTHORIZATION_TRANSACTION_TTL_MS,
|
|
942
|
+
DEFAULT_EXCHANGE_CODE_TTL_MS,
|
|
943
|
+
DEFAULT_OIDC_SCOPES,
|
|
944
|
+
DEFAULT_OIDC_VAULT_BASE_PATH,
|
|
945
|
+
DEFAULT_SESSION_COOKIE_NAME,
|
|
946
|
+
OIDC_VAULT_ROUTE_PATHS,
|
|
947
|
+
OidcVaultStoreConflictError,
|
|
948
|
+
createOidcVaultAccessTokenMiddleware,
|
|
949
|
+
createOidcVaultJwtAccessTokenValidator,
|
|
950
|
+
createOidcVaultMiddleware,
|
|
951
|
+
normalizeOidcVaultBasePath,
|
|
952
|
+
resolveOidcVaultConfig,
|
|
953
|
+
resolveOidcVaultConfigFromEnv
|
|
954
|
+
};
|