@better-auth/oauth-provider 1.7.10 → 1.8.0-beta.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/dist/{authorize-dkOG8GqZ.mjs → authorize-92EtLsBi.mjs} +578 -431
- package/dist/client-resource.d.mts +2 -2
- package/dist/client-resource.mjs +2 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index-sGnh4IfY.d.mts +275 -0
- package/dist/index.d.mts +4 -203
- package/dist/index.mjs +196 -40
- package/dist/internal.d.mts +109 -22
- package/dist/internal.mjs +4 -2
- package/dist/{introspect-BVm_zFQC.mjs → introspect-CEo9Q06v.mjs} +294 -227
- package/dist/{oauth-DXiqbRzK.d.mts → oauth-BlEuA8oD.d.mts} +346 -13
- package/dist/{oauth-Cse_eZ6i.d.mts → oauth-mdkaIwmm.d.mts} +163 -87
- package/dist/{utils-DfMNRh3J.mjs → utils-BMNe8Sg2.mjs} +287 -20
- package/dist/{version-aKfgWHzo.mjs → version-DeABnmP9.mjs} +1 -1
- package/package.json +6 -6
- package/dist/client-jwks-DY2QBrsl.d.mts +0 -25
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
|
|
2
|
-
import { A as
|
|
3
|
-
import { A as
|
|
2
|
+
import { A as getAcrValuesSupported, C as authorizationQuerySchema, D as STANDARD_CLAIMS, F as getRequestedUserInfoClaims, I as isValidOidcClaimsRequest, M as canSatisfyEssentialAcrRequest, N as claimsRequestParameterSchema, O as STANDARD_CLAIM_NAMES, P as filterClaimsRequestUserInfoClaims, S as SafeUrlSchema, T as clientRegistrationRequestSchema, _ as logStartupSeedFailure, a as tokenEndpoint, b as userInfoResource, c as normalizeClientCredentialsScopes, d as JWS_ALGORITHMS, f as assertIdentifierValid, g as logEnforcePerClientResourcesResolution, h as isAudienceClaimAllowed, i as invalidateRefreshFamily, j as resolveAuthenticationContext, k as getSupportedClaims, l as validateClientCredentialsScopes, m as invalidateResourceCache, o as userInfoEndpoint, p as getResource, s as findToken, t as introspectEndpoint, u as getEndpointUrl, v as resolveResourcePolicy, w as clientJwksSchema, x as ResourceUriSchema, y as seedResourcesOnce } from "./introspect-CEo9Q06v.mjs";
|
|
3
|
+
import { $ as validateOAuthProviderExtensions, A as storeClientSecret, E as removePromptFromQuery, F as validateClientCredentials, J as getSupportedGrantTypes, K as getExtensionInteractionRequest, L as verifyOAuthQueryParams, N as toResourceList, O as resolveSubjectIdentifier, Q as validateExtensionClientMetadata, R as applyOAuthProviderMetadataExtensions, S as parsePrompt, T as removeMaxAgeFromQuery, U as getClientDiscoveries, W as getExtensionClientMetadataFields, Y as hasExtensionClientMetadata, Z as isExtensionTokenEndpointAuthMethod, _ as mergeDiscoveryMetadata, a as decryptStoredClientSecret, b as parseBearerToken, c as extractClientCredentials, d as getConsentReferenceId, f as getJwtPlugin, g as isSessionFreshForSignedQuery, h as isPKCERequired, i as consentCovers, j as storeToken, k as searchParamsToQuery, l as findConsent, n as assertNotTrustedClient, o as deriveSectorIdentifier, q as getSupportedAuthMethods, r as clientAllowsGrant, s as destructureCredentials, t as applyFormParams, tt as stripReservedOAuthClientMetadataExtensions, u as getClient, w as readFormParams, x as parseClientMetadata } from "./utils-BMNe8Sg2.mjs";
|
|
4
4
|
import { a as setSignedOAuthQueryParameterNames, i as postLoginClearedParam, n as canonicalizeOAuthQueryParams, o as signedQueryIssuedAtParam, r as getSignedQueryIssuedAt } from "./signed-query-Df1MNiSH.mjs";
|
|
5
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
5
|
+
import { t as PACKAGE_VERSION } from "./version-DeABnmP9.mjs";
|
|
6
6
|
import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
|
|
7
7
|
import { isLoopbackHost, isLoopbackIP, isPublicRoutableHost } from "@better-auth/core/utils/host";
|
|
8
8
|
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
@@ -11,16 +11,16 @@ import { generateRandomString, makeSignature } from "better-auth/crypto";
|
|
|
11
11
|
import { APIError as APIError$1 } from "better-call";
|
|
12
12
|
import { logger } from "@better-auth/core/env";
|
|
13
13
|
import * as z from "zod";
|
|
14
|
-
import { defineRequestState, getCurrentAdapter, runWithTransaction } from "@better-auth/core/context";
|
|
15
14
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
15
|
+
import { CLIENT_ASSERTION_TYPE, DPOP_SIGNING_ALGORITHMS, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
|
|
16
|
+
import { defineRequestState, getCurrentAdapter, hasRequestState, runWithTransaction } from "@better-auth/core/context";
|
|
16
17
|
import { deleteSessionCookie, parseSetCookieHeader } from "better-auth/cookies";
|
|
17
18
|
import { mergeSchema } from "better-auth/db";
|
|
18
|
-
import { CLIENT_ASSERTION_TYPE, DPOP_SIGNING_ALGORITHMS, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
|
|
19
19
|
import { createHash } from "@better-auth/utils/hash";
|
|
20
|
-
import { compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader, errors, jwtVerify } from "jose";
|
|
21
20
|
import { getJwks, stripAccessTokenAuthorizationScheme } from "better-auth/oauth2";
|
|
22
21
|
import { isNativePrivateUseRedirectUri } from "@better-auth/core/utils/redirect-uri";
|
|
23
22
|
import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
|
|
23
|
+
import { compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
|
|
24
24
|
import { encodeDeterministicId } from "@better-auth/core/db/internal";
|
|
25
25
|
//#region src/consent.ts
|
|
26
26
|
async function consentEndpoint(ctx, opts, authorize) {
|
|
@@ -31,7 +31,7 @@ async function consentEndpoint(ctx, opts, authorize) {
|
|
|
31
31
|
error: "invalid_request"
|
|
32
32
|
});
|
|
33
33
|
const query = new URLSearchParams(_query);
|
|
34
|
-
const originalRequestedScopes = query.get("scope")?.split(" ") ?? [];
|
|
34
|
+
const originalRequestedScopes = query.get("scope")?.split(" ").filter((scope) => scope) ?? [];
|
|
35
35
|
const supportedClaims = getSupportedClaims(opts);
|
|
36
36
|
const originalRequestedUserInfoClaims = getRequestedUserInfoClaims(query.get("claims"), supportedClaims);
|
|
37
37
|
const clientId = query.get("client_id");
|
|
@@ -39,7 +39,7 @@ async function consentEndpoint(ctx, opts, authorize) {
|
|
|
39
39
|
error_description: "client_id is required",
|
|
40
40
|
error: "invalid_client"
|
|
41
41
|
});
|
|
42
|
-
const requestedScopes = ctx.body.scope?.split(" ");
|
|
42
|
+
const requestedScopes = ctx.body.scope?.split(" ").filter((scope) => scope);
|
|
43
43
|
if (requestedScopes) {
|
|
44
44
|
if (!requestedScopes.every((sc) => originalRequestedScopes?.includes(sc))) throw new APIError("BAD_REQUEST", {
|
|
45
45
|
error_description: "Scope not originally requested",
|
|
@@ -52,11 +52,25 @@ async function consentEndpoint(ctx, opts, authorize) {
|
|
|
52
52
|
error_description: "Claim not originally requested",
|
|
53
53
|
error: "invalid_request"
|
|
54
54
|
});
|
|
55
|
-
if (!(ctx.body.accept === true)) return {
|
|
56
|
-
redirect: true,
|
|
57
|
-
url: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0, getIssuer(ctx, opts))
|
|
58
|
-
};
|
|
59
55
|
const session = await getSessionFromCtx(ctx);
|
|
56
|
+
if (!(ctx.body.accept === true)) {
|
|
57
|
+
const interaction = getExtensionInteractionRequest(opts, searchParamsToQuery(query));
|
|
58
|
+
if (interaction) return {
|
|
59
|
+
redirect: true,
|
|
60
|
+
url: await interaction.handler.deny({
|
|
61
|
+
ctx,
|
|
62
|
+
value: interaction.value,
|
|
63
|
+
userId: session?.user.id
|
|
64
|
+
}).catch((error) => getAPIErrorURL(ctx, error, interaction.handler.errorPage?.({
|
|
65
|
+
ctx,
|
|
66
|
+
value: interaction.value
|
|
67
|
+
})))
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
redirect: true,
|
|
71
|
+
url: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0, getIssuer(ctx, opts))
|
|
72
|
+
};
|
|
73
|
+
}
|
|
60
74
|
const hasLoginPrompt = parsePrompt(query.get("prompt") ?? "").has("login");
|
|
61
75
|
const hasSatisfiedLoginPrompt = hasLoginPrompt && isSessionFreshForSignedQuery(session?.session.createdAt, oauthRequest?.signedQueryIssuedAt);
|
|
62
76
|
if (hasLoginPrompt && !hasSatisfiedLoginPrompt) {
|
|
@@ -114,7 +128,10 @@ async function consentEndpoint(ctx, opts, authorize) {
|
|
|
114
128
|
authorizationQuery = removeMaxAgeFromQuery(authorizationQuery);
|
|
115
129
|
}
|
|
116
130
|
ctx.query = searchParamsToQuery(authorizationQuery);
|
|
117
|
-
return await authorize(ctx, {
|
|
131
|
+
return await authorize(ctx, {
|
|
132
|
+
postLogin: oauthRequest?.postLoginClearedForSession !== void 0 && oauthRequest.postLoginClearedForSession === session?.session.id,
|
|
133
|
+
consentAccepted: true
|
|
134
|
+
});
|
|
118
135
|
}
|
|
119
136
|
//#endregion
|
|
120
137
|
//#region src/continue.ts
|
|
@@ -170,6 +187,7 @@ const BACKCHANNEL_LOGOUT_EVENT_URI = "http://schemas.openid.net/event/backchanne
|
|
|
170
187
|
const LOGOUT_TOKEN_JWT_TYP = "logout+jwt";
|
|
171
188
|
const LOGOUT_TOKEN_LIFETIME_SECONDS = 120;
|
|
172
189
|
const BACKCHANNEL_DISPATCH_TIMEOUT_MS = 5e3;
|
|
190
|
+
const FRONTCHANNEL_LOGOUT_TIMEOUT_MS = 3e3;
|
|
173
191
|
/**
|
|
174
192
|
* Signs a Back-Channel Logout Token per OIDC Back-Channel Logout 1.0 §2.4.
|
|
175
193
|
*
|
|
@@ -211,11 +229,12 @@ async function signLogoutToken(ctx, options, resolvedKey, claims) {
|
|
|
211
229
|
* Token revocation runs regardless of the JWT plugin (refresh-token revocation
|
|
212
230
|
* has no dependency on signing). Only Logout Token delivery needs the JWT
|
|
213
231
|
* plugin, so a plan may contain token identifiers with no delivery targets.
|
|
232
|
+
* Front-channel URIs sign nothing and are planned in either mode.
|
|
214
233
|
*
|
|
215
234
|
* Returns `null` when there is nothing to do, so the caller can skip the
|
|
216
235
|
* background handoff entirely.
|
|
217
236
|
*/
|
|
218
|
-
async function
|
|
237
|
+
async function prepareSessionLogoutPlan(ctx, opts, input) {
|
|
219
238
|
const { sessionId, userId } = input;
|
|
220
239
|
if (!userId) return null;
|
|
221
240
|
const logger = ctx.context.logger;
|
|
@@ -247,24 +266,38 @@ async function prepareBackchannelLogoutPlan(ctx, opts, input) {
|
|
|
247
266
|
const accessToRevokeIds = accessTokens.filter((t) => !t.revoked).map((t) => t.id);
|
|
248
267
|
const refreshToRevokeIds = refreshTokens.filter((t) => !t.revoked && !t.scopes?.includes("offline_access")).map((t) => t.id);
|
|
249
268
|
const eligibleClients = opts.disableJwtPlugin ? [] : clients.filter((c) => Boolean(c.backchannelLogoutUri) && !c.disabled);
|
|
269
|
+
const backchannelTargets = (await Promise.all(eligibleClients.map(async (client) => {
|
|
270
|
+
try {
|
|
271
|
+
return {
|
|
272
|
+
client,
|
|
273
|
+
sub: await resolveSubjectIdentifier(userId, client, opts)
|
|
274
|
+
};
|
|
275
|
+
} catch (error) {
|
|
276
|
+
logger.warn(`back-channel logout: unable to resolve subject for client ${client.clientId}`, error);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}))).filter((target) => target !== null);
|
|
280
|
+
const issuer = getIssuer(ctx, opts);
|
|
250
281
|
return {
|
|
251
282
|
accessTokenIds: accessToRevokeIds,
|
|
252
283
|
refreshTokenIds: refreshToRevokeIds,
|
|
253
284
|
sessionId,
|
|
254
|
-
|
|
285
|
+
backchannelTargets,
|
|
286
|
+
frontchannelLogoutUris: clients.flatMap((client) => {
|
|
287
|
+
if (!client.frontchannelLogoutUri || client.disabled) return [];
|
|
255
288
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
289
|
+
const uri = new URL(client.frontchannelLogoutUri);
|
|
290
|
+
uri.searchParams.set("iss", issuer);
|
|
291
|
+
uri.searchParams.set("sid", sessionId);
|
|
292
|
+
return [uri.toString()];
|
|
260
293
|
} catch (error) {
|
|
261
|
-
logger.warn(`
|
|
262
|
-
return
|
|
294
|
+
logger.warn(`front-channel logout: invalid frontchannel_logout_uri for client ${client.clientId}`, error);
|
|
295
|
+
return [];
|
|
263
296
|
}
|
|
264
|
-
})
|
|
297
|
+
})
|
|
265
298
|
};
|
|
266
299
|
} catch (error) {
|
|
267
|
-
logger.error("
|
|
300
|
+
logger.error("logout planning failed", error);
|
|
268
301
|
return null;
|
|
269
302
|
}
|
|
270
303
|
}
|
|
@@ -273,7 +306,7 @@ async function prepareBackchannelLogoutPlan(ctx, opts, input) {
|
|
|
273
306
|
* `session.delete.after` after the row is consumed and, for transactional
|
|
274
307
|
* callers, only after the transaction commits.
|
|
275
308
|
*/
|
|
276
|
-
async function
|
|
309
|
+
async function applySessionLogoutPlan(ctx, plan) {
|
|
277
310
|
const revokedAt = /* @__PURE__ */ new Date();
|
|
278
311
|
const adapter = await getCurrentAdapter(ctx.context.adapter);
|
|
279
312
|
const revocations = await Promise.allSettled([plan.accessTokenIds.length > 0 ? adapter.updateMany({
|
|
@@ -294,7 +327,7 @@ async function applyBackchannelLogoutPlan(ctx, plan) {
|
|
|
294
327
|
update: { revoked: revokedAt }
|
|
295
328
|
}) : Promise.resolve()]);
|
|
296
329
|
for (const result of revocations) if (result.status === "rejected") ctx.context.logger.error("back-channel logout: token revocation update failed", result.reason);
|
|
297
|
-
if (plan.
|
|
330
|
+
if (plan.backchannelTargets.length > 0) await deliverBackchannelLogoutTokens(ctx, plan);
|
|
298
331
|
}
|
|
299
332
|
/**
|
|
300
333
|
* Asynchronous phase: sign one Logout Token per target client and POST it to
|
|
@@ -315,7 +348,7 @@ async function deliverBackchannelLogoutTokens(ctx, plan) {
|
|
|
315
348
|
const iat = Math.floor(Date.now() / 1e3);
|
|
316
349
|
const exp = iat + LOGOUT_TOKEN_LIFETIME_SECONDS;
|
|
317
350
|
const resolvedKey = jwtPluginOptions?.jwt?.sign ? null : await resolveSigningKey(ctx, jwtPluginOptions);
|
|
318
|
-
await Promise.allSettled(plan.
|
|
351
|
+
await Promise.allSettled(plan.backchannelTargets.map(async ({ client, sub }) => {
|
|
319
352
|
try {
|
|
320
353
|
const jti = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
321
354
|
const token = await signLogoutToken(ctx, jwtPluginOptions, resolvedKey, {
|
|
@@ -343,6 +376,30 @@ async function deliverBackchannelLogoutTokens(ctx, plan) {
|
|
|
343
376
|
}
|
|
344
377
|
}));
|
|
345
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Front-channel logout URIs collected from sessions deleted during the current
|
|
381
|
+
* request. `session.delete.after` records them only once the deletion has
|
|
382
|
+
* committed, so a vetoed or rolled-back delete never reaches the logout page.
|
|
383
|
+
*/
|
|
384
|
+
const frontchannelLogoutUrisState = defineRequestState(() => []);
|
|
385
|
+
/**
|
|
386
|
+
* Records a committed plan's front-channel URIs for the logout page of the
|
|
387
|
+
* current request. Deletions outside a request (no browser to render iframes)
|
|
388
|
+
* have nowhere to deliver them, so they are dropped.
|
|
389
|
+
*/
|
|
390
|
+
async function recordFrontchannelLogoutUris(uris) {
|
|
391
|
+
if (uris.length === 0 || !await hasRequestState()) return;
|
|
392
|
+
const recorded = await frontchannelLogoutUrisState.get();
|
|
393
|
+
await frontchannelLogoutUrisState.set([...recorded, ...uris]);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Returns and clears the front-channel URIs recorded during this request.
|
|
397
|
+
*/
|
|
398
|
+
async function takeFrontchannelLogoutUris() {
|
|
399
|
+
const recorded = await frontchannelLogoutUrisState.get();
|
|
400
|
+
await frontchannelLogoutUrisState.set([]);
|
|
401
|
+
return recorded;
|
|
402
|
+
}
|
|
346
403
|
const LOGOUT_CONFIRMATION_TTL_SECONDS = 300;
|
|
347
404
|
const LOGOUT_CONFIRMATION_COOKIE_SUFFIX = ".oauth_logout_confirmation";
|
|
348
405
|
function escapeHtml(value) {
|
|
@@ -363,14 +420,16 @@ function isBrowserNavigation(ctx) {
|
|
|
363
420
|
const accept = headers.get("accept") ?? "";
|
|
364
421
|
return headers.get("sec-fetch-mode") === "navigate" || accept.includes("text/html") || accept.includes("application/xhtml+xml");
|
|
365
422
|
}
|
|
366
|
-
|
|
367
|
-
|
|
423
|
+
const LOGOUT_PAGE_CSP = "default-src 'none'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
|
|
424
|
+
function logoutPage(title, body, status = 200, page = {}) {
|
|
425
|
+
return new Response(`<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title>${page.head ?? ""}</head><body>${body}</body></html>`, {
|
|
368
426
|
status,
|
|
369
427
|
headers: {
|
|
370
428
|
"cache-control": "no-store",
|
|
371
|
-
"content-security-policy":
|
|
429
|
+
"content-security-policy": page.contentSecurityPolicy ?? LOGOUT_PAGE_CSP,
|
|
372
430
|
"content-type": "text/html; charset=utf-8",
|
|
373
431
|
pragma: "no-cache",
|
|
432
|
+
"referrer-policy": "no-referrer",
|
|
374
433
|
"x-content-type-options": "nosniff"
|
|
375
434
|
}
|
|
376
435
|
});
|
|
@@ -391,6 +450,53 @@ function logoutConfirmationPage(ctx) {
|
|
|
391
450
|
function logoutSuccessPage(note) {
|
|
392
451
|
return logoutPage("Logged out", `<main><p data-oidc-logout-state="logged-out">${note ? `Logged out. ${escapeHtml(note)}` : "Logged out."}</p></main>`);
|
|
393
452
|
}
|
|
453
|
+
const FRONTCHANNEL_LOGOUT_SCRIPT = `(function () {
|
|
454
|
+
var status = document.querySelector("[data-oidc-logout-state]");
|
|
455
|
+
var redirectUri = status.getAttribute("data-post-logout-redirect-uri");
|
|
456
|
+
var frames = document.querySelectorAll("iframe");
|
|
457
|
+
var pending = frames.length;
|
|
458
|
+
var finished = false;
|
|
459
|
+
function finish() {
|
|
460
|
+
if (finished) return;
|
|
461
|
+
finished = true;
|
|
462
|
+
if (redirectUri) {
|
|
463
|
+
window.location.replace(redirectUri);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
status.setAttribute("data-oidc-logout-state", "logged-out");
|
|
467
|
+
status.textContent = status.getAttribute("data-logged-out-message");
|
|
468
|
+
}
|
|
469
|
+
function settle() {
|
|
470
|
+
pending -= 1;
|
|
471
|
+
if (pending <= 0) finish();
|
|
472
|
+
}
|
|
473
|
+
for (var i = 0; i < frames.length; i++) {
|
|
474
|
+
frames[i].addEventListener("load", settle);
|
|
475
|
+
frames[i].addEventListener("error", settle);
|
|
476
|
+
}
|
|
477
|
+
setTimeout(finish, ${FRONTCHANNEL_LOGOUT_TIMEOUT_MS});
|
|
478
|
+
})();`;
|
|
479
|
+
let frontchannelLogoutScriptHash;
|
|
480
|
+
/**
|
|
481
|
+
* OIDC Front-Channel Logout 1.0 §3 logout page: one hidden iframe per relying
|
|
482
|
+
* party, then a redirect to the verified `post_logout_redirect_uri` or a
|
|
483
|
+
* logged-out message. The OP session has already ended when this renders.
|
|
484
|
+
* Without JavaScript the iframes still load, and a meta refresh performs the
|
|
485
|
+
* redirect.
|
|
486
|
+
*/
|
|
487
|
+
async function frontchannelLogoutPage(uris, redirect) {
|
|
488
|
+
frontchannelLogoutScriptHash ??= createHash("SHA-256", "base64").digest(FRONTCHANNEL_LOGOUT_SCRIPT);
|
|
489
|
+
const scriptHash = await frontchannelLogoutScriptHash;
|
|
490
|
+
const frameOrigins = [...new Set(uris.map((uri) => new URL(uri).origin))];
|
|
491
|
+
const loggedOutMessage = redirect.note ? `Logged out. ${redirect.note}` : "Logged out.";
|
|
492
|
+
const iframes = uris.map((uri) => `<iframe hidden src="${escapeHtml(uri)}"></iframe>`).join("");
|
|
493
|
+
const redirectAttribute = redirect.uri ? ` data-post-logout-redirect-uri="${escapeHtml(redirect.uri)}"` : "";
|
|
494
|
+
const noscript = redirect.uri ? `<noscript><a href="${escapeHtml(redirect.uri)}">Continue</a></noscript>` : `<noscript><p>${escapeHtml(loggedOutMessage)}</p></noscript>`;
|
|
495
|
+
return logoutPage("Signing out", `<main><p data-oidc-logout-state="pending" data-logged-out-message="${escapeHtml(loggedOutMessage)}"${redirectAttribute}>Signing out of connected applications…</p>${noscript}</main>${iframes}<script>${FRONTCHANNEL_LOGOUT_SCRIPT}<\/script>`, 200, {
|
|
496
|
+
head: redirect.uri ? `<noscript><meta http-equiv="refresh" content="${Math.ceil(FRONTCHANNEL_LOGOUT_TIMEOUT_MS / 1e3)};url=${escapeHtml(redirect.uri)}"></noscript>` : void 0,
|
|
497
|
+
contentSecurityPolicy: `${LOGOUT_PAGE_CSP}; frame-src ${frameOrigins.join(" ")}; script-src 'sha256-${scriptHash}'`
|
|
498
|
+
});
|
|
499
|
+
}
|
|
394
500
|
function logoutErrorPage(description, status) {
|
|
395
501
|
return logoutPage("Logout error", `<main><h1>Logout error</h1><p data-oidc-logout-state="error">${escapeHtml(description)}</p></main>`, status);
|
|
396
502
|
}
|
|
@@ -588,6 +694,22 @@ async function confirmationRequired(ctx, currentSession, confirmation = {}) {
|
|
|
588
694
|
await setLogoutConfirmationState(ctx, currentSession.session.id, confirmation);
|
|
589
695
|
return logoutConfirmationPage(ctx);
|
|
590
696
|
}
|
|
697
|
+
const UNREGISTERED_REDIRECT_NOTE = "The requested post-logout redirect was not registered.";
|
|
698
|
+
/**
|
|
699
|
+
* Response once the session has ended. A browser navigation renders the
|
|
700
|
+
* front-channel logout page when relying parties must be notified; otherwise
|
|
701
|
+
* the caller gets the verified redirect, the success page, or an empty result.
|
|
702
|
+
*/
|
|
703
|
+
async function loggedOutResponse(ctx, redirect) {
|
|
704
|
+
const note = redirect.invalid ? UNREGISTERED_REDIRECT_NOTE : void 0;
|
|
705
|
+
const frontchannelLogoutUris = await takeFrontchannelLogoutUris();
|
|
706
|
+
if (frontchannelLogoutUris.length > 0 && isBrowserNavigation(ctx)) return frontchannelLogoutPage(frontchannelLogoutUris, {
|
|
707
|
+
uri: redirect.uri,
|
|
708
|
+
note
|
|
709
|
+
});
|
|
710
|
+
if (redirect.uri) return handleRedirect(ctx, redirect.uri);
|
|
711
|
+
return isBrowserNavigation(ctx) ? logoutSuccessPage(note) : null;
|
|
712
|
+
}
|
|
591
713
|
async function completeConfirmedLogout(ctx, opts) {
|
|
592
714
|
const state = await getLogoutConfirmationState(ctx);
|
|
593
715
|
const currentSession = await getCurrentBrowserSession(ctx);
|
|
@@ -596,14 +718,13 @@ async function completeConfirmedLogout(ctx, opts) {
|
|
|
596
718
|
if (!currentSession) {
|
|
597
719
|
clearLogoutConfirmationState(ctx);
|
|
598
720
|
if (redirect.uri) return handleRedirect(ctx, redirect.uri);
|
|
599
|
-
return isBrowserNavigation(ctx) ? logoutSuccessPage(redirect.invalid ?
|
|
721
|
+
return isBrowserNavigation(ctx) ? logoutSuccessPage(redirect.invalid ? UNREGISTERED_REDIRECT_NOTE : void 0) : logoutProtocolError(ctx, "BAD_REQUEST", "invalid_request", "No active session is available for logout");
|
|
600
722
|
}
|
|
601
723
|
if (state.sessionId && state.sessionId !== currentSession.session.id) return logoutProtocolError(ctx, "BAD_REQUEST", "invalid_request", "The logout confirmation is invalid or expired");
|
|
602
724
|
await deleteLogoutSession(ctx, currentSession.session);
|
|
603
725
|
deleteSessionCookie(ctx);
|
|
604
726
|
clearLogoutConfirmationState(ctx);
|
|
605
|
-
|
|
606
|
-
return isBrowserNavigation(ctx) ? logoutSuccessPage(redirect.invalid ? "The requested post-logout redirect was not registered." : void 0) : null;
|
|
727
|
+
return loggedOutResponse(ctx, redirect);
|
|
607
728
|
}
|
|
608
729
|
/**
|
|
609
730
|
* RP-Initiated Logout (OIDC RP-Initiated Logout 1.0). The RP presents a signed
|
|
@@ -614,7 +735,9 @@ async function completeConfirmedLogout(ctx, opts) {
|
|
|
614
735
|
*
|
|
615
736
|
* Session termination goes through `internalAdapter.deleteSession`, which fires
|
|
616
737
|
* `session.delete.after` so the hook drives revocation and back-channel
|
|
617
|
-
* notifications to every RP with tokens on the session.
|
|
738
|
+
* notifications to every RP with tokens on the session. When the browser
|
|
739
|
+
* navigates here, RPs with a `frontchannel_logout_uri` are then notified
|
|
740
|
+
* through hidden iframes before the redirect.
|
|
618
741
|
*
|
|
619
742
|
* @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html
|
|
620
743
|
*/
|
|
@@ -658,8 +781,7 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
|
|
|
658
781
|
if (sessionToDelete) await deleteLogoutSession(ctx, sessionToDelete);
|
|
659
782
|
if (matchesCurrentSession) deleteSessionCookie(ctx);
|
|
660
783
|
clearLogoutConfirmationState(ctx);
|
|
661
|
-
|
|
662
|
-
return isBrowserNavigation(ctx) ? logoutSuccessPage(redirect.invalid ? "The requested post-logout redirect was not registered." : void 0) : null;
|
|
784
|
+
return loggedOutResponse(ctx, redirect);
|
|
663
785
|
}
|
|
664
786
|
/**
|
|
665
787
|
* Completes the browser confirmation flow for RP-Initiated Logout. This route
|
|
@@ -713,7 +835,9 @@ function authServerMetadata(ctx, opts, overrides) {
|
|
|
713
835
|
authorization_response_iss_parameter_supported: true,
|
|
714
836
|
dpop_signing_alg_values_supported: overrides?.dpop_signing_alg_values_supported ?? [...DPOP_SIGNING_ALGORITHMS],
|
|
715
837
|
backchannel_logout_supported: backchannelSupported,
|
|
716
|
-
backchannel_logout_session_supported: backchannelSupported
|
|
838
|
+
backchannel_logout_session_supported: backchannelSupported,
|
|
839
|
+
frontchannel_logout_supported: true,
|
|
840
|
+
frontchannel_logout_session_supported: true
|
|
717
841
|
};
|
|
718
842
|
}
|
|
719
843
|
/**
|
|
@@ -756,7 +880,7 @@ function oidcServerMetadata(ctx, opts) {
|
|
|
756
880
|
claims_parameter_supported: true,
|
|
757
881
|
userinfo_endpoint: `${baseURL}/oauth2/userinfo`,
|
|
758
882
|
subject_types_supported: opts.pairwiseSecret ? ["public", "pairwise"] : ["public"],
|
|
759
|
-
acr_values_supported:
|
|
883
|
+
acr_values_supported: getAcrValuesSupported(opts),
|
|
760
884
|
id_token_signing_alg_values_supported: (() => {
|
|
761
885
|
if (opts.disableJwtPlugin) return ["HS256"];
|
|
762
886
|
const primary = jwtPluginOptions?.jwks?.keyPairConfig?.alg ?? "EdDSA";
|
|
@@ -830,7 +954,8 @@ const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
|
830
954
|
*
|
|
831
955
|
* A failing issue is routed by its first path segment via `errorCodesByField`:
|
|
832
956
|
* - missing required (`invalid_type` + "received undefined") → `.missing`
|
|
833
|
-
* - unsupported value (`invalid_value`)
|
|
957
|
+
* - unsupported value (`invalid_value`) or a value a refinement rejected
|
|
958
|
+
* (`custom`, also inside a union branch) → `.invalid`
|
|
834
959
|
* - anything else (wrong type, duplicated params, bad format) → `defaultError`
|
|
835
960
|
*
|
|
836
961
|
* For enum fields that need to distinguish missing from unsupported, compose
|
|
@@ -839,16 +964,19 @@ const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
|
|
|
839
964
|
* enum value.
|
|
840
965
|
*/
|
|
841
966
|
function createOAuthEndpoint(path, options, handler) {
|
|
842
|
-
const { redirectOnError, onValidationError: userHook, errorCodesByField, defaultError = "invalid_request", ...rest } = options;
|
|
843
|
-
if (!redirectOnError
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
967
|
+
const { redirectOnError, onValidationError: userHook, errorCodesByField, defaultError = "invalid_request", formParams, ...rest } = options;
|
|
968
|
+
if (!redirectOnError && !formParams) {
|
|
969
|
+
const noStore = rest.metadata?.noStore === true;
|
|
970
|
+
return createAuthEndpoint(path, {
|
|
971
|
+
...rest,
|
|
972
|
+
onValidationError: async (args) => {
|
|
973
|
+
if (userHook) await userHook(args);
|
|
974
|
+
throw new APIError$1("BAD_REQUEST", mapIssuesToOAuthError(args.issues, errorCodesByField, defaultError), noStore ? NO_STORE_HEADERS : void 0);
|
|
975
|
+
}
|
|
976
|
+
}, handler);
|
|
977
|
+
}
|
|
851
978
|
const { body: bodySchema, query: querySchema, ...forwarded } = rest;
|
|
979
|
+
if (formParams) forwarded.cloneRequest = true;
|
|
852
980
|
async function validateSlot(ctx, slot, schema) {
|
|
853
981
|
if (!schema) return { ok: true };
|
|
854
982
|
const result = await schema.safeParseAsync(ctx[slot] ?? {});
|
|
@@ -860,15 +988,19 @@ function createOAuthEndpoint(path, options, handler) {
|
|
|
860
988
|
message: result.error.message,
|
|
861
989
|
issues: result.error.issues
|
|
862
990
|
});
|
|
991
|
+
const error = mapIssuesToOAuthError(result.error.issues, errorCodesByField, defaultError);
|
|
992
|
+
if (!redirectOnError) throw new APIError$1("BAD_REQUEST", error);
|
|
863
993
|
return {
|
|
864
994
|
ok: false,
|
|
865
|
-
response:
|
|
866
|
-
...
|
|
995
|
+
response: redirectOnError({
|
|
996
|
+
...error,
|
|
867
997
|
ctx
|
|
868
998
|
})
|
|
869
999
|
};
|
|
870
1000
|
}
|
|
871
1001
|
return createAuthEndpoint(path, forwarded, async (ctx) => {
|
|
1002
|
+
const form = formParams && ctx.request ? await readFormParams(ctx.request) : void 0;
|
|
1003
|
+
if (form) ctx.body = applyFormParams(form, ctx.body ?? {}, formParams?.repeatable);
|
|
872
1004
|
const body = await validateSlot(ctx, "body", bodySchema);
|
|
873
1005
|
if (!body.ok) return body.response;
|
|
874
1006
|
const query = await validateSlot(ctx, "query", querySchema);
|
|
@@ -886,18 +1018,32 @@ function mapIssuesToOAuthError(issues, errorCodesByField, defaultError = "invali
|
|
|
886
1018
|
const fieldKey = typeof first === "string" ? first : void 0;
|
|
887
1019
|
const mapping = fieldKey ? errorCodesByField?.[fieldKey] : void 0;
|
|
888
1020
|
const field = issue.path?.length ? z.core.toDotPath(issue.path) : "";
|
|
1021
|
+
const invalidValue = findInvalidValueIssue(issue);
|
|
889
1022
|
return {
|
|
890
|
-
error: resolveErrorCode(issue, mapping, defaultError),
|
|
891
|
-
error_description: describeIssue(issue, field)
|
|
1023
|
+
error: resolveErrorCode(issue, invalidValue, mapping, defaultError),
|
|
1024
|
+
error_description: describeIssue(invalidValue ?? issue, field)
|
|
892
1025
|
};
|
|
893
1026
|
}
|
|
894
|
-
function resolveErrorCode(issue, mapping, defaultError) {
|
|
1027
|
+
function resolveErrorCode(issue, invalidValue, mapping, defaultError) {
|
|
895
1028
|
if (typeof mapping === "string") return mapping;
|
|
896
1029
|
if (isMissingValueIssue(issue)) return mapping?.missing ?? defaultError;
|
|
897
|
-
if (
|
|
1030
|
+
if (invalidValue) return mapping?.invalid ?? defaultError;
|
|
898
1031
|
return defaultError;
|
|
899
1032
|
}
|
|
900
1033
|
/**
|
|
1034
|
+
* Returns the issue that routes to `errorCodesByField[field].invalid`: an
|
|
1035
|
+
* unsupported enum value, or a value of the expected type that a refinement
|
|
1036
|
+
* rejected, such as a resource URI with a fragment. A union reports a
|
|
1037
|
+
* refinement failure as `invalid_union`, so its branches are searched too.
|
|
1038
|
+
*/
|
|
1039
|
+
function findInvalidValueIssue(issue) {
|
|
1040
|
+
if (issue.code === "invalid_value" || issue.code === "custom") return issue;
|
|
1041
|
+
if (issue.code === "invalid_union") for (const branch of issue.errors) {
|
|
1042
|
+
const rejected = branch.find((nested) => nested.code === "custom");
|
|
1043
|
+
if (rejected) return rejected;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
901
1047
|
* Returns `true` for issues that represent an absent required value. Zod v4
|
|
902
1048
|
* strips `input` from published issues, so the signal is the `invalid_type`
|
|
903
1049
|
* code combined with a message suffix of "received undefined". The suffix is
|
|
@@ -1028,137 +1174,6 @@ function assertValidClientJwks(jwks) {
|
|
|
1028
1174
|
});
|
|
1029
1175
|
}
|
|
1030
1176
|
//#endregion
|
|
1031
|
-
//#region src/client-metadata.ts
|
|
1032
|
-
const OAUTH_CLIENT_RECORD_FIELD_NAMES = Object.freeze(Object.keys({
|
|
1033
|
-
clientId: true,
|
|
1034
|
-
clientSecret: true,
|
|
1035
|
-
clientDiscoveryId: true,
|
|
1036
|
-
disabled: true,
|
|
1037
|
-
scopes: true,
|
|
1038
|
-
clientCredentialsScopes: true,
|
|
1039
|
-
userId: true,
|
|
1040
|
-
createdAt: true,
|
|
1041
|
-
updatedAt: true,
|
|
1042
|
-
expiresAt: true,
|
|
1043
|
-
name: true,
|
|
1044
|
-
uri: true,
|
|
1045
|
-
icon: true,
|
|
1046
|
-
contacts: true,
|
|
1047
|
-
tos: true,
|
|
1048
|
-
policy: true,
|
|
1049
|
-
softwareId: true,
|
|
1050
|
-
softwareVersion: true,
|
|
1051
|
-
softwareStatement: true,
|
|
1052
|
-
redirectUris: true,
|
|
1053
|
-
postLogoutRedirectUris: true,
|
|
1054
|
-
backchannelLogoutUri: true,
|
|
1055
|
-
backchannelLogoutSessionRequired: true,
|
|
1056
|
-
tokenEndpointAuthMethod: true,
|
|
1057
|
-
grantTypes: true,
|
|
1058
|
-
responseTypes: true,
|
|
1059
|
-
applicationType: true,
|
|
1060
|
-
jwks: true,
|
|
1061
|
-
jwksUri: true,
|
|
1062
|
-
requirePKCE: true,
|
|
1063
|
-
dpopBoundAccessTokens: true,
|
|
1064
|
-
skipConsent: true,
|
|
1065
|
-
enableEndSession: true,
|
|
1066
|
-
subjectType: true,
|
|
1067
|
-
referenceId: true,
|
|
1068
|
-
metadata: true
|
|
1069
|
-
}));
|
|
1070
|
-
/**
|
|
1071
|
-
* Canonical wire fields emitted by schemaToOAuth. They are mapped explicitly
|
|
1072
|
-
* and therefore must never also survive inside the opaque metadata envelope.
|
|
1073
|
-
*/
|
|
1074
|
-
const OAUTH_CLIENT_WIRE_FIELD_NAMES = [
|
|
1075
|
-
"client_id",
|
|
1076
|
-
"client_secret",
|
|
1077
|
-
"client_secret_expires_at",
|
|
1078
|
-
"scope",
|
|
1079
|
-
"user_id",
|
|
1080
|
-
"client_id_issued_at",
|
|
1081
|
-
"client_name",
|
|
1082
|
-
"client_uri",
|
|
1083
|
-
"logo_uri",
|
|
1084
|
-
"contacts",
|
|
1085
|
-
"tos_uri",
|
|
1086
|
-
"policy_uri",
|
|
1087
|
-
"jwks",
|
|
1088
|
-
"jwks_uri",
|
|
1089
|
-
"software_id",
|
|
1090
|
-
"software_version",
|
|
1091
|
-
"software_statement",
|
|
1092
|
-
"redirect_uris",
|
|
1093
|
-
"post_logout_redirect_uris",
|
|
1094
|
-
"backchannel_logout_uri",
|
|
1095
|
-
"backchannel_logout_session_required",
|
|
1096
|
-
"token_endpoint_auth_method",
|
|
1097
|
-
"grant_types",
|
|
1098
|
-
"response_types",
|
|
1099
|
-
"application_type",
|
|
1100
|
-
"disabled",
|
|
1101
|
-
"skip_consent",
|
|
1102
|
-
"enable_end_session",
|
|
1103
|
-
"require_pkce",
|
|
1104
|
-
"dpop_bound_access_tokens",
|
|
1105
|
-
"subject_type",
|
|
1106
|
-
"reference_id"
|
|
1107
|
-
];
|
|
1108
|
-
const OPAQUE_METADATA_RESERVED_FIELDS = new Set([
|
|
1109
|
-
...OAUTH_CLIENT_RECORD_FIELD_NAMES,
|
|
1110
|
-
...OAUTH_CLIENT_WIRE_FIELD_NAMES,
|
|
1111
|
-
"public",
|
|
1112
|
-
"type",
|
|
1113
|
-
"resources",
|
|
1114
|
-
"client_credentials_scopes"
|
|
1115
|
-
]);
|
|
1116
|
-
const CIMD_FORBIDDEN_SERVER_FIELD_NAMES = new Set([
|
|
1117
|
-
"disabled",
|
|
1118
|
-
"client_secret",
|
|
1119
|
-
"client_secret_expires_at",
|
|
1120
|
-
"client_id_issued_at",
|
|
1121
|
-
"skip_consent",
|
|
1122
|
-
"enable_end_session",
|
|
1123
|
-
"require_pkce",
|
|
1124
|
-
"reference_id",
|
|
1125
|
-
"user_id",
|
|
1126
|
-
"resources",
|
|
1127
|
-
"clientSecret",
|
|
1128
|
-
"clientDiscoveryId",
|
|
1129
|
-
"skipConsent",
|
|
1130
|
-
"enableEndSession",
|
|
1131
|
-
"requirePKCE",
|
|
1132
|
-
"referenceId",
|
|
1133
|
-
"userId",
|
|
1134
|
-
"clientId",
|
|
1135
|
-
"applicationType",
|
|
1136
|
-
"tokenEndpointAuthMethod",
|
|
1137
|
-
"redirectUris",
|
|
1138
|
-
"postLogoutRedirectUris",
|
|
1139
|
-
"grantTypes",
|
|
1140
|
-
"responseTypes",
|
|
1141
|
-
"scopes",
|
|
1142
|
-
"expiresAt",
|
|
1143
|
-
"createdAt",
|
|
1144
|
-
"updatedAt",
|
|
1145
|
-
"softwareId",
|
|
1146
|
-
"softwareVersion",
|
|
1147
|
-
"softwareStatement",
|
|
1148
|
-
"backchannelLogoutUri",
|
|
1149
|
-
"backchannelLogoutSessionRequired",
|
|
1150
|
-
"jwksUri",
|
|
1151
|
-
"dpopBoundAccessTokens",
|
|
1152
|
-
"subjectType"
|
|
1153
|
-
]);
|
|
1154
|
-
function isForbiddenCimdClientMetadataField(field) {
|
|
1155
|
-
return CIMD_FORBIDDEN_SERVER_FIELD_NAMES.has(field);
|
|
1156
|
-
}
|
|
1157
|
-
function stripReservedOAuthClientMetadataExtensions(metadata) {
|
|
1158
|
-
if (!metadata) return void 0;
|
|
1159
|
-
return Object.fromEntries(Object.entries(metadata).filter(([field]) => !OPAQUE_METADATA_RESERVED_FIELDS.has(field)));
|
|
1160
|
-
}
|
|
1161
|
-
//#endregion
|
|
1162
1177
|
//#region src/oauthClient/privileges.ts
|
|
1163
1178
|
/**
|
|
1164
1179
|
* Authorizes a client action against the configured `clientPrivileges` hook.
|
|
@@ -1789,8 +1804,54 @@ function validateClientRedirectUri(redirectUri, applicationType) {
|
|
|
1789
1804
|
}
|
|
1790
1805
|
if (!isNativePrivateUseRedirectUri(url)) invalidRedirectUri(`native private-use redirect URI schemes must not use a reserved scheme; they must be an authority-free reverse-domain URI or a custom-scheme URI with a host and a path: ${redirectUri}`);
|
|
1791
1806
|
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Rejects a URI the server will POST to unless it is an absolute https URL
|
|
1809
|
+
* with no fragment or credentials on a publicly routable host. Back-channel
|
|
1810
|
+
* endpoints use it as an SSRF guard, since the server, not the browser, calls
|
|
1811
|
+
* them.
|
|
1812
|
+
*/
|
|
1813
|
+
function assertPublicRoutableHttpsUri(field, uri) {
|
|
1814
|
+
const reject = (description) => {
|
|
1815
|
+
throw new APIError("BAD_REQUEST", {
|
|
1816
|
+
error: "invalid_client_metadata",
|
|
1817
|
+
error_description: `${field} ${description}`
|
|
1818
|
+
});
|
|
1819
|
+
};
|
|
1820
|
+
let url;
|
|
1821
|
+
try {
|
|
1822
|
+
url = new URL(uri);
|
|
1823
|
+
} catch {
|
|
1824
|
+
return reject("must be an absolute URL");
|
|
1825
|
+
}
|
|
1826
|
+
if (uri.includes("#")) reject("must not include a fragment component");
|
|
1827
|
+
if (url.protocol !== "https:") reject("must use https");
|
|
1828
|
+
if (url.username || url.password) reject("must not contain credentials");
|
|
1829
|
+
if (isPrivateHostname(url.hostname)) reject("must not point to a private or reserved address");
|
|
1830
|
+
}
|
|
1831
|
+
const REGISTRATION_REQUEST_FIELDS = new Set(Object.keys(clientRegistrationRequestSchema.shape));
|
|
1832
|
+
/**
|
|
1833
|
+
* Splits a dynamic registration request into its core fields and the fields
|
|
1834
|
+
* extensions define, which are stored in the client's `metadata`. Any other
|
|
1835
|
+
* unrecognized field is ignored (RFC 7591 §2), so a registrant cannot set
|
|
1836
|
+
* server-only fields or arbitrary `metadata`.
|
|
1837
|
+
*/
|
|
1838
|
+
function splitRegistrationRequest(input, opts) {
|
|
1839
|
+
return {
|
|
1840
|
+
body: Object.fromEntries(Object.entries(input).filter(([field]) => REGISTRATION_REQUEST_FIELDS.has(field))),
|
|
1841
|
+
metadata: pickExtensionClientMetadata(input, opts)
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* The top-level fields of a registration that extensions declare, which are
|
|
1846
|
+
* stored in the client's `metadata`.
|
|
1847
|
+
*/
|
|
1848
|
+
function pickExtensionClientMetadata(input, opts) {
|
|
1849
|
+
const extensionFields = getExtensionClientMetadataFields(opts);
|
|
1850
|
+
const metadata = Object.fromEntries(Object.entries(input).filter(([field]) => extensionFields.has(field)));
|
|
1851
|
+
return Object.keys(metadata).length ? metadata : void 0;
|
|
1852
|
+
}
|
|
1792
1853
|
async function registerEndpoint(ctx, opts) {
|
|
1793
|
-
const body = ctx.body;
|
|
1854
|
+
const { body, metadata } = splitRegistrationRequest(ctx.body, opts);
|
|
1794
1855
|
if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
|
|
1795
1856
|
error: "access_denied",
|
|
1796
1857
|
error_description: "Client registration is disabled"
|
|
@@ -1804,13 +1865,13 @@ async function registerEndpoint(ctx, opts) {
|
|
|
1804
1865
|
});
|
|
1805
1866
|
const grantTypes = body.grant_types?.length ? resolveMutuallySupportedGrantTypes(opts, body.grant_types) : body.grant_types;
|
|
1806
1867
|
const droppedAuthorizationCode = body.grant_types?.includes("authorization_code") && !grantTypes?.includes("authorization_code");
|
|
1807
|
-
const
|
|
1868
|
+
const registrationMetadata = {
|
|
1808
1869
|
...body,
|
|
1809
1870
|
grant_types: grantTypes,
|
|
1810
1871
|
response_types: droppedAuthorizationCode ? void 0 : body.response_types
|
|
1811
1872
|
};
|
|
1812
1873
|
if (!session && !isTokenAuthorized) {
|
|
1813
|
-
if (
|
|
1874
|
+
if (registrationMetadata.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
|
|
1814
1875
|
error: "invalid_client_metadata",
|
|
1815
1876
|
error_description: "client_credentials grant requires authenticated registration"
|
|
1816
1877
|
});
|
|
@@ -1822,7 +1883,10 @@ async function registerEndpoint(ctx, opts) {
|
|
|
1822
1883
|
session: session.session
|
|
1823
1884
|
}) : void 0);
|
|
1824
1885
|
const response = await createOAuthClientRegistration(ctx, opts, {
|
|
1825
|
-
metadata
|
|
1886
|
+
metadata: {
|
|
1887
|
+
...registrationMetadata,
|
|
1888
|
+
metadata
|
|
1889
|
+
},
|
|
1826
1890
|
registrationSource: "dynamic",
|
|
1827
1891
|
userId: referenceId ? void 0 : session?.session.userId,
|
|
1828
1892
|
referenceId,
|
|
@@ -1953,32 +2017,40 @@ async function checkOAuthClient(client, opts, settings) {
|
|
|
1953
2017
|
error: "invalid_client_metadata",
|
|
1954
2018
|
error_description: "backchannel_logout_uri requires the jwt plugin (disableJwtPlugin must be false)"
|
|
1955
2019
|
});
|
|
2020
|
+
assertPublicRoutableHttpsUri("backchannel_logout_uri", clientWithDefaults.backchannel_logout_uri);
|
|
2021
|
+
}
|
|
2022
|
+
if (clientWithDefaults.frontchannel_logout_uri !== void 0) {
|
|
1956
2023
|
let url;
|
|
1957
2024
|
try {
|
|
1958
|
-
url = new URL(clientWithDefaults.
|
|
2025
|
+
url = new URL(clientWithDefaults.frontchannel_logout_uri);
|
|
1959
2026
|
} catch {
|
|
1960
2027
|
throw new APIError("BAD_REQUEST", {
|
|
1961
2028
|
error: "invalid_client_metadata",
|
|
1962
|
-
error_description: "
|
|
2029
|
+
error_description: "frontchannel_logout_uri must be an absolute URL"
|
|
1963
2030
|
});
|
|
1964
2031
|
}
|
|
1965
|
-
if (clientWithDefaults.
|
|
2032
|
+
if (clientWithDefaults.frontchannel_logout_uri.includes("#")) throw new APIError("BAD_REQUEST", {
|
|
1966
2033
|
error: "invalid_client_metadata",
|
|
1967
|
-
error_description: "
|
|
1968
|
-
});
|
|
1969
|
-
if (url.protocol !== "https:") throw new APIError("BAD_REQUEST", {
|
|
1970
|
-
error: "invalid_client_metadata",
|
|
1971
|
-
error_description: "backchannel_logout_uri must use https"
|
|
2034
|
+
error_description: "frontchannel_logout_uri must not include a fragment component"
|
|
1972
2035
|
});
|
|
1973
2036
|
if (url.username || url.password) throw new APIError("BAD_REQUEST", {
|
|
1974
2037
|
error: "invalid_client_metadata",
|
|
1975
|
-
error_description: "
|
|
2038
|
+
error_description: "frontchannel_logout_uri must not contain credentials"
|
|
1976
2039
|
});
|
|
1977
|
-
|
|
2040
|
+
const isHttpOrHttps = url.protocol === "https:" || url.protocol === "http:";
|
|
2041
|
+
const sharesRedirectOrigin = (clientWithDefaults.redirect_uris ?? []).some((redirectUri) => {
|
|
2042
|
+
try {
|
|
2043
|
+
return new URL(redirectUri).origin === url.origin;
|
|
2044
|
+
} catch {
|
|
2045
|
+
return false;
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
if (!isHttpOrHttps || !sharesRedirectOrigin) throw new APIError("BAD_REQUEST", {
|
|
1978
2049
|
error: "invalid_client_metadata",
|
|
1979
|
-
error_description: "
|
|
2050
|
+
error_description: "frontchannel_logout_uri must use the scheme, host, and port of a registered redirect_uri"
|
|
1980
2051
|
});
|
|
1981
2052
|
}
|
|
2053
|
+
if (!settings?.skipExtensionValidation) await validateExtensionClientMetadata(opts, clientWithDefaults, client.metadata);
|
|
1982
2054
|
}
|
|
1983
2055
|
const CLIENT_REGISTRATION_COLLISION = Symbol("client-registration-collision");
|
|
1984
2056
|
function isClientRegistrationCollision(error) {
|
|
@@ -2005,7 +2077,8 @@ async function persistOAuthClientRegistration(ctx, opts, input) {
|
|
|
2005
2077
|
const isExtensionAuthMethod = isExtensionTokenEndpointAuthMethod(opts, body.token_endpoint_auth_method);
|
|
2006
2078
|
await checkOAuthClient(body, opts, {
|
|
2007
2079
|
registrationSource: input.registrationSource,
|
|
2008
|
-
ctx
|
|
2080
|
+
ctx,
|
|
2081
|
+
skipExtensionValidation: input.registrationSource === "clientMetadataDocument" && Boolean(input.existingClient)
|
|
2009
2082
|
});
|
|
2010
2083
|
const clientId = input.clientId ?? opts.generateClientId?.() ?? generateRandomString(32, "a-z", "A-Z");
|
|
2011
2084
|
const clientSecret = isPublic || isPrivateKeyJwt || isExtensionAuthMethod ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
|
|
@@ -2035,6 +2108,7 @@ async function persistOAuthClientRegistration(ctx, opts, input) {
|
|
|
2035
2108
|
});
|
|
2036
2109
|
schema.clientCredentialsScopes = input.registrationSource === "clientMetadataDocument" ? input.existingClient?.clientCredentialsScopes ?? [] : input.registrationSource === "managed" ? input.clientCredentialsScopes ?? [] : [];
|
|
2037
2110
|
schema.clientDiscoveryId = input.registrationSource === "clientMetadataDocument" ? input.clientDiscoveryId : null;
|
|
2111
|
+
schema.sectorIdentifier = deriveSectorIdentifier(clientId, body.redirect_uris);
|
|
2038
2112
|
if (input.registrationSource === "clientMetadataDocument" && body.application_type === void 0) schema.applicationType = null;
|
|
2039
2113
|
const requestedResources = input.registrationSource === "clientMetadataDocument" ? [] : input.requestedResources ?? [];
|
|
2040
2114
|
if (input.registrationSource === "dynamic") assertDynamicRegistrationResourcesAllowed(opts, requestedResources);
|
|
@@ -2048,6 +2122,19 @@ async function persistOAuthClientRegistration(ctx, opts, input) {
|
|
|
2048
2122
|
let storedClient;
|
|
2049
2123
|
let created = false;
|
|
2050
2124
|
if (existingClient) {
|
|
2125
|
+
let metadata;
|
|
2126
|
+
if (hasExtensionClientMetadata(opts)) {
|
|
2127
|
+
const extensionFields = getExtensionClientMetadataFields(opts);
|
|
2128
|
+
const merged = stripReservedOAuthClientMetadataExtensions(mergeDocumentExtensionMetadata(extensionFields, (await adapter.findOne({
|
|
2129
|
+
model: clientModel,
|
|
2130
|
+
where: [{
|
|
2131
|
+
field: "clientId",
|
|
2132
|
+
value: clientId
|
|
2133
|
+
}]
|
|
2134
|
+
}))?.metadata, body.metadata));
|
|
2135
|
+
await validateExtensionClientMetadata(opts, body, merged);
|
|
2136
|
+
if (extensionFields.size > 0) metadata = merged && Object.keys(merged).length > 0 ? JSON.stringify(merged) : null;
|
|
2137
|
+
}
|
|
2051
2138
|
const updatedClient = await adapter.update({
|
|
2052
2139
|
model: clientModel,
|
|
2053
2140
|
where: [{
|
|
@@ -2059,9 +2146,11 @@ async function persistOAuthClientRegistration(ctx, opts, input) {
|
|
|
2059
2146
|
}],
|
|
2060
2147
|
update: {
|
|
2061
2148
|
...schema,
|
|
2149
|
+
metadata,
|
|
2062
2150
|
disabled: existingClient.disabled,
|
|
2063
2151
|
skipConsent: existingClient.skipConsent,
|
|
2064
2152
|
enableEndSession: existingClient.enableEndSession,
|
|
2153
|
+
sectorIdentifier: existingClient.sectorIdentifier ? schema.sectorIdentifier : null,
|
|
2065
2154
|
createdAt: existingClient.createdAt,
|
|
2066
2155
|
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
2067
2156
|
}
|
|
@@ -2166,6 +2255,20 @@ function assertClientDiscoveryOwnership(client, clientDiscoveryId) {
|
|
|
2166
2255
|
});
|
|
2167
2256
|
}
|
|
2168
2257
|
/**
|
|
2258
|
+
* The `metadata` a metadata document registration stores. The document owns the
|
|
2259
|
+
* fields extensions declare, so a refresh replaces them, including removing
|
|
2260
|
+
* ones it no longer declares. Other stored keys, such as ones an admin set, are
|
|
2261
|
+
* kept.
|
|
2262
|
+
*/
|
|
2263
|
+
function mergeDocumentExtensionMetadata(extensionFields, storedMetadata, documentMetadata) {
|
|
2264
|
+
const owned = ([field]) => extensionFields.has(field);
|
|
2265
|
+
const metadata = {
|
|
2266
|
+
...Object.fromEntries(Object.entries(parseClientMetadata(storedMetadata) ?? {}).filter((entry) => !owned(entry))),
|
|
2267
|
+
...Object.fromEntries(Object.entries(documentMetadata ?? {}).filter(owned))
|
|
2268
|
+
};
|
|
2269
|
+
return Object.keys(metadata).length ? metadata : void 0;
|
|
2270
|
+
}
|
|
2271
|
+
/**
|
|
2169
2272
|
* First-party persistence seam for a validated Client ID Metadata Document.
|
|
2170
2273
|
*
|
|
2171
2274
|
* This operation owns both initial creation and atomic replacement. It never
|
|
@@ -2185,18 +2288,20 @@ async function registerClientMetadataDocument(ctx, opts, input) {
|
|
|
2185
2288
|
error: "invalid_client_metadata",
|
|
2186
2289
|
error_description: "Client ID Metadata Document clients cannot use a shared secret"
|
|
2187
2290
|
});
|
|
2188
|
-
const
|
|
2291
|
+
const toRegistrationInput = (existingClient) => ({
|
|
2189
2292
|
...input,
|
|
2293
|
+
existingClient,
|
|
2190
2294
|
metadata: {
|
|
2191
2295
|
...input.metadata,
|
|
2296
|
+
metadata: pickExtensionClientMetadata(input.metadata, opts),
|
|
2192
2297
|
token_endpoint_auth_method: input.metadata.token_endpoint_auth_method ?? "none",
|
|
2193
2298
|
client_secret: void 0,
|
|
2194
2299
|
client_secret_expires_at: void 0
|
|
2195
2300
|
},
|
|
2196
2301
|
registrationSource: "clientMetadataDocument"
|
|
2197
|
-
};
|
|
2302
|
+
});
|
|
2198
2303
|
try {
|
|
2199
|
-
return await persistOAuthClientRegistration(ctx, opts,
|
|
2304
|
+
return await persistOAuthClientRegistration(ctx, opts, toRegistrationInput(input.existingClient));
|
|
2200
2305
|
} catch (error) {
|
|
2201
2306
|
if (!isClientRegistrationCollision(error)) throw error;
|
|
2202
2307
|
const clientModel = opts.schema?.oauthClient?.modelName ?? "oauthClient";
|
|
@@ -2222,10 +2327,7 @@ async function registerClientMetadataDocument(ctx, opts, input) {
|
|
|
2222
2327
|
})) throw error.cause;
|
|
2223
2328
|
}
|
|
2224
2329
|
try {
|
|
2225
|
-
return await persistOAuthClientRegistration(ctx, opts,
|
|
2226
|
-
...registrationInput,
|
|
2227
|
-
existingClient: currentClient
|
|
2228
|
-
});
|
|
2330
|
+
return await persistOAuthClientRegistration(ctx, opts, toRegistrationInput(currentClient));
|
|
2229
2331
|
} catch (retryError) {
|
|
2230
2332
|
if (isClientRegistrationCollision(retryError)) throw retryError.cause;
|
|
2231
2333
|
throw retryError;
|
|
@@ -2274,7 +2376,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
|
2274
2376
|
return ctx.json(responseBody);
|
|
2275
2377
|
}
|
|
2276
2378
|
function oauthToSchema(input) {
|
|
2277
|
-
const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, backchannel_logout_uri: backchannelLogoutUri, backchannel_logout_session_required: backchannelLogoutSessionRequired, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, application_type: applicationType, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, dpop_bound_access_tokens: dpopBoundAccessTokens, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata } = input;
|
|
2379
|
+
const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, backchannel_logout_uri: backchannelLogoutUri, backchannel_logout_session_required: backchannelLogoutSessionRequired, frontchannel_logout_uri: frontchannelLogoutUri, frontchannel_logout_session_required: frontchannelLogoutSessionRequired, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, application_type: applicationType, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, dpop_bound_access_tokens: dpopBoundAccessTokens, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata } = input;
|
|
2278
2380
|
const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(Number(_expiresAt) * 1e3) : void 0;
|
|
2279
2381
|
const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
|
|
2280
2382
|
const scopes = _scope?.split(" ");
|
|
@@ -2301,6 +2403,8 @@ function oauthToSchema(input) {
|
|
|
2301
2403
|
postLogoutRedirectUris,
|
|
2302
2404
|
backchannelLogoutUri,
|
|
2303
2405
|
backchannelLogoutSessionRequired,
|
|
2406
|
+
frontchannelLogoutUri,
|
|
2407
|
+
frontchannelLogoutSessionRequired,
|
|
2304
2408
|
tokenEndpointAuthMethod,
|
|
2305
2409
|
grantTypes,
|
|
2306
2410
|
responseTypes,
|
|
@@ -2323,7 +2427,7 @@ function oauthToSchema(input) {
|
|
|
2323
2427
|
* @returns
|
|
2324
2428
|
*/
|
|
2325
2429
|
function schemaToOAuth(input) {
|
|
2326
|
-
const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, backchannelLogoutUri, backchannelLogoutSessionRequired, tokenEndpointAuthMethod, grantTypes, responseTypes, applicationType, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, dpopBoundAccessTokens, subjectType, referenceId, metadata } = input;
|
|
2430
|
+
const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, backchannelLogoutUri, backchannelLogoutSessionRequired, frontchannelLogoutUri, frontchannelLogoutSessionRequired, tokenEndpointAuthMethod, grantTypes, responseTypes, applicationType, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, dpopBoundAccessTokens, subjectType, referenceId, metadata } = input;
|
|
2327
2431
|
const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
|
|
2328
2432
|
const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
|
|
2329
2433
|
const _scopes = scopes?.join(" ");
|
|
@@ -2350,6 +2454,8 @@ function schemaToOAuth(input) {
|
|
|
2350
2454
|
post_logout_redirect_uris: postLogoutRedirectUris ?? void 0,
|
|
2351
2455
|
backchannel_logout_uri: backchannelLogoutUri ?? void 0,
|
|
2352
2456
|
backchannel_logout_session_required: backchannelLogoutSessionRequired ?? void 0,
|
|
2457
|
+
frontchannel_logout_uri: frontchannelLogoutUri ?? void 0,
|
|
2458
|
+
frontchannel_logout_session_required: frontchannelLogoutSessionRequired ?? void 0,
|
|
2353
2459
|
token_endpoint_auth_method: tokenEndpointAuthMethod ?? void 0,
|
|
2354
2460
|
grant_types: grantTypes ?? void 0,
|
|
2355
2461
|
response_types: responseTypes ?? void 0,
|
|
@@ -2477,12 +2583,20 @@ async function updateClientEndpoint(ctx, opts, settings) {
|
|
|
2477
2583
|
error_description: "client not found",
|
|
2478
2584
|
error: "not_found"
|
|
2479
2585
|
});
|
|
2480
|
-
const { client_credentials_scopes: rawClientCredentialsScopes, ...
|
|
2586
|
+
const { client_credentials_scopes: rawClientCredentialsScopes, backchannel_logout_uri: backchannelLogoutUri, frontchannel_logout_uri: frontchannelLogoutUri, ...otherUpdates } = ctx.body.update;
|
|
2587
|
+
const clearsBackchannelLogout = backchannelLogoutUri === null;
|
|
2588
|
+
const clearsFrontchannelLogout = frontchannelLogoutUri === null;
|
|
2589
|
+
const updates = {
|
|
2590
|
+
...otherUpdates,
|
|
2591
|
+
...backchannelLogoutUri != null ? { backchannel_logout_uri: backchannelLogoutUri } : {},
|
|
2592
|
+
...frontchannelLogoutUri != null ? { frontchannel_logout_uri: frontchannelLogoutUri } : {}
|
|
2593
|
+
};
|
|
2594
|
+
const hasUpdates = Object.keys(updates).length > 0 || clearsBackchannelLogout || clearsFrontchannelLogout;
|
|
2481
2595
|
const ownsClient = client.userId ? client.userId === session.user.id : client.referenceId && opts.clientReference ? client.referenceId === await opts.clientReference(session) : false;
|
|
2482
|
-
const isCrossOwnerScopeConfiguration = !ownsClient && settings?.admin && rawClientCredentialsScopes !== void 0 &&
|
|
2596
|
+
const isCrossOwnerScopeConfiguration = !ownsClient && settings?.admin && rawClientCredentialsScopes !== void 0 && !hasUpdates;
|
|
2483
2597
|
if (!ownsClient && !isCrossOwnerScopeConfiguration) throw new APIError("UNAUTHORIZED");
|
|
2484
2598
|
if (isCrossOwnerScopeConfiguration) await assertClientPrivileges(ctx, session, opts, "configure-client-credentials-scopes", clientId);
|
|
2485
|
-
if (
|
|
2599
|
+
if (!hasUpdates && rawClientCredentialsScopes === void 0) {
|
|
2486
2600
|
const res = schemaToOAuth(client);
|
|
2487
2601
|
res.client_secret = void 0;
|
|
2488
2602
|
if (settings?.admin) return {
|
|
@@ -2498,11 +2612,34 @@ async function updateClientEndpoint(ctx, opts, settings) {
|
|
|
2498
2612
|
validateClientCredentialsScopes(clientCredentialsScopes, finalGrantTypes, finalTokenEndpointAuthMethod, opts);
|
|
2499
2613
|
if (clientCredentialsScopes.length > 0 && !isCrossOwnerScopeConfiguration) await assertClientPrivileges(ctx, session, opts, "configure-client-credentials-scopes", clientId);
|
|
2500
2614
|
}
|
|
2501
|
-
|
|
2502
|
-
...schemaToOAuth(
|
|
2503
|
-
|
|
2504
|
-
|
|
2615
|
+
const updatedMetadata = {
|
|
2616
|
+
...schemaToOAuth({
|
|
2617
|
+
...client,
|
|
2618
|
+
metadata: void 0
|
|
2619
|
+
}),
|
|
2620
|
+
...updates,
|
|
2621
|
+
metadata: updates.metadata ?? parseClientMetadata(client.metadata)
|
|
2622
|
+
};
|
|
2623
|
+
if (clearsBackchannelLogout) {
|
|
2624
|
+
updatedMetadata.backchannel_logout_uri = void 0;
|
|
2625
|
+
updatedMetadata.backchannel_logout_session_required = void 0;
|
|
2626
|
+
}
|
|
2627
|
+
if (clearsFrontchannelLogout) {
|
|
2628
|
+
updatedMetadata.frontchannel_logout_uri = void 0;
|
|
2629
|
+
updatedMetadata.frontchannel_logout_session_required = void 0;
|
|
2630
|
+
}
|
|
2631
|
+
await checkOAuthClient(updatedMetadata, opts, { ctx });
|
|
2505
2632
|
const schemaUpdates = { ...oauthToSchema(updates) };
|
|
2633
|
+
if (updates.redirect_uris && client.sectorIdentifier) schemaUpdates.sectorIdentifier = deriveSectorIdentifier(client.clientId, updates.redirect_uris);
|
|
2634
|
+
if (clearsBackchannelLogout) {
|
|
2635
|
+
schemaUpdates.backchannelLogoutUri = null;
|
|
2636
|
+
schemaUpdates.backchannelLogoutSessionRequired = null;
|
|
2637
|
+
}
|
|
2638
|
+
if (clearsFrontchannelLogout) {
|
|
2639
|
+
schemaUpdates.frontchannelLogoutUri = null;
|
|
2640
|
+
schemaUpdates.frontchannelLogoutSessionRequired = null;
|
|
2641
|
+
}
|
|
2642
|
+
if (updates.metadata !== void 0 && schemaUpdates.metadata === void 0) schemaUpdates.metadata = null;
|
|
2506
2643
|
if (!finalGrantTypes.includes("client_credentials") || finalTokenEndpointAuthMethod === "none") schemaUpdates.clientCredentialsScopes = [];
|
|
2507
2644
|
else if (clientCredentialsScopes !== void 0) schemaUpdates.clientCredentialsScopes = clientCredentialsScopes;
|
|
2508
2645
|
if (updates.token_endpoint_auth_method) if (updates.token_endpoint_auth_method === "private_key_jwt") schemaUpdates.clientSecret = null;
|
|
@@ -2648,6 +2785,8 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
|
|
|
2648
2785
|
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
2649
2786
|
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
2650
2787
|
backchannel_logout_session_required: z.boolean().optional(),
|
|
2788
|
+
frontchannel_logout_uri: SafeUrlSchema.optional(),
|
|
2789
|
+
frontchannel_logout_session_required: z.boolean().optional(),
|
|
2651
2790
|
token_endpoint_auth_method: tokenEndpointAuthMethodSchema.optional(),
|
|
2652
2791
|
application_type: z.enum(["web", "native"]).optional(),
|
|
2653
2792
|
jwks: clientJwksSchema.optional(),
|
|
@@ -2821,6 +2960,8 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
|
|
|
2821
2960
|
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
2822
2961
|
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
2823
2962
|
backchannel_logout_session_required: z.boolean().optional(),
|
|
2963
|
+
frontchannel_logout_uri: SafeUrlSchema.optional(),
|
|
2964
|
+
frontchannel_logout_session_required: z.boolean().optional(),
|
|
2824
2965
|
token_endpoint_auth_method: tokenEndpointAuthMethodSchema.optional(),
|
|
2825
2966
|
application_type: z.enum(["web", "native"]).optional(),
|
|
2826
2967
|
jwks: clientJwksSchema.optional(),
|
|
@@ -3010,8 +3151,10 @@ const adminUpdateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/updat
|
|
|
3010
3151
|
software_version: z.string().optional(),
|
|
3011
3152
|
software_statement: z.string().optional(),
|
|
3012
3153
|
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
3013
|
-
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
3154
|
+
backchannel_logout_uri: SafeUrlSchema.nullable().optional(),
|
|
3014
3155
|
backchannel_logout_session_required: z.boolean().optional(),
|
|
3156
|
+
frontchannel_logout_uri: SafeUrlSchema.nullable().optional(),
|
|
3157
|
+
frontchannel_logout_session_required: z.boolean().optional(),
|
|
3015
3158
|
application_type: z.enum(["web", "native"]).optional(),
|
|
3016
3159
|
grant_types: grantTypesSchema.optional(),
|
|
3017
3160
|
client_credentials_scopes: z.array(z.string().trim().min(1)).optional(),
|
|
@@ -3049,8 +3192,10 @@ const updateOAuthClient = (opts) => createAuthEndpoint("/oauth2/update-client",
|
|
|
3049
3192
|
software_version: z.string().optional(),
|
|
3050
3193
|
software_statement: z.string().optional(),
|
|
3051
3194
|
post_logout_redirect_uris: z.array(SafeUrlSchema).min(1).optional(),
|
|
3052
|
-
backchannel_logout_uri: SafeUrlSchema.optional(),
|
|
3195
|
+
backchannel_logout_uri: SafeUrlSchema.nullable().optional(),
|
|
3053
3196
|
backchannel_logout_session_required: z.boolean().optional(),
|
|
3197
|
+
frontchannel_logout_uri: SafeUrlSchema.nullable().optional(),
|
|
3198
|
+
frontchannel_logout_session_required: z.boolean().optional(),
|
|
3054
3199
|
application_type: z.enum(["web", "native"]).optional(),
|
|
3055
3200
|
grant_types: grantTypesSchema.optional(),
|
|
3056
3201
|
response_types: z.array(z.enum(["code"])).optional()
|
|
@@ -3540,113 +3685,47 @@ const adminUnlinkClientResource = (opts) => createAuthEndpoint("/admin/oauth2/re
|
|
|
3540
3685
|
//#endregion
|
|
3541
3686
|
//#region src/revoke.ts
|
|
3542
3687
|
/**
|
|
3543
|
-
*
|
|
3544
|
-
* Revocation follows RFC7009
|
|
3688
|
+
* Revocation follows RFC 7009
|
|
3545
3689
|
* https://datatracker.ietf.org/doc/html/rfc7009
|
|
3546
|
-
*
|
|
3547
|
-
*
|
|
3690
|
+
*
|
|
3691
|
+
* A token this server cannot find is already unusable, so it resolves as a
|
|
3692
|
+
* successful no-op (§2.2). Only explicit protocol errors surface.
|
|
3548
3693
|
*/
|
|
3549
3694
|
/**
|
|
3550
|
-
* Revokes a JWT access token against the configured JWKs.
|
|
3551
|
-
*
|
|
3552
3695
|
* A JWT access token is self-contained and never stored, so there is nothing to
|
|
3553
3696
|
* delete. Once the token is confirmed to be a valid JWT for this server, the
|
|
3554
3697
|
* endpoint reports `unsupported_token_type` (RFC 7009 §2.2.1) instead of a
|
|
3555
3698
|
* silent success, so callers can tell that no server-side revocation happened.
|
|
3556
|
-
*
|
|
3557
|
-
*
|
|
3558
|
-
*
|
|
3559
|
-
*
|
|
3699
|
+
* A JWT with an audience rejected by the OAuth resource model is already
|
|
3700
|
+
* inactive and resolves as a successful no-op. Session-bound tokens (carrying
|
|
3701
|
+
* `sid`) are cut off early by the session-liveness check in introspection and
|
|
3702
|
+
* userinfo.
|
|
3560
3703
|
*/
|
|
3561
|
-
async function revokeJwtAccessToken(ctx, opts,
|
|
3562
|
-
if (opts.
|
|
3563
|
-
error_description: "JWT access tokens are disabled",
|
|
3564
|
-
error: "invalid_request"
|
|
3565
|
-
});
|
|
3566
|
-
const jwtPlugin = getJwtPlugin(ctx.context);
|
|
3567
|
-
const jwtPluginOptions = jwtPlugin.options;
|
|
3568
|
-
let jwtPayload;
|
|
3569
|
-
try {
|
|
3570
|
-
jwtPayload = (await jwtVerify(token, createLocalJWKSet(await getJwks(token, {
|
|
3571
|
-
jwksFetch: jwtPluginOptions?.jwks?.remoteUrl ? jwtPluginOptions.jwks.remoteUrl : async () => {
|
|
3572
|
-
return (await jwtPlugin.endpoints.getJwks(ctx))?.response;
|
|
3573
|
-
},
|
|
3574
|
-
jwksCacheKey: jwtPlugin
|
|
3575
|
-
})), { issuer: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL })).payload;
|
|
3576
|
-
} catch (error) {
|
|
3577
|
-
if (error instanceof errors.JWTExpired || error instanceof errors.JWTInvalid) return null;
|
|
3578
|
-
if (error instanceof TypeError || isInvalidJwtError(error)) throw new APIError$1("BAD_REQUEST", {
|
|
3579
|
-
error_description: "invalid JWT signature",
|
|
3580
|
-
error: "invalid_request"
|
|
3581
|
-
});
|
|
3582
|
-
throw error;
|
|
3583
|
-
}
|
|
3584
|
-
const userInfoAudience = `${ctx.context.baseURL}/oauth2/userinfo`;
|
|
3585
|
-
if (!jwtPayload.azp || !await isAudienceClaimAllowed(ctx, opts, jwtPayload.aud, [userInfoAudience])) return null;
|
|
3704
|
+
async function revokeJwtAccessToken(ctx, opts, payload) {
|
|
3705
|
+
if (!await isAudienceClaimAllowed(ctx, opts, payload.aud, [userInfoResource(ctx.context.baseURL)])) return null;
|
|
3586
3706
|
throw new APIError$1("BAD_REQUEST", {
|
|
3587
3707
|
error_description: "JWT access tokens are self-contained and cannot be revoked server-side",
|
|
3588
3708
|
error: "unsupported_token_type"
|
|
3589
3709
|
});
|
|
3590
3710
|
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
*/
|
|
3594
|
-
async function revokeOpaqueAccessToken(ctx, opts, token, clientId) {
|
|
3595
|
-
let tokenValue = token;
|
|
3596
|
-
if (opts.prefix?.opaqueAccessToken) if (tokenValue.startsWith(opts.prefix.opaqueAccessToken)) tokenValue = tokenValue.replace(opts.prefix.opaqueAccessToken, "");
|
|
3597
|
-
else throw new APIError$1("BAD_REQUEST", {
|
|
3598
|
-
error_description: "opaque access token not found",
|
|
3599
|
-
error: "invalid_request"
|
|
3600
|
-
});
|
|
3601
|
-
const accessToken = await ctx.context.adapter.findOne({
|
|
3602
|
-
model: "oauthAccessToken",
|
|
3603
|
-
where: [{
|
|
3604
|
-
field: "token",
|
|
3605
|
-
value: await getStoredToken(opts.storeTokens, tokenValue, "access_token")
|
|
3606
|
-
}]
|
|
3607
|
-
});
|
|
3608
|
-
if (!accessToken) throw new APIError$1("BAD_REQUEST", {
|
|
3609
|
-
error_description: "opaque access token not found",
|
|
3610
|
-
error: "invalid_request"
|
|
3611
|
-
});
|
|
3612
|
-
if (!accessToken.clientId || accessToken.clientId !== clientId) return null;
|
|
3613
|
-
accessToken.id ? await ctx.context.adapter.delete({
|
|
3711
|
+
async function revokeOpaqueAccessToken(ctx, accessToken) {
|
|
3712
|
+
await ctx.context.adapter.delete({
|
|
3614
3713
|
model: "oauthAccessToken",
|
|
3615
|
-
where: [{
|
|
3714
|
+
where: accessToken.id ? [{
|
|
3616
3715
|
field: "id",
|
|
3617
3716
|
value: accessToken.id
|
|
3618
|
-
}]
|
|
3619
|
-
}) : await ctx.context.adapter.delete({
|
|
3620
|
-
model: "oauthAccessToken",
|
|
3621
|
-
where: [{
|
|
3717
|
+
}] : [{
|
|
3622
3718
|
field: "token",
|
|
3623
3719
|
value: accessToken.token
|
|
3624
3720
|
}]
|
|
3625
3721
|
});
|
|
3626
3722
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
*/
|
|
3630
|
-
async function revokeRefreshToken(ctx, opts, token, clientId) {
|
|
3631
|
-
const refreshToken = await ctx.context.adapter.findOne({
|
|
3632
|
-
model: "oauthRefreshToken",
|
|
3633
|
-
where: [{
|
|
3634
|
-
field: "token",
|
|
3635
|
-
value: await getStoredToken(opts.storeTokens, token, "refresh_token")
|
|
3636
|
-
}]
|
|
3637
|
-
});
|
|
3638
|
-
if (!refreshToken) throw new APIError$1("BAD_REQUEST", {
|
|
3639
|
-
error_description: "token not found",
|
|
3640
|
-
error: "invalid_request"
|
|
3641
|
-
});
|
|
3723
|
+
async function revokeRefreshToken(ctx, refreshToken, clientId) {
|
|
3724
|
+
const { userId } = refreshToken;
|
|
3642
3725
|
if (refreshToken.revoked) {
|
|
3643
|
-
await invalidateRefreshFamily(ctx, clientId,
|
|
3644
|
-
|
|
3645
|
-
error_description: "refresh token revoked",
|
|
3646
|
-
error: "invalid_request"
|
|
3647
|
-
});
|
|
3726
|
+
await invalidateRefreshFamily(ctx, clientId, userId);
|
|
3727
|
+
return;
|
|
3648
3728
|
}
|
|
3649
|
-
if (!refreshToken.clientId || refreshToken.clientId !== clientId) return null;
|
|
3650
3729
|
const iat = Math.floor(Date.now() / 1e3);
|
|
3651
3730
|
if (!await ctx.context.adapter.incrementOne({
|
|
3652
3731
|
model: "oauthRefreshToken",
|
|
@@ -3661,11 +3740,8 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
|
|
|
3661
3740
|
increment: {},
|
|
3662
3741
|
set: { revoked: /* @__PURE__ */ new Date(iat * 1e3) }
|
|
3663
3742
|
})) {
|
|
3664
|
-
await invalidateRefreshFamily(ctx, clientId,
|
|
3665
|
-
|
|
3666
|
-
error_description: "refresh token revoked",
|
|
3667
|
-
error: "invalid_request"
|
|
3668
|
-
});
|
|
3743
|
+
await invalidateRefreshFamily(ctx, clientId, userId);
|
|
3744
|
+
return;
|
|
3669
3745
|
}
|
|
3670
3746
|
await ctx.context.adapter.deleteMany({
|
|
3671
3747
|
model: "oauthAccessToken",
|
|
@@ -3675,33 +3751,8 @@ async function revokeRefreshToken(ctx, opts, token, clientId) {
|
|
|
3675
3751
|
}]
|
|
3676
3752
|
});
|
|
3677
3753
|
}
|
|
3678
|
-
/**
|
|
3679
|
-
* We don't know the access token format so we try to validate it
|
|
3680
|
-
* as a JWT first, then as an opaque token.
|
|
3681
|
-
*/
|
|
3682
|
-
async function revokeAccessToken(ctx, opts, clientId, token) {
|
|
3683
|
-
try {
|
|
3684
|
-
return await revokeJwtAccessToken(ctx, opts, token);
|
|
3685
|
-
} catch (err) {
|
|
3686
|
-
if (err instanceof APIError$1) {
|
|
3687
|
-
if (err.body?.error === "unsupported_token_type") throw err;
|
|
3688
|
-
} else if (err instanceof Error) throw err;
|
|
3689
|
-
else throw new Error(err);
|
|
3690
|
-
}
|
|
3691
|
-
try {
|
|
3692
|
-
return await revokeOpaqueAccessToken(ctx, opts, token, clientId);
|
|
3693
|
-
} catch (err) {
|
|
3694
|
-
if (err instanceof APIError$1) {} else if (err instanceof Error) throw err;
|
|
3695
|
-
else throw new Error("Unknown error validating access token");
|
|
3696
|
-
}
|
|
3697
|
-
throw new APIError$1("BAD_REQUEST", {
|
|
3698
|
-
error_description: "Invalid access token",
|
|
3699
|
-
error: "invalid_request"
|
|
3700
|
-
});
|
|
3701
|
-
}
|
|
3702
3754
|
async function revokeEndpoint(ctx, opts) {
|
|
3703
3755
|
let { token, token_type_hint } = ctx.body;
|
|
3704
|
-
if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
|
|
3705
3756
|
const { clientId: client_id, clientSecret: client_secret, preVerified, authMethod } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
|
|
3706
3757
|
if (!client_id) throw new APIError$1("UNAUTHORIZED", {
|
|
3707
3758
|
error_description: "missing required credentials",
|
|
@@ -3714,38 +3765,23 @@ async function revokeEndpoint(ctx, opts) {
|
|
|
3714
3765
|
});
|
|
3715
3766
|
const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerified, void 0, authMethod);
|
|
3716
3767
|
try {
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
if (
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
if (token_type_hint === void 0 || token_type_hint === "refresh_token") try {
|
|
3726
|
-
return await revokeRefreshToken(ctx, opts, (await decodeRefreshToken(opts, token)).token, client.clientId);
|
|
3727
|
-
} catch (error) {
|
|
3728
|
-
if (error instanceof APIError$1) {
|
|
3729
|
-
if (token_type_hint === "refresh_token") throw error;
|
|
3730
|
-
} else if (error instanceof Error) throw error;
|
|
3731
|
-
else throw new Error(error);
|
|
3768
|
+
const found = await findToken(ctx, opts, token, token_type_hint);
|
|
3769
|
+
if (!found) return null;
|
|
3770
|
+
if ((found.type === "jwt" ? found.payload.azp : found.record.clientId) !== client.clientId) {
|
|
3771
|
+
if (client.tokenEndpointAuthMethod === "none") return null;
|
|
3772
|
+
throw new APIError$1("BAD_REQUEST", {
|
|
3773
|
+
error_description: "client is not authorized to revoke the presented token",
|
|
3774
|
+
error: "invalid_request"
|
|
3775
|
+
});
|
|
3732
3776
|
}
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3777
|
+
if (found.type === "jwt") return await revokeJwtAccessToken(ctx, opts, found.payload);
|
|
3778
|
+
if (found.type === "opaque") await revokeOpaqueAccessToken(ctx, found.record);
|
|
3779
|
+
else await revokeRefreshToken(ctx, found.record, client.clientId);
|
|
3780
|
+
return null;
|
|
3737
3781
|
} catch (error) {
|
|
3738
|
-
if (error instanceof APIError$1)
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
throw error;
|
|
3742
|
-
} else if (error instanceof Error) {
|
|
3743
|
-
logger.error("Introspection error:", error.message, error.stack);
|
|
3744
|
-
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
3745
|
-
} else {
|
|
3746
|
-
logger.error("Introspection error:", error);
|
|
3747
|
-
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
3748
|
-
}
|
|
3782
|
+
if (error instanceof APIError$1) throw error;
|
|
3783
|
+
logger.error("Revocation error:", error);
|
|
3784
|
+
throw new APIError$1("INTERNAL_SERVER_ERROR");
|
|
3749
3785
|
}
|
|
3750
3786
|
}
|
|
3751
3787
|
//#endregion
|
|
@@ -3784,6 +3820,10 @@ const schema = {
|
|
|
3784
3820
|
type: "string",
|
|
3785
3821
|
required: false
|
|
3786
3822
|
},
|
|
3823
|
+
sectorIdentifier: {
|
|
3824
|
+
type: "string",
|
|
3825
|
+
required: false
|
|
3826
|
+
},
|
|
3787
3827
|
scopes: {
|
|
3788
3828
|
type: "string[]",
|
|
3789
3829
|
required: false
|
|
@@ -3862,6 +3902,14 @@ const schema = {
|
|
|
3862
3902
|
type: "boolean",
|
|
3863
3903
|
required: false
|
|
3864
3904
|
},
|
|
3905
|
+
frontchannelLogoutUri: {
|
|
3906
|
+
type: "string",
|
|
3907
|
+
required: false
|
|
3908
|
+
},
|
|
3909
|
+
frontchannelLogoutSessionRequired: {
|
|
3910
|
+
type: "boolean",
|
|
3911
|
+
required: false
|
|
3912
|
+
},
|
|
3865
3913
|
tokenEndpointAuthMethod: {
|
|
3866
3914
|
type: "string",
|
|
3867
3915
|
required: false
|
|
@@ -4110,6 +4158,14 @@ const schema = {
|
|
|
4110
4158
|
type: "date",
|
|
4111
4159
|
required: false
|
|
4112
4160
|
},
|
|
4161
|
+
acr: {
|
|
4162
|
+
type: "string",
|
|
4163
|
+
required: false
|
|
4164
|
+
},
|
|
4165
|
+
amr: {
|
|
4166
|
+
type: "string[]",
|
|
4167
|
+
required: false
|
|
4168
|
+
},
|
|
4113
4169
|
confirmation: {
|
|
4114
4170
|
type: "json",
|
|
4115
4171
|
required: false
|
|
@@ -4290,7 +4346,7 @@ const rpInitiatedLogoutRequestSchema = z.object({
|
|
|
4290
4346
|
});
|
|
4291
4347
|
const oAuthState = defineRequestState(() => null);
|
|
4292
4348
|
const getOAuthProviderState = oAuthState.get;
|
|
4293
|
-
const
|
|
4349
|
+
const sessionLogoutPlansByContext = /* @__PURE__ */ new WeakMap();
|
|
4294
4350
|
const signedQueryIssuedAtMsKey = "signedQueryIssuedAtMs";
|
|
4295
4351
|
function getServerContextSignedQueryIssuedAt(value) {
|
|
4296
4352
|
const issuedAtMs = typeof value === "number" ? value : typeof value === "string" ? Number(value) : void 0;
|
|
@@ -4314,6 +4370,8 @@ const oauthProvider = (options) => {
|
|
|
4314
4370
|
if (clientRegistrationAllowedScopes) {
|
|
4315
4371
|
for (const sc of clientRegistrationAllowedScopes) if (!scopes.has(sc)) throw new BetterAuthError(`clientRegistrationAllowedScope ${sc} not found in scopes`);
|
|
4316
4372
|
}
|
|
4373
|
+
const acrValuesSupported = options.authenticationContext?.acrValuesSupported;
|
|
4374
|
+
if (acrValuesSupported && (acrValuesSupported.length === 0 || acrValuesSupported.includes(""))) throw new BetterAuthError("authenticationContext.acrValuesSupported must list at least one non-empty acr value");
|
|
4317
4375
|
for (const sc of options.advertisedMetadata?.scopes_supported ?? []) if (!scopes?.has(sc)) throw new BetterAuthError(`advertisedMetadata.scopes_supported ${sc} not found in scopes`);
|
|
4318
4376
|
const configuredResourceIdentifiers = new Set((options.resources ?? []).map((resource) => typeof resource === "string" ? resource : resource.identifier));
|
|
4319
4377
|
for (const [optionName, identifiers] of [["clientRegistrationDefaultResources", options.clientRegistrationDefaultResources], ["clientRegistrationAllowedResources", options.clientRegistrationAllowedResources]]) for (const identifier of identifiers ?? []) if (!configuredResourceIdentifiers.has(identifier)) throw new BetterAuthError(`${optionName} resource ${identifier} not found in resources`);
|
|
@@ -4405,12 +4463,19 @@ const oauthProvider = (options) => {
|
|
|
4405
4463
|
schema: { type: "string" },
|
|
4406
4464
|
description: "OAuth 2.1 response type (e.g., 'code')"
|
|
4407
4465
|
},
|
|
4466
|
+
{
|
|
4467
|
+
name: "user_code",
|
|
4468
|
+
in: "query",
|
|
4469
|
+
required: false,
|
|
4470
|
+
schema: { type: "string" },
|
|
4471
|
+
description: "Device user code to approve instead of an authorization request (requires oauthDeviceAuthorization())"
|
|
4472
|
+
},
|
|
4408
4473
|
{
|
|
4409
4474
|
name: "client_id",
|
|
4410
4475
|
in: "query",
|
|
4411
|
-
required:
|
|
4476
|
+
required: false,
|
|
4412
4477
|
schema: { type: "string" },
|
|
4413
|
-
description: "OAuth 2.1 client ID"
|
|
4478
|
+
description: "OAuth 2.1 client ID, required for authorization requests (not for user_code)"
|
|
4414
4479
|
},
|
|
4415
4480
|
{
|
|
4416
4481
|
name: "redirect_uri",
|
|
@@ -4554,25 +4619,26 @@ const oauthProvider = (options) => {
|
|
|
4554
4619
|
return { options: { databaseHooks: { session: { delete: {
|
|
4555
4620
|
async before(session, hookCtx) {
|
|
4556
4621
|
if (!hookCtx) return;
|
|
4557
|
-
const plan = await
|
|
4622
|
+
const plan = await prepareSessionLogoutPlan(hookCtx, opts, {
|
|
4558
4623
|
sessionId: session.id,
|
|
4559
4624
|
userId: session.userId
|
|
4560
4625
|
});
|
|
4561
4626
|
if (!plan) return;
|
|
4562
|
-
const logoutPlanBySessionId =
|
|
4627
|
+
const logoutPlanBySessionId = sessionLogoutPlansByContext.get(hookCtx) ?? /* @__PURE__ */ new Map();
|
|
4563
4628
|
logoutPlanBySessionId.set(session.id, plan);
|
|
4564
|
-
|
|
4629
|
+
sessionLogoutPlansByContext.set(hookCtx, logoutPlanBySessionId);
|
|
4565
4630
|
},
|
|
4566
4631
|
async after(session, hookCtx) {
|
|
4567
4632
|
if (!hookCtx) return;
|
|
4568
|
-
const logoutPlanBySessionId =
|
|
4633
|
+
const logoutPlanBySessionId = sessionLogoutPlansByContext.get(hookCtx);
|
|
4569
4634
|
if (!logoutPlanBySessionId) return;
|
|
4570
4635
|
const plan = logoutPlanBySessionId.get(session.id);
|
|
4571
4636
|
logoutPlanBySessionId.delete(session.id);
|
|
4572
|
-
if (logoutPlanBySessionId.size === 0)
|
|
4637
|
+
if (logoutPlanBySessionId.size === 0) sessionLogoutPlansByContext.delete(hookCtx);
|
|
4573
4638
|
if (!plan) return;
|
|
4574
|
-
|
|
4575
|
-
|
|
4639
|
+
await recordFrontchannelLogoutUris(plan.frontchannelLogoutUris);
|
|
4640
|
+
const logoutTask = applySessionLogoutPlan(hookCtx, plan).catch((error) => {
|
|
4641
|
+
hookCtx.context.logger.error("Logout notification failed after session deletion", error);
|
|
4576
4642
|
});
|
|
4577
4643
|
await hookCtx.context.runInBackgroundOrAwait(logoutTask);
|
|
4578
4644
|
}
|
|
@@ -4716,7 +4782,7 @@ const oauthProvider = (options) => {
|
|
|
4716
4782
|
}),
|
|
4717
4783
|
oauth2Token: createOAuthEndpoint("/oauth2/token", {
|
|
4718
4784
|
method: "POST",
|
|
4719
|
-
|
|
4785
|
+
formParams: { repeatable: ["resource", "audience"] },
|
|
4720
4786
|
body: z.object({
|
|
4721
4787
|
grant_type: z.string().trim().min(1),
|
|
4722
4788
|
client_id: z.string().optional(),
|
|
@@ -4857,15 +4923,11 @@ const oauthProvider = (options) => {
|
|
|
4857
4923
|
}
|
|
4858
4924
|
}
|
|
4859
4925
|
}, async (ctx) => {
|
|
4860
|
-
if (ctx.request) {
|
|
4861
|
-
const repeated = await extractRepeatedResourceFromForm(ctx.request);
|
|
4862
|
-
if (repeated && repeated.length > 1) ctx.body.resource = repeated;
|
|
4863
|
-
}
|
|
4864
4926
|
return tokenEndpoint(ctx, opts);
|
|
4865
4927
|
}),
|
|
4866
4928
|
oauth2Introspect: createOAuthEndpoint("/oauth2/introspect", {
|
|
4867
4929
|
method: "POST",
|
|
4868
|
-
|
|
4930
|
+
formParams: {},
|
|
4869
4931
|
body: z.object({
|
|
4870
4932
|
client_id: z.string().optional(),
|
|
4871
4933
|
client_secret: z.string().optional(),
|
|
@@ -4982,7 +5044,7 @@ const oauthProvider = (options) => {
|
|
|
4982
5044
|
}),
|
|
4983
5045
|
oauth2Revoke: createOAuthEndpoint("/oauth2/revoke", {
|
|
4984
5046
|
method: "POST",
|
|
4985
|
-
|
|
5047
|
+
formParams: {},
|
|
4986
5048
|
body: z.object({
|
|
4987
5049
|
client_id: z.string().optional(),
|
|
4988
5050
|
client_secret: z.string().optional(),
|
|
@@ -5160,7 +5222,7 @@ const oauthProvider = (options) => {
|
|
|
5160
5222
|
openapi: {
|
|
5161
5223
|
description: "RP-Initiated Logout endpoint. Accepts standard logout parameters through GET or an application/x-www-form-urlencoded POST body, plus JSON for Better Auth generated-client calls. Requests without a usable id_token_hint may require explicit OP confirmation.",
|
|
5162
5224
|
responses: { "200": {
|
|
5163
|
-
description: "Logout completed, or an HTML
|
|
5225
|
+
description: "Logout completed, or an HTML page was returned for browser navigation — a confirmation page, or the OIDC front-channel logout page when clients with a registered frontchannel_logout_uri hold tokens on the session. A redirect is used only for an exact registered post_logout_redirect_uri.",
|
|
5164
5226
|
content: {
|
|
5165
5227
|
"application/json": { schema: {
|
|
5166
5228
|
type: "object",
|
|
@@ -5176,7 +5238,10 @@ const oauthProvider = (options) => {
|
|
|
5176
5238
|
}
|
|
5177
5239
|
}
|
|
5178
5240
|
} },
|
|
5179
|
-
"text/html": { schema: {
|
|
5241
|
+
"text/html": { schema: {
|
|
5242
|
+
type: "string",
|
|
5243
|
+
description: "Logout confirmation page, or the OIDC Front-Channel Logout page with one hidden iframe per registered RP"
|
|
5244
|
+
} }
|
|
5180
5245
|
}
|
|
5181
5246
|
} }
|
|
5182
5247
|
}
|
|
@@ -5197,7 +5262,7 @@ const oauthProvider = (options) => {
|
|
|
5197
5262
|
}),
|
|
5198
5263
|
registerOAuthClient: createOAuthEndpoint("/oauth2/register", {
|
|
5199
5264
|
method: "POST",
|
|
5200
|
-
body: clientRegistrationRequestSchema,
|
|
5265
|
+
body: clientRegistrationRequestSchema.loose(),
|
|
5201
5266
|
errorCodesByField: {
|
|
5202
5267
|
redirect_uris: "invalid_redirect_uri",
|
|
5203
5268
|
post_logout_redirect_uris: "invalid_redirect_uri",
|
|
@@ -5301,6 +5366,15 @@ const oauthProvider = (options) => {
|
|
|
5301
5366
|
type: "boolean",
|
|
5302
5367
|
description: "Whether the RP requires a `sid` claim in every Logout Token"
|
|
5303
5368
|
},
|
|
5369
|
+
frontchannel_logout_uri: {
|
|
5370
|
+
type: "string",
|
|
5371
|
+
format: "uri",
|
|
5372
|
+
description: "RP URL rendered in a hidden iframe on the OP's logout page when the end-user's OP session terminates"
|
|
5373
|
+
},
|
|
5374
|
+
frontchannel_logout_session_required: {
|
|
5375
|
+
type: "boolean",
|
|
5376
|
+
description: "Whether the RP requires `iss` and `sid` query parameters on its front-channel logout URI. The OP always sends both."
|
|
5377
|
+
},
|
|
5304
5378
|
token_endpoint_auth_method: {
|
|
5305
5379
|
type: "string",
|
|
5306
5380
|
description: "Requested authentication method for the token endpoint"
|
|
@@ -5477,8 +5551,20 @@ const handleRedirect = (ctx, uri) => {
|
|
|
5477
5551
|
};
|
|
5478
5552
|
else throw ctx.redirect(uri);
|
|
5479
5553
|
};
|
|
5480
|
-
|
|
5481
|
-
|
|
5554
|
+
/** Sends an OAuth error to the client's redirect URI, or to the error page without one. */
|
|
5555
|
+
function redirectWithAuthorizationError(ctx, opts, query, error, description) {
|
|
5556
|
+
return handleRedirect(ctx, query.redirect_uri ? formatErrorURL(query.redirect_uri, error, description, query.state, getIssuer(ctx, opts)) : getErrorURL(ctx, error, description));
|
|
5557
|
+
}
|
|
5558
|
+
/**
|
|
5559
|
+
* Returns the URL that reports an `APIError`: `errorPage` when given, otherwise
|
|
5560
|
+
* the provider error page. Rethrows anything else.
|
|
5561
|
+
*/
|
|
5562
|
+
function getAPIErrorURL(ctx, error, errorPage) {
|
|
5563
|
+
if (!(error instanceof APIError$1)) throw error;
|
|
5564
|
+
const body = error.body;
|
|
5565
|
+
const code = body?.error ?? "invalid_request";
|
|
5566
|
+
const description = body?.error_description ?? "invalid request";
|
|
5567
|
+
return errorPage ? formatErrorURL(errorPage, code, description) : getErrorURL(ctx, code, description);
|
|
5482
5568
|
}
|
|
5483
5569
|
/**
|
|
5484
5570
|
* Validates that the issuer URL
|
|
@@ -5640,13 +5726,45 @@ function authorizeRedirectOnError(opts) {
|
|
|
5640
5726
|
};
|
|
5641
5727
|
}
|
|
5642
5728
|
async function authorizeEndpoint(ctx, opts, settings) {
|
|
5643
|
-
if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
|
|
5644
5729
|
if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
|
|
5645
5730
|
error_description: "request not found",
|
|
5646
5731
|
error: "invalid_request"
|
|
5647
5732
|
});
|
|
5648
5733
|
const request = ctx.request;
|
|
5649
5734
|
let query = getAuthorizationRequestParameters(ctx, settings);
|
|
5735
|
+
const interaction = getExtensionInteractionRequest(opts, query);
|
|
5736
|
+
if (interaction) {
|
|
5737
|
+
if (settings?.isAuthorize === true && !await getSessionFromCtx(ctx)) {
|
|
5738
|
+
ctx.query = { [interaction.parameter]: interaction.value };
|
|
5739
|
+
return redirectWithPromptCode(ctx, opts, "login");
|
|
5740
|
+
}
|
|
5741
|
+
const errorPage = interaction.handler.errorPage?.({
|
|
5742
|
+
ctx,
|
|
5743
|
+
value: interaction.value
|
|
5744
|
+
});
|
|
5745
|
+
const toErrorURL = (error) => getAPIErrorURL(ctx, error, errorPage);
|
|
5746
|
+
let interactionRequest;
|
|
5747
|
+
try {
|
|
5748
|
+
interactionRequest = await interaction.handler.load({
|
|
5749
|
+
ctx,
|
|
5750
|
+
opts,
|
|
5751
|
+
value: interaction.value,
|
|
5752
|
+
signedQuery: settings?.isAuthorize === true ? void 0 : query
|
|
5753
|
+
});
|
|
5754
|
+
if (interactionRequest.client.disabled) throw new APIError$1("BAD_REQUEST", {
|
|
5755
|
+
error: "client_disabled",
|
|
5756
|
+
error_description: "client is disabled"
|
|
5757
|
+
});
|
|
5758
|
+
} catch (error) {
|
|
5759
|
+
return handleRedirect(ctx, toErrorURL(error));
|
|
5760
|
+
}
|
|
5761
|
+
return runAuthorizationInteraction(ctx, opts, request.headers, settings, {
|
|
5762
|
+
...interactionRequest,
|
|
5763
|
+
approve: (approval) => interactionRequest.approve(approval).catch(toErrorURL),
|
|
5764
|
+
errorURL: toErrorURL
|
|
5765
|
+
});
|
|
5766
|
+
}
|
|
5767
|
+
if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
|
|
5650
5768
|
ctx.query = query;
|
|
5651
5769
|
const requestObject = getStringParameter(query.request);
|
|
5652
5770
|
const requestUri = getStringParameter(query.request_uri);
|
|
@@ -5703,9 +5821,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
5703
5821
|
error_description: "response_type is required",
|
|
5704
5822
|
ctx
|
|
5705
5823
|
});
|
|
5706
|
-
|
|
5707
|
-
const promptNone = promptSet?.has("none") ?? false;
|
|
5708
|
-
if (promptSet?.has("select_account") && !opts.selectAccount?.page) return handleRedirect(ctx, getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
|
|
5824
|
+
if ((ctx.query?.prompt ? parsePrompt(ctx.query?.prompt) : void 0)?.has("select_account") && !opts.selectAccount?.page) return handleRedirect(ctx, getErrorURL(ctx, `unsupported_prompt_select_account`, "unsupported prompt type"));
|
|
5709
5825
|
if (!(query.response_type === "code")) return handleRedirect(ctx, getErrorURL(ctx, "unsupported_response_type", "unsupported response type"));
|
|
5710
5826
|
const client = await getClient(ctx, opts, query.client_id);
|
|
5711
5827
|
if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client not found"));
|
|
@@ -5738,8 +5854,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
5738
5854
|
const openidRequested = requestedScopes.includes("openid");
|
|
5739
5855
|
if (query.claims !== void 0 && !openidRequested) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "openid scope must be requested when using the claims parameter", query.state, getIssuer(ctx, opts)));
|
|
5740
5856
|
if (openidRequested && !isValidOidcClaimsRequest(query.claims)) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "claims must be a valid Claims request object", query.state, getIssuer(ctx, opts)));
|
|
5741
|
-
if (openidRequested && !canSatisfyEssentialAcrRequest(query.claims,
|
|
5742
|
-
const requestedUserInfoClaims = getRequestedUserInfoClaims(query.claims, getSupportedClaims(opts));
|
|
5857
|
+
if (openidRequested && !getAcrValuesSupported(opts).some((acr) => canSatisfyEssentialAcrRequest(query.claims, acr))) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "access_denied", "essential acr requirement cannot be met", query.state, getIssuer(ctx, opts)));
|
|
5743
5858
|
if (query.resource !== void 0) try {
|
|
5744
5859
|
await resolveResourcePolicy(ctx, opts, {
|
|
5745
5860
|
resource: query.resource,
|
|
@@ -5765,12 +5880,35 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
5765
5880
|
if (!query.code_challenge || !query.code_challenge_method) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "code_challenge and code_challenge_method must both be provided", query.state, getIssuer(ctx, opts)));
|
|
5766
5881
|
if (!["S256"].includes(query.code_challenge_method)) return handleRedirect(ctx, formatErrorURL(query.redirect_uri, "invalid_request", "invalid code_challenge method, only S256 is supported", query.state, getIssuer(ctx, opts)));
|
|
5767
5882
|
}
|
|
5768
|
-
|
|
5883
|
+
return runAuthorizationInteraction(ctx, opts, request.headers, settings, {
|
|
5884
|
+
query,
|
|
5885
|
+
client,
|
|
5886
|
+
approve: (approval) => createAuthorizationCodeURL(ctx, opts, {
|
|
5887
|
+
...approval,
|
|
5888
|
+
query: approval.query,
|
|
5889
|
+
clientId: client.clientId,
|
|
5890
|
+
resource: toResourceList(approval.query.resource) ?? []
|
|
5891
|
+
})
|
|
5892
|
+
});
|
|
5893
|
+
}
|
|
5894
|
+
/**
|
|
5895
|
+
* Sends the user to the next required interaction page, or approves the
|
|
5896
|
+
* request once none remain.
|
|
5897
|
+
*/
|
|
5898
|
+
async function runAuthorizationInteraction(ctx, opts, headers, settings, request) {
|
|
5899
|
+
let { query } = request;
|
|
5900
|
+
const { client } = request;
|
|
5901
|
+
ctx.query = query;
|
|
5902
|
+
const scopes = query.scope?.split(" ").filter((scope) => scope) ?? [];
|
|
5903
|
+
const userInfoClaims = getRequestedUserInfoClaims(query.claims, getSupportedClaims(opts));
|
|
5904
|
+
const resources = toResourceList(query.resource) ?? [];
|
|
5905
|
+
const promptSet = query.prompt ? parsePrompt(query.prompt) : void 0;
|
|
5906
|
+
const promptNone = promptSet?.has("none") ?? false;
|
|
5769
5907
|
const session = await getSessionFromCtx(ctx);
|
|
5770
5908
|
const maxAgeSeconds = query.max_age;
|
|
5771
5909
|
const hasSatisfiedMaxAge = session != null && maxAgeSeconds !== void 0 && isWithinMaxAge(new Date(session.session.createdAt), maxAgeSeconds, /* @__PURE__ */ new Date());
|
|
5772
5910
|
if (!session || session != null && maxAgeSeconds !== void 0 && !hasSatisfiedMaxAge || promptSet?.has("login") || promptSet?.has("create")) {
|
|
5773
|
-
if (promptNone) return
|
|
5911
|
+
if (promptNone) return redirectWithAuthorizationError(ctx, opts, query, "login_required", "authentication required");
|
|
5774
5912
|
return redirectWithPromptCode(ctx, opts, promptSet?.has("create") ? "create" : "login");
|
|
5775
5913
|
}
|
|
5776
5914
|
if (hasSatisfiedMaxAge) {
|
|
@@ -5780,73 +5918,79 @@ async function authorizeEndpoint(ctx, opts, settings) {
|
|
|
5780
5918
|
if (settings?.isAuthorize && promptSet?.has("select_account")) return redirectWithPromptCode(ctx, opts, "select_account");
|
|
5781
5919
|
if (settings?.isAuthorize && opts.selectAccount) {
|
|
5782
5920
|
if (await opts.selectAccount.shouldRedirect({
|
|
5783
|
-
headers
|
|
5921
|
+
headers,
|
|
5784
5922
|
user: session.user,
|
|
5785
5923
|
session: session.session,
|
|
5786
|
-
scopes
|
|
5924
|
+
scopes
|
|
5787
5925
|
})) {
|
|
5788
|
-
if (promptNone) return
|
|
5926
|
+
if (promptNone) return redirectWithAuthorizationError(ctx, opts, query, "account_selection_required", "End-User account selection is required");
|
|
5789
5927
|
return redirectWithPromptCode(ctx, opts, "select_account");
|
|
5790
5928
|
}
|
|
5791
5929
|
}
|
|
5792
5930
|
if (opts.signup?.shouldRedirect) {
|
|
5793
5931
|
const signupRedirect = await opts.signup.shouldRedirect({
|
|
5794
|
-
headers
|
|
5932
|
+
headers,
|
|
5795
5933
|
user: session.user,
|
|
5796
5934
|
session: session.session,
|
|
5797
|
-
scopes
|
|
5935
|
+
scopes
|
|
5798
5936
|
});
|
|
5799
5937
|
if (signupRedirect) {
|
|
5800
|
-
if (promptNone) return
|
|
5938
|
+
if (promptNone) return redirectWithAuthorizationError(ctx, opts, query, "interaction_required", "End-User interaction is required");
|
|
5801
5939
|
return redirectWithPromptCode(ctx, opts, "create", { page: typeof signupRedirect === "string" ? signupRedirect : void 0 });
|
|
5802
5940
|
}
|
|
5803
5941
|
}
|
|
5804
5942
|
if (!settings?.postLogin && opts.postLogin) {
|
|
5805
5943
|
if (await opts.postLogin.shouldRedirect({
|
|
5806
|
-
headers
|
|
5944
|
+
headers,
|
|
5807
5945
|
user: session.user,
|
|
5808
5946
|
session: session.session,
|
|
5809
|
-
scopes
|
|
5947
|
+
scopes,
|
|
5810
5948
|
isContinuation: settings?.isPostLoginContinuation ?? false
|
|
5811
5949
|
})) {
|
|
5812
|
-
if (promptNone) return
|
|
5950
|
+
if (promptNone) return redirectWithAuthorizationError(ctx, opts, query, "interaction_required", "End-User interaction is required");
|
|
5813
5951
|
return redirectWithPromptCode(ctx, opts, "post_login");
|
|
5814
5952
|
}
|
|
5815
5953
|
}
|
|
5816
|
-
if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
|
|
5954
|
+
if (promptSet?.has("consent") || request.requireConsent && !settings?.consentAccepted) return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
|
|
5817
5955
|
const referenceId = await getConsentReferenceId(opts, {
|
|
5818
5956
|
user: session.user,
|
|
5819
5957
|
session: session.session,
|
|
5820
|
-
scopes
|
|
5821
|
-
});
|
|
5822
|
-
if (client.skipConsent) return redirectWithAuthorizationCode(ctx, opts, {
|
|
5823
|
-
query,
|
|
5824
|
-
clientId: client.clientId,
|
|
5825
|
-
userId: session.user.id,
|
|
5826
|
-
sessionId: session.session.id,
|
|
5827
|
-
authTime: new Date(session.session.createdAt).getTime(),
|
|
5828
|
-
referenceId,
|
|
5829
|
-
resource: requestedResources
|
|
5958
|
+
scopes
|
|
5830
5959
|
});
|
|
5960
|
+
const approve = async () => {
|
|
5961
|
+
let authenticationContext;
|
|
5962
|
+
try {
|
|
5963
|
+
authenticationContext = await resolveAuthenticationContext(opts, {
|
|
5964
|
+
user: session.user,
|
|
5965
|
+
session: session.session,
|
|
5966
|
+
client,
|
|
5967
|
+
scopes,
|
|
5968
|
+
headers,
|
|
5969
|
+
query
|
|
5970
|
+
});
|
|
5971
|
+
} catch (error) {
|
|
5972
|
+
if (!(error instanceof APIError$1)) throw error;
|
|
5973
|
+
if (request.errorURL) return handleRedirect(ctx, request.errorURL(error));
|
|
5974
|
+
const body = error.body;
|
|
5975
|
+
return redirectWithAuthorizationError(ctx, opts, query, body?.error ?? "server_error", body?.error_description ?? "authentication context unavailable");
|
|
5976
|
+
}
|
|
5977
|
+
if (!canSatisfyEssentialAcrRequest(query.claims, authenticationContext.acr ?? "0")) return redirectWithAuthorizationError(ctx, opts, query, "access_denied", "essential acr requirement cannot be met");
|
|
5978
|
+
return handleRedirect(ctx, await request.approve({
|
|
5979
|
+
query,
|
|
5980
|
+
userId: session.user.id,
|
|
5981
|
+
sessionId: session.session.id,
|
|
5982
|
+
authTime: new Date(session.session.createdAt).getTime(),
|
|
5983
|
+
...authenticationContext,
|
|
5984
|
+
referenceId
|
|
5985
|
+
}));
|
|
5986
|
+
};
|
|
5987
|
+
if (client.skipConsent) return approve();
|
|
5831
5988
|
const consent = await findConsent(ctx, client.clientId, session.user.id, referenceId);
|
|
5832
|
-
if (!consent || !
|
|
5833
|
-
if (promptNone) return
|
|
5834
|
-
return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
|
|
5835
|
-
}
|
|
5836
|
-
const consentedResources = consent?.resources ?? [];
|
|
5837
|
-
if (requestedResources.some((requestedResource) => !consentedResources.includes(requestedResource))) {
|
|
5838
|
-
if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
|
|
5989
|
+
if (!consent || !consentCovers(consent, scopes, resources) || !userInfoClaims.every((claim) => (consent.requestedUserInfoClaims ?? []).includes(claim))) {
|
|
5990
|
+
if (promptNone) return redirectWithAuthorizationError(ctx, opts, query, "consent_required", "End-User consent is required");
|
|
5839
5991
|
return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
|
|
5840
5992
|
}
|
|
5841
|
-
return
|
|
5842
|
-
query,
|
|
5843
|
-
clientId: client.clientId,
|
|
5844
|
-
userId: session.user.id,
|
|
5845
|
-
sessionId: session.session.id,
|
|
5846
|
-
authTime: new Date(session.session.createdAt).getTime(),
|
|
5847
|
-
referenceId,
|
|
5848
|
-
resource: requestedResources
|
|
5849
|
-
});
|
|
5993
|
+
return approve();
|
|
5850
5994
|
}
|
|
5851
5995
|
function serializeAuthorizationQuery(query) {
|
|
5852
5996
|
const params = new URLSearchParams();
|
|
@@ -5858,7 +6002,8 @@ function serializeAuthorizationQuery(query) {
|
|
|
5858
6002
|
}
|
|
5859
6003
|
return params;
|
|
5860
6004
|
}
|
|
5861
|
-
|
|
6005
|
+
/** Stores a single-use authorization code and returns the client redirect URL carrying it. */
|
|
6006
|
+
async function createAuthorizationCodeURL(ctx, opts, verificationValue) {
|
|
5862
6007
|
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
|
|
5863
6008
|
const iat = Math.floor(Date.now() / 1e3);
|
|
5864
6009
|
const exp = iat + (opts.codeExpiresIn ?? 600);
|
|
@@ -5873,6 +6018,8 @@ async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
|
|
|
5873
6018
|
sessionId: verificationValue?.sessionId,
|
|
5874
6019
|
referenceId: verificationValue.referenceId,
|
|
5875
6020
|
authTime: verificationValue.authTime,
|
|
6021
|
+
acr: verificationValue.acr,
|
|
6022
|
+
amr: verificationValue.amr,
|
|
5876
6023
|
resource: verificationValue.resource
|
|
5877
6024
|
})
|
|
5878
6025
|
};
|
|
@@ -5884,7 +6031,7 @@ async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
|
|
|
5884
6031
|
redirectUriWithCode.searchParams.set("code", code);
|
|
5885
6032
|
if (verificationValue.query.state) redirectUriWithCode.searchParams.set("state", verificationValue.query.state);
|
|
5886
6033
|
redirectUriWithCode.searchParams.set("iss", getIssuer(ctx, opts));
|
|
5887
|
-
return
|
|
6034
|
+
return redirectUriWithCode.toString();
|
|
5888
6035
|
}
|
|
5889
6036
|
async function redirectWithPromptCode(ctx, opts, type, options) {
|
|
5890
6037
|
const queryParams = await signParams(ctx, opts, { postLoginClearedForSession: type === "consent" && opts.postLogin ? options?.sessionId : void 0 });
|
|
@@ -5912,4 +6059,4 @@ async function signParams(ctx, opts, flags) {
|
|
|
5912
6059
|
return params.toString();
|
|
5913
6060
|
}
|
|
5914
6061
|
//#endregion
|
|
5915
|
-
export {
|
|
6062
|
+
export { assertPublicRoutableHttpsUri as a, consumeClientAssertion as c, authServerMetadata as d, metadataResponse as f, oidcServerMetadata as g, oauthProviderOpenIdConfigMetadata as h, oauthProvider as i, validatePublicClientJwks as l, oauthProviderAuthServerMetadata as m, DEFAULT_OAUTH_SCOPES as n, registerClientMetadataDocument as o, oauthAuthorizationServerMetadata as p, getOAuthProviderState as r, client_assertion_exports as s, getIssuer as t, createOAuthEndpoint as u };
|