@better-auth/oauth-provider 1.7.0-beta.1 → 1.7.0-beta.3

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/index.mjs CHANGED
@@ -1,12 +1,13 @@
1
- import { n as isPrivateHostname } from "./client-assertion-CderPEmR.mjs";
1
+ import { n as isPrivateHostname } from "./client-assertion-BYtMWGCE.mjs";
2
2
  import { n as mcpHandler } from "./mcp-CYnz-MXn.mjs";
3
- import { _ as storeClientSecret, a as getClient, b as validateClientCredentials, c as getStoredToken, d as normalizeTimestampValue, f as parseClientMetadata, g as searchParamsToQuery, h as resolveSubjectIdentifier, i as extractClientCredentials, l as isPKCERequired, m as resolveSessionAuthTime, n as deleteFromPrompt, o as getJwtPlugin, p as parsePrompt, r as destructureCredentials, t as decryptStoredClientSecret, u as mergeDiscoveryMetadata, v as storeToken, x as verifyOAuthQueryParams, y as toClientDiscoveryArray } from "./utils-Cx_XnD9i.mjs";
4
- import { t as PACKAGE_VERSION } from "./version-DIwdpXrQ.mjs";
3
+ import { C as validateClientCredentials, S as toClientDiscoveryArray, _ as resolveSubjectIdentifier, a as getJwtPlugin, b as storeClientSecret, c as getStoredToken, d as normalizeTimestampValue, f as parseClientMetadata, g as resolveSessionAuthTime, h as removePromptFromQuery, i as getClient, l as isPKCERequired, m as postLoginClearedParam, n as destructureCredentials, p as parsePrompt, r as extractClientCredentials, s as getSignedQueryIssuedAt, t as decryptStoredClientSecret, u as mergeDiscoveryMetadata, v as searchParamsToQuery, w as verifyOAuthQueryParams, x as storeToken, y as signedQueryIssuedAtParam } from "./utils-_Jr_enAe.mjs";
4
+ import { t as PACKAGE_VERSION } from "./version-CG1YnCiF.mjs";
5
5
  import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
6
6
  import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
7
7
  import { APIError as APIError$1 } from "better-call";
8
8
  import { ASSERTION_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
9
9
  import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
10
+ import { isLoopbackHost, isLoopbackIP } from "@better-auth/core/utils/host";
10
11
  import { generateRandomString, makeSignature } from "better-auth/crypto";
11
12
  import { defineRequestState } from "@better-auth/core/context";
12
13
  import { logger } from "@better-auth/core/env";
@@ -18,7 +19,8 @@ import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
18
19
  import { SignJWT, base64url, compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader } from "jose";
19
20
  //#region src/consent.ts
20
21
  async function consentEndpoint(ctx, opts) {
21
- const _query = (await oAuthState.get())?.query;
22
+ const oauthRequest = await oAuthState.get();
23
+ const _query = oauthRequest?.query;
22
24
  if (!_query) throw new APIError("BAD_REQUEST", {
23
25
  error_description: "missing oauth query",
24
26
  error: "invalid_request"
@@ -42,6 +44,17 @@ async function consentEndpoint(ctx, opts) {
42
44
  url: formatErrorURL(query.get("redirect_uri") ?? "", "access_denied", "User denied access", query.get("state") ?? void 0, getIssuer(ctx, opts))
43
45
  };
44
46
  const session = await getSessionFromCtx(ctx);
47
+ const hasLoginPrompt = parsePrompt(query.get("prompt") ?? "").has("login");
48
+ const hasSatisfiedLoginPrompt = hasLoginPrompt && sessionSatisfiesLoginPrompt(session?.session.createdAt, oauthRequest?.signedQueryIssuedAt);
49
+ if (hasLoginPrompt && !hasSatisfiedLoginPrompt) {
50
+ ctx?.headers?.set("accept", "application/json");
51
+ ctx.query = searchParamsToQuery(query);
52
+ const { url } = await authorizeEndpoint(ctx, opts);
53
+ return {
54
+ redirect: true,
55
+ url
56
+ };
57
+ }
45
58
  const referenceId = await opts.postLogin?.consentReferenceId?.({
46
59
  user: session?.user,
47
60
  session: session?.session,
@@ -92,14 +105,21 @@ async function consentEndpoint(ctx, opts) {
92
105
  });
93
106
  if (requestedScopes) query.set("scope", consent.scopes.join(" "));
94
107
  ctx?.headers?.set("accept", "application/json");
95
- ctx.query = deleteFromPrompt(query, "consent");
96
- ctx.context.postLogin = true;
97
- const { url } = await authorizeEndpoint(ctx, opts);
108
+ let authorizationQuery = removePromptFromQuery(query, "consent");
109
+ if (hasSatisfiedLoginPrompt) authorizationQuery = removePromptFromQuery(authorizationQuery, "login");
110
+ ctx.query = searchParamsToQuery(authorizationQuery);
111
+ const { url } = await authorizeEndpoint(ctx, opts, { postLogin: oauthRequest?.postLoginClearedForSession !== void 0 && oauthRequest.postLoginClearedForSession === session?.session.id });
98
112
  return {
99
113
  redirect: true,
100
114
  url
101
115
  };
102
116
  }
117
+ function sessionSatisfiesLoginPrompt(sessionCreatedAt, signedQueryIssuedAt) {
118
+ if (!signedQueryIssuedAt) return false;
119
+ const normalized = normalizeTimestampValue(sessionCreatedAt);
120
+ if (!normalized) return false;
121
+ return normalized.getTime() >= signedQueryIssuedAt.getTime();
122
+ }
103
123
  //#endregion
104
124
  //#region src/continue.ts
105
125
  async function continueEndpoint(ctx, opts) {
@@ -118,7 +138,7 @@ async function selected(ctx, opts) {
118
138
  error: "invalid_request"
119
139
  });
120
140
  ctx.headers?.set("accept", "application/json");
121
- ctx.query = deleteFromPrompt(new URLSearchParams(_query), "select_account");
141
+ ctx.query = searchParamsToQuery(removePromptFromQuery(new URLSearchParams(_query), "select_account"));
122
142
  const { url } = await authorizeEndpoint(ctx, opts);
123
143
  return {
124
144
  redirect: true,
@@ -133,7 +153,7 @@ async function created(ctx, opts) {
133
153
  });
134
154
  const query = new URLSearchParams(_query);
135
155
  ctx.headers?.set("accept", "application/json");
136
- ctx.query = deleteFromPrompt(query, "create");
156
+ ctx.query = searchParamsToQuery(removePromptFromQuery(query, "create"));
137
157
  const { url } = await authorizeEndpoint(ctx, opts);
138
158
  return {
139
159
  redirect: true,
@@ -162,9 +182,6 @@ const DANGEROUS_SCHEMES = [
162
182
  "data:",
163
183
  "vbscript:"
164
184
  ];
165
- function isLocalhost(hostname) {
166
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
167
- }
168
185
  /**
169
186
  * Runtime schema for OAuthAuthorizationQuery.
170
187
  * Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
@@ -203,7 +220,7 @@ const verificationValueSchema = z.object({
203
220
  /**
204
221
  * Reusable URL validation for OAuth redirect URIs.
205
222
  * - Blocks dangerous schemes (javascript:, data:, vbscript:)
206
- * - For http/https: requires HTTPS (HTTP allowed only for localhost)
223
+ * - For http/https: requires HTTPS (HTTP allowed only for loopback hosts: 127.0.0.0/8, [::1], *.localhost per RFC 6761)
207
224
  * - Allows custom schemes for mobile apps (e.g., myapp://callback)
208
225
  */
209
226
  const SafeUrlSchema = z.url().superRefine((val, ctx) => {
@@ -223,12 +240,10 @@ const SafeUrlSchema = z.url().superRefine((val, ctx) => {
223
240
  });
224
241
  return;
225
242
  }
226
- if (u.protocol === "http:" || u.protocol === "https:") {
227
- if (u.protocol === "http:" && !isLocalhost(u.hostname)) ctx.addIssue({
228
- code: "custom",
229
- message: "Redirect URI must use HTTPS (HTTP allowed only for localhost)"
230
- });
231
- }
243
+ if (u.protocol === "http:" && !isLoopbackHost(u.host)) ctx.addIssue({
244
+ code: "custom",
245
+ message: "Redirect URI must use HTTPS (HTTP allowed only for loopback hosts)"
246
+ });
232
247
  });
233
248
  //#endregion
234
249
  //#region src/userinfo.ts
@@ -259,11 +274,7 @@ function userNormalClaims(user, scopes) {
259
274
  * Handles the /oauth2/userinfo endpoint
260
275
  */
261
276
  async function userInfoEndpoint(ctx, opts) {
262
- if (!ctx.request) throw new APIError("UNAUTHORIZED", {
263
- error_description: "request not found",
264
- error: "invalid_request"
265
- });
266
- const authorization = ctx.request.headers.get("authorization");
277
+ const authorization = ctx.headers?.get("authorization");
267
278
  const token = typeof authorization === "string" && authorization?.startsWith("Bearer ") ? authorization?.replace("Bearer ", "") : authorization;
268
279
  if (!token?.length) throw new APIError("UNAUTHORIZED", {
269
280
  error_description: "authorization header not found",
@@ -309,8 +320,8 @@ async function userInfoEndpoint(ctx, opts) {
309
320
  * the grant types
310
321
  */
311
322
  async function tokenEndpoint(ctx, opts) {
312
- const grantType = ctx.body?.grant_type;
313
- if (opts.grantTypes && grantType && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
323
+ const grantType = ctx.body.grant_type;
324
+ if (opts.grantTypes && !opts.grantTypes.includes(grantType)) throw new APIError("BAD_REQUEST", {
314
325
  error_description: `unsupported grant_type ${grantType}`,
315
326
  error: "unsupported_grant_type"
316
327
  });
@@ -318,14 +329,6 @@ async function tokenEndpoint(ctx, opts) {
318
329
  case "authorization_code": return handleAuthorizationCodeGrant(ctx, opts);
319
330
  case "client_credentials": return handleClientCredentialsGrant(ctx, opts);
320
331
  case "refresh_token": return handleRefreshTokenGrant(ctx, opts);
321
- case void 0: throw new APIError("BAD_REQUEST", {
322
- error_description: "missing required grant_type",
323
- error: "unsupported_grant_type"
324
- });
325
- default: throw new APIError("BAD_REQUEST", {
326
- error_description: `unsupported grant_type ${grantType}`,
327
- error: "unsupported_grant_type"
328
- });
329
332
  }
330
333
  }
331
334
  async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, overrides) {
@@ -955,7 +958,7 @@ async function validateRefreshToken(ctx, opts, token, clientId) {
955
958
  model: "session",
956
959
  where: [{
957
960
  field: "id",
958
- value: refreshToken.sessionId
961
+ value: sessionId
959
962
  }]
960
963
  });
961
964
  if (!session || session.expiresAt < /* @__PURE__ */ new Date()) sessionId = void 0;
@@ -1016,6 +1019,7 @@ async function resolveIntrospectionSub(opts, payload, client) {
1016
1019
  }
1017
1020
  async function introspectEndpoint(ctx, opts) {
1018
1021
  let { token, token_type_hint } = ctx.body;
1022
+ if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
1019
1023
  const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
1020
1024
  if (!client_id || !client_secret && !preVerifiedClient) throw new APIError$1("UNAUTHORIZED", {
1021
1025
  error_description: "missing required credentials",
@@ -1173,6 +1177,109 @@ async function rpInitiatedLogoutEndpoint(ctx, opts) {
1173
1177
  }
1174
1178
  }
1175
1179
  //#endregion
1180
+ //#region src/oauth-endpoint.ts
1181
+ /**
1182
+ * Wraps `createAuthEndpoint` so zod schemas stay the single source of truth
1183
+ * for body/query shape while validation failures serialize as the RFC 6749
1184
+ * §5.2 error envelope `{ error, error_description }`.
1185
+ *
1186
+ * A failing issue is routed by its first path segment via `errorCodesByField`:
1187
+ * - missing required (`invalid_type` + "received undefined") → `.missing`
1188
+ * - unsupported value (`invalid_value`) → `.invalid`
1189
+ * - anything else (wrong type, duplicated params, bad format) → `defaultError`
1190
+ *
1191
+ * For enum fields that need to distinguish missing from unsupported, compose
1192
+ * as `z.string().pipe(z.enum([...]))` so duplicated params fail the outer
1193
+ * `z.string()` as `invalid_type` instead of masquerading as an unsupported
1194
+ * enum value.
1195
+ */
1196
+ function createOAuthEndpoint(path, options, handler) {
1197
+ const { redirectOnError, onValidationError: userHook, errorCodesByField, defaultError = "invalid_request", ...rest } = options;
1198
+ if (!redirectOnError) return createAuthEndpoint(path, {
1199
+ ...rest,
1200
+ onValidationError: async (args) => {
1201
+ if (userHook) await userHook(args);
1202
+ throw new APIError$1("BAD_REQUEST", { ...mapIssuesToOAuthError(args.issues, errorCodesByField, defaultError) });
1203
+ }
1204
+ }, handler);
1205
+ const redirect = redirectOnError;
1206
+ const { body: bodySchema, query: querySchema, ...forwarded } = rest;
1207
+ async function validateSlot(ctx, slot, schema) {
1208
+ if (!schema) return { ok: true };
1209
+ const result = await schema.safeParseAsync(ctx[slot] ?? {});
1210
+ if (result.success) {
1211
+ ctx[slot] = result.data;
1212
+ return { ok: true };
1213
+ }
1214
+ if (userHook) await userHook({
1215
+ message: result.error.message,
1216
+ issues: result.error.issues
1217
+ });
1218
+ return {
1219
+ ok: false,
1220
+ response: redirect({
1221
+ ...mapIssuesToOAuthError(result.error.issues, errorCodesByField, defaultError),
1222
+ ctx
1223
+ })
1224
+ };
1225
+ }
1226
+ return createAuthEndpoint(path, forwarded, async (ctx) => {
1227
+ const body = await validateSlot(ctx, "body", bodySchema);
1228
+ if (!body.ok) return body.response;
1229
+ const query = await validateSlot(ctx, "query", querySchema);
1230
+ if (!query.ok) return query.response;
1231
+ return handler(ctx);
1232
+ });
1233
+ }
1234
+ function mapIssuesToOAuthError(issues, errorCodesByField, defaultError = "invalid_request") {
1235
+ const issue = issues[0];
1236
+ if (!issue) return {
1237
+ error: defaultError,
1238
+ error_description: "Invalid request."
1239
+ };
1240
+ const first = issue.path?.[0];
1241
+ const fieldKey = typeof first === "string" ? first : void 0;
1242
+ const mapping = fieldKey ? errorCodesByField?.[fieldKey] : void 0;
1243
+ const field = issue.path?.length ? z.core.toDotPath(issue.path) : "";
1244
+ return {
1245
+ error: resolveErrorCode(issue, mapping, defaultError),
1246
+ error_description: describeIssue(issue, field)
1247
+ };
1248
+ }
1249
+ function resolveErrorCode(issue, mapping, defaultError) {
1250
+ if (typeof mapping === "string") return mapping;
1251
+ if (isMissingValueIssue(issue)) return mapping?.missing ?? defaultError;
1252
+ if (issue.code === "invalid_value") return mapping?.invalid ?? defaultError;
1253
+ return defaultError;
1254
+ }
1255
+ /**
1256
+ * Returns `true` for issues that represent an absent required value. Zod v4
1257
+ * strips `input` from published issues, so the signal is the `invalid_type`
1258
+ * code combined with a message suffix of "received undefined". The suffix is
1259
+ * pinned by a regression test so a zod rephrase fails the test instead of
1260
+ * silently reclassifying missing fields.
1261
+ *
1262
+ * Assumes the default zod error map. Consumers that install a localized map
1263
+ * via `z.setErrorMap()` will break this check, collapsing missing-field
1264
+ * failures to `defaultError`.
1265
+ */
1266
+ function isMissingValueIssue(issue) {
1267
+ return issue.code === "invalid_type" && issue.message.endsWith("received undefined");
1268
+ }
1269
+ function describeIssue(issue, field) {
1270
+ if (!field) return issue.message;
1271
+ if (issue.code === "invalid_type") {
1272
+ if (issue.message.endsWith("received undefined")) return `${field} is required`;
1273
+ if (issue.message.endsWith("received array")) return `${field} must not appear more than once`;
1274
+ return `${field} must be a ${issue.expected ?? "valid value"}`;
1275
+ }
1276
+ if (issue.code === "invalid_value") {
1277
+ const values = issue.values;
1278
+ if (Array.isArray(values) && values.length > 0) return `${field} must be one of: ${values.join(", ")}`;
1279
+ }
1280
+ return `${field}: ${issue.message}`;
1281
+ }
1282
+ //#endregion
1176
1283
  //#region src/middleware/index.ts
1177
1284
  const publicSessionMiddleware = (opts) => createAuthMiddleware(async (ctx) => {
1178
1285
  if (!opts.allowPublicClientPrelogin) throw new APIError("BAD_REQUEST");
@@ -1458,14 +1565,8 @@ function schemaToOAuth(input) {
1458
1565
  //#region src/oauthClient/endpoints.ts
1459
1566
  async function getClientEndpoint(ctx, opts) {
1460
1567
  const session = await getSessionFromCtx(ctx);
1568
+ await assertClientPrivileges(ctx, session, opts, "read");
1461
1569
  if (!session) throw new APIError("UNAUTHORIZED");
1462
- if (!ctx.headers) throw new APIError("BAD_REQUEST");
1463
- if (opts.clientPrivileges && !await opts.clientPrivileges({
1464
- headers: ctx.headers,
1465
- action: "read",
1466
- session: session.session,
1467
- user: session.user
1468
- })) throw new APIError("UNAUTHORIZED");
1469
1570
  const client = await getClient(ctx, opts, ctx.query.client_id);
1470
1571
  if (!client) throw new APIError("NOT_FOUND", {
1471
1572
  error_description: "client not found",
@@ -1506,14 +1607,8 @@ async function getClientPublicEndpoint(ctx, opts, clientId) {
1506
1607
  }
1507
1608
  async function getClientsEndpoint(ctx, opts) {
1508
1609
  const session = await getSessionFromCtx(ctx);
1610
+ await assertClientPrivileges(ctx, session, opts, "list");
1509
1611
  if (!session) throw new APIError("UNAUTHORIZED");
1510
- if (!ctx.headers) throw new APIError("BAD_REQUEST");
1511
- if (opts.clientPrivileges && !await opts.clientPrivileges({
1512
- headers: ctx.headers,
1513
- action: "list",
1514
- session: session.session,
1515
- user: session.user
1516
- })) throw new APIError("UNAUTHORIZED");
1517
1612
  const referenceId = await opts.clientReference?.(session);
1518
1613
  if (referenceId) return await ctx.context.adapter.findMany({
1519
1614
  model: "oauthClient",
@@ -1547,14 +1642,8 @@ async function getClientsEndpoint(ctx, opts) {
1547
1642
  }
1548
1643
  async function deleteClientEndpoint(ctx, opts) {
1549
1644
  const session = await getSessionFromCtx(ctx);
1645
+ await assertClientPrivileges(ctx, session, opts, "delete");
1550
1646
  if (!session) throw new APIError("UNAUTHORIZED");
1551
- if (!ctx.headers) throw new APIError("BAD_REQUEST");
1552
- if (opts.clientPrivileges && !await opts.clientPrivileges({
1553
- headers: ctx.headers,
1554
- action: "delete",
1555
- session: session.session,
1556
- user: session.user
1557
- })) throw new APIError("UNAUTHORIZED");
1558
1647
  const clientId = ctx.body.client_id;
1559
1648
  if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
1560
1649
  error_description: "trusted clients must be updated manually",
@@ -1580,14 +1669,8 @@ async function deleteClientEndpoint(ctx, opts) {
1580
1669
  }
1581
1670
  async function updateClientEndpoint(ctx, opts) {
1582
1671
  const session = await getSessionFromCtx(ctx);
1672
+ await assertClientPrivileges(ctx, session, opts, "update");
1583
1673
  if (!session) throw new APIError("UNAUTHORIZED");
1584
- if (!ctx.headers) throw new APIError("BAD_REQUEST");
1585
- if (opts.clientPrivileges && !await opts.clientPrivileges({
1586
- headers: ctx.headers,
1587
- action: "update",
1588
- session: session.session,
1589
- user: session.user
1590
- })) throw new APIError("UNAUTHORIZED");
1591
1674
  const clientId = ctx.body.client_id;
1592
1675
  if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
1593
1676
  error_description: "trusted clients must be updated manually",
@@ -1641,14 +1724,8 @@ async function updateClientEndpoint(ctx, opts) {
1641
1724
  }
1642
1725
  async function rotateClientSecretEndpoint(ctx, opts) {
1643
1726
  const session = await getSessionFromCtx(ctx);
1727
+ await assertClientPrivileges(ctx, session, opts, "rotate");
1644
1728
  if (!session) throw new APIError("UNAUTHORIZED");
1645
- if (!ctx.headers) throw new APIError("BAD_REQUEST");
1646
- if (opts.clientPrivileges && !await opts.clientPrivileges({
1647
- headers: ctx.headers,
1648
- action: "rotate",
1649
- session: session.session,
1650
- user: session.user
1651
- })) throw new APIError("UNAUTHORIZED");
1652
1729
  const clientId = ctx.body.client_id;
1653
1730
  if (opts.cachedTrustedClients?.has(clientId)) throw new APIError("INTERNAL_SERVER_ERROR", {
1654
1731
  error_description: "trusted clients must be updated manually",
@@ -1690,6 +1767,16 @@ async function rotateClientSecretEndpoint(ctx, opts) {
1690
1767
  clientSecret: (opts.prefix?.clientSecret ?? "") + clientSecret
1691
1768
  });
1692
1769
  }
1770
+ async function assertClientPrivileges(ctx, session, opts, action) {
1771
+ if (!session) throw new APIError("UNAUTHORIZED");
1772
+ if (!ctx.headers) throw new APIError("BAD_REQUEST");
1773
+ if (opts.clientPrivileges && !await opts.clientPrivileges({
1774
+ headers: ctx.headers,
1775
+ action,
1776
+ session: session.session,
1777
+ user: session.user
1778
+ })) throw new APIError("UNAUTHORIZED");
1779
+ }
1693
1780
  //#endregion
1694
1781
  //#region src/oauthClient/index.ts
1695
1782
  const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/create-client", {
@@ -1875,6 +1962,7 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1875
1962
  }
1876
1963
  }
1877
1964
  }, async (ctx) => {
1965
+ await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
1878
1966
  return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
1879
1967
  });
1880
1968
  const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client", {
@@ -2047,6 +2135,7 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
2047
2135
  } }
2048
2136
  } }
2049
2137
  }, async (ctx) => {
2138
+ await assertClientPrivileges(ctx, await getSessionFromCtx(ctx), opts, "create");
2050
2139
  return createOAuthClientEndpoint(ctx, opts, { isRegister: false });
2051
2140
  });
2052
2141
  const getOAuthClient = (opts) => createAuthEndpoint("/oauth2/get-client", {
@@ -2459,6 +2548,7 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
2459
2548
  }
2460
2549
  async function revokeEndpoint(ctx, opts) {
2461
2550
  let { token, token_type_hint } = ctx.body;
2551
+ if (token_type_hint !== "access_token" && token_type_hint !== "refresh_token") token_type_hint = void 0;
2462
2552
  const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
2463
2553
  if (!client_id) throw new APIError$1("UNAUTHORIZED", {
2464
2554
  error_description: "missing required credentials",
@@ -2546,7 +2636,8 @@ const schema = {
2546
2636
  references: {
2547
2637
  model: "user",
2548
2638
  field: "id"
2549
- }
2639
+ },
2640
+ index: true
2550
2641
  },
2551
2642
  createdAt: {
2552
2643
  type: "date",
@@ -2653,7 +2744,8 @@ const schema = {
2653
2744
  references: {
2654
2745
  model: "oauthClient",
2655
2746
  field: "clientId"
2656
- }
2747
+ },
2748
+ index: true
2657
2749
  },
2658
2750
  sessionId: {
2659
2751
  type: "string",
@@ -2662,7 +2754,8 @@ const schema = {
2662
2754
  model: "session",
2663
2755
  field: "id",
2664
2756
  onDelete: "set null"
2665
- }
2757
+ },
2758
+ index: true
2666
2759
  },
2667
2760
  userId: {
2668
2761
  type: "string",
@@ -2670,7 +2763,8 @@ const schema = {
2670
2763
  references: {
2671
2764
  model: "user",
2672
2765
  field: "id"
2673
- }
2766
+ },
2767
+ index: true
2674
2768
  },
2675
2769
  referenceId: {
2676
2770
  type: "string",
@@ -2704,7 +2798,8 @@ const schema = {
2704
2798
  references: {
2705
2799
  model: "oauthClient",
2706
2800
  field: "clientId"
2707
- }
2801
+ },
2802
+ index: true
2708
2803
  },
2709
2804
  sessionId: {
2710
2805
  type: "string",
@@ -2713,7 +2808,8 @@ const schema = {
2713
2808
  model: "session",
2714
2809
  field: "id",
2715
2810
  onDelete: "set null"
2716
- }
2811
+ },
2812
+ index: true
2717
2813
  },
2718
2814
  userId: {
2719
2815
  type: "string",
@@ -2721,7 +2817,8 @@ const schema = {
2721
2817
  references: {
2722
2818
  model: "user",
2723
2819
  field: "id"
2724
- }
2820
+ },
2821
+ index: true
2725
2822
  },
2726
2823
  referenceId: {
2727
2824
  type: "string",
@@ -2733,7 +2830,8 @@ const schema = {
2733
2830
  references: {
2734
2831
  model: "oauthRefreshToken",
2735
2832
  field: "id"
2736
- }
2833
+ },
2834
+ index: true
2737
2835
  },
2738
2836
  expiresAt: { type: "date" },
2739
2837
  createdAt: { type: "date" },
@@ -2752,7 +2850,8 @@ const schema = {
2752
2850
  references: {
2753
2851
  model: "oauthClient",
2754
2852
  field: "clientId"
2755
- }
2853
+ },
2854
+ index: true
2756
2855
  },
2757
2856
  userId: {
2758
2857
  type: "string",
@@ -2760,7 +2859,8 @@ const schema = {
2760
2859
  references: {
2761
2860
  model: "user",
2762
2861
  field: "id"
2763
- }
2862
+ },
2863
+ index: true
2764
2864
  },
2765
2865
  referenceId: {
2766
2866
  type: "string",
@@ -2872,10 +2972,18 @@ const oauthProvider = (options) => {
2872
2972
  handler: createAuthMiddleware(async (ctx) => {
2873
2973
  const query = ctx.body.oauth_query;
2874
2974
  if (!await verifyOAuthQueryParams(query, ctx.context.secret)) throw new APIError("BAD_REQUEST", { error: "invalid_signature" });
2975
+ const signedQueryIssuedAt = getSignedQueryIssuedAt(query);
2875
2976
  const queryParams = new URLSearchParams(query);
2977
+ const postLoginClearedForSession = queryParams.get("ba_pl") ?? void 0;
2876
2978
  queryParams.delete("sig");
2877
2979
  queryParams.delete("exp");
2878
- await oAuthState.set({ query: queryParams.toString() });
2980
+ queryParams.delete(signedQueryIssuedAtParam);
2981
+ queryParams.delete(postLoginClearedParam);
2982
+ await oAuthState.set({
2983
+ query: queryParams.toString(),
2984
+ signedQueryIssuedAt: signedQueryIssuedAt ?? void 0,
2985
+ postLoginClearedForSession
2986
+ });
2879
2987
  if (ctx.path === "/sign-in/social") {
2880
2988
  if (ctx.body.additionalData?.query) return;
2881
2989
  if (!ctx.body.additionalData) ctx.body.additionalData = {};
@@ -2899,7 +3007,7 @@ const oauthProvider = (options) => {
2899
3007
  const secFetchMode = ctx.request?.headers?.get("sec-fetch-mode")?.toLowerCase();
2900
3008
  const acceptHeader = ctx.request?.headers?.get("accept")?.toLowerCase() ?? "";
2901
3009
  if (!(secFetchMode === "navigate" || !secFetchMode && (acceptHeader.includes("text/html") || acceptHeader.includes("application/xhtml+xml")))) ctx.headers?.set("accept", "application/json");
2902
- ctx.query = deleteFromPrompt(query, "login");
3010
+ ctx.query = searchParamsToQuery(removePromptFromQuery(query, "login"));
2903
3011
  return await authorizeEndpoint(ctx, opts);
2904
3012
  })
2905
3013
  }]
@@ -2927,19 +3035,19 @@ const oauthProvider = (options) => {
2927
3035
  if (opts.scopes && !opts.scopes.includes("openid")) throw new APIError("NOT_FOUND");
2928
3036
  return oidcServerMetadata(ctx, opts);
2929
3037
  }),
2930
- oauth2Authorize: createAuthEndpoint("/oauth2/authorize", {
3038
+ oauth2Authorize: createOAuthEndpoint("/oauth2/authorize", {
2931
3039
  method: "GET",
2932
3040
  query: z.object({
2933
- response_type: z.enum(["code"]).optional(),
3041
+ response_type: z.string().pipe(z.enum(["code"])).optional(),
2934
3042
  client_id: z.string(),
2935
3043
  redirect_uri: SafeUrlSchema.optional(),
2936
3044
  scope: z.string().optional(),
2937
3045
  state: z.string().optional(),
2938
3046
  request_uri: z.string().optional(),
2939
3047
  code_challenge: z.string().optional(),
2940
- code_challenge_method: z.enum(["S256"]).optional(),
3048
+ code_challenge_method: z.string().pipe(z.enum(["S256"])).optional(),
2941
3049
  nonce: z.string().optional(),
2942
- prompt: z.enum([
3050
+ prompt: z.string().pipe(z.enum([
2943
3051
  "none",
2944
3052
  "consent",
2945
3053
  "login",
@@ -2947,8 +3055,10 @@ const oauthProvider = (options) => {
2947
3055
  "select_account",
2948
3056
  "login consent",
2949
3057
  "select_account consent"
2950
- ]).optional()
3058
+ ])).optional()
2951
3059
  }),
3060
+ redirectOnError: authorizeRedirectOnError(opts),
3061
+ errorCodesByField: { response_type: { invalid: "unsupported_response_type" } },
2952
3062
  metadata: { openapi: {
2953
3063
  description: "Authorize an OAuth2 request",
2954
3064
  parameters: [
@@ -3107,14 +3217,14 @@ const oauthProvider = (options) => {
3107
3217
  }, async (ctx) => {
3108
3218
  return continueEndpoint(ctx, opts);
3109
3219
  }),
3110
- oauth2Token: createAuthEndpoint("/oauth2/token", {
3220
+ oauth2Token: createOAuthEndpoint("/oauth2/token", {
3111
3221
  method: "POST",
3112
3222
  body: z.object({
3113
- grant_type: z.enum([
3223
+ grant_type: z.string().pipe(z.enum([
3114
3224
  "authorization_code",
3115
3225
  "client_credentials",
3116
3226
  "refresh_token"
3117
- ]),
3227
+ ])),
3118
3228
  client_id: z.string().optional(),
3119
3229
  client_secret: z.string().optional(),
3120
3230
  client_assertion: z.string().optional(),
@@ -3126,6 +3236,10 @@ const oauthProvider = (options) => {
3126
3236
  resource: z.string().optional(),
3127
3237
  scope: z.string().optional()
3128
3238
  }),
3239
+ errorCodesByField: { grant_type: {
3240
+ missing: "invalid_request",
3241
+ invalid: "unsupported_grant_type"
3242
+ } },
3129
3243
  metadata: {
3130
3244
  allowedMediaTypes: ["application/x-www-form-urlencoded"],
3131
3245
  openapi: {
@@ -3238,7 +3352,7 @@ const oauthProvider = (options) => {
3238
3352
  }, async (ctx) => {
3239
3353
  return tokenEndpoint(ctx, opts);
3240
3354
  }),
3241
- oauth2Introspect: createAuthEndpoint("/oauth2/introspect", {
3355
+ oauth2Introspect: createOAuthEndpoint("/oauth2/introspect", {
3242
3356
  method: "POST",
3243
3357
  body: z.object({
3244
3358
  client_id: z.string().optional(),
@@ -3246,7 +3360,7 @@ const oauthProvider = (options) => {
3246
3360
  client_assertion: z.string().optional(),
3247
3361
  client_assertion_type: z.string().optional(),
3248
3362
  token: z.string(),
3249
- token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
3363
+ token_type_hint: z.string().optional()
3250
3364
  }),
3251
3365
  metadata: {
3252
3366
  allowedMediaTypes: ["application/x-www-form-urlencoded"],
@@ -3271,8 +3385,7 @@ const oauthProvider = (options) => {
3271
3385
  },
3272
3386
  token_type_hint: {
3273
3387
  type: "string",
3274
- enum: ["access_token", "refresh_token"],
3275
- description: "Hint about the type of the token submitted for introspection"
3388
+ description: "Hint about the token type. Recognized values: `access_token`, `refresh_token`."
3276
3389
  },
3277
3390
  resource: {
3278
3391
  type: "string",
@@ -3358,7 +3471,7 @@ const oauthProvider = (options) => {
3358
3471
  }, async (ctx) => {
3359
3472
  return introspectEndpoint(ctx, opts);
3360
3473
  }),
3361
- oauth2Revoke: createAuthEndpoint("/oauth2/revoke", {
3474
+ oauth2Revoke: createOAuthEndpoint("/oauth2/revoke", {
3362
3475
  method: "POST",
3363
3476
  body: z.object({
3364
3477
  client_id: z.string().optional(),
@@ -3366,7 +3479,7 @@ const oauthProvider = (options) => {
3366
3479
  client_assertion: z.string().optional(),
3367
3480
  client_assertion_type: z.string().optional(),
3368
3481
  token: z.string(),
3369
- token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
3482
+ token_type_hint: z.string().optional()
3370
3483
  }),
3371
3484
  metadata: {
3372
3485
  allowedMediaTypes: ["application/x-www-form-urlencoded"],
@@ -3391,8 +3504,7 @@ const oauthProvider = (options) => {
3391
3504
  },
3392
3505
  token_type_hint: {
3393
3506
  type: "string",
3394
- enum: ["access_token", "refresh_token"],
3395
- description: "Hint about the type of the token submitted for revocation"
3507
+ description: "Hint about the token type. Recognized values: `access_token`, `refresh_token`."
3396
3508
  }
3397
3509
  },
3398
3510
  required: ["token"]
@@ -3513,7 +3625,7 @@ const oauthProvider = (options) => {
3513
3625
  }, async (ctx) => {
3514
3626
  return userInfoEndpoint(ctx, opts);
3515
3627
  }),
3516
- oauth2EndSession: createAuthEndpoint("/oauth2/end-session", {
3628
+ oauth2EndSession: createOAuthEndpoint("/oauth2/end-session", {
3517
3629
  method: "GET",
3518
3630
  query: z.object({
3519
3631
  id_token_hint: z.string(),
@@ -3544,7 +3656,7 @@ const oauthProvider = (options) => {
3544
3656
  }, async (ctx) => {
3545
3657
  return rpInitiatedLogoutEndpoint(ctx, opts);
3546
3658
  }),
3547
- registerOAuthClient: createAuthEndpoint("/oauth2/register", {
3659
+ registerOAuthClient: createOAuthEndpoint("/oauth2/register", {
3548
3660
  method: "POST",
3549
3661
  body: z.object({
3550
3662
  redirect_uris: z.array(SafeUrlSchema).min(1).min(1),
@@ -3581,6 +3693,12 @@ const oauthProvider = (options) => {
3581
3693
  subject_type: z.enum(["public", "pairwise"]).optional(),
3582
3694
  skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
3583
3695
  }),
3696
+ errorCodesByField: {
3697
+ redirect_uris: "invalid_redirect_uri",
3698
+ post_logout_redirect_uris: "invalid_redirect_uri",
3699
+ software_statement: "invalid_software_statement"
3700
+ },
3701
+ defaultError: "invalid_client_metadata",
3584
3702
  metadata: { openapi: {
3585
3703
  description: "Register an OAuth2 application",
3586
3704
  responses: { "200": {
@@ -3773,17 +3891,37 @@ const oauthProvider = (options) => {
3773
3891
  //#endregion
3774
3892
  //#region src/authorize.ts
3775
3893
  /**
3776
- * Formats an error url
3894
+ * Formats an error url. Per OIDC Core 1.0 §5 / RFC 6749 §4.2.2.1, errors on
3895
+ * implicit and hybrid flows are delivered in the URL fragment, not the query.
3896
+ * Callers on the code flow (default) omit `mode` and get query delivery.
3777
3897
  */
3778
- function formatErrorURL(url, error, description, state, iss) {
3898
+ function formatErrorURL(url, error, description, state, iss, mode = "query") {
3779
3899
  const searchParams = new URLSearchParams({
3780
3900
  error,
3781
3901
  error_description: description
3782
3902
  });
3783
3903
  state && searchParams.append("state", state);
3784
3904
  iss && searchParams.append("iss", iss);
3905
+ if (mode === "fragment") return `${url}#${searchParams.toString()}`;
3785
3906
  return `${url}${url.includes("?") ? "&" : "?"}${searchParams.toString()}`;
3786
3907
  }
3908
+ /**
3909
+ * Selects the response mode for an error redirect to the RP. OIDC Core 1.0 §5
3910
+ * defines defaults based on response_type: `code` → query, types containing
3911
+ * `token` / `id_token` → fragment. An explicit `response_mode` overrides.
3912
+ *
3913
+ * When `response_type` is duplicated (array) or absent, we can't trust the
3914
+ * caller's intent, so we default to query — the safer channel for
3915
+ * unrecognized shapes.
3916
+ */
3917
+ function deriveResponseMode(raw) {
3918
+ const responseMode = typeof raw.response_mode === "string" ? raw.response_mode : void 0;
3919
+ if (responseMode === "fragment") return "fragment";
3920
+ if (responseMode === "query") return "query";
3921
+ const responseType = typeof raw.response_type === "string" ? raw.response_type : void 0;
3922
+ if (responseType && /\b(token|id_token)\b/.test(responseType)) return "fragment";
3923
+ return "query";
3924
+ }
3787
3925
  const handleRedirect = (ctx, uri) => {
3788
3926
  const fromFetch = isBrowserFetchRequest(ctx.request?.headers);
3789
3927
  const acceptJson = ctx.headers?.get("accept")?.includes("application/json");
@@ -3807,8 +3945,7 @@ function redirectWithPromptNoneError(ctx, opts, query, error, description) {
3807
3945
  function validateIssuerUrl(issuer) {
3808
3946
  try {
3809
3947
  const url = new URL(issuer);
3810
- const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
3811
- if (url.protocol !== "https:" && !isLocalhost) url.protocol = "https:";
3948
+ if (url.protocol !== "https:" && !isLoopbackHost(url.host)) url.protocol = "https:";
3812
3949
  url.search = "";
3813
3950
  url.hash = "";
3814
3951
  return url.toString().replace(/\/$/, "");
@@ -3836,6 +3973,64 @@ function getIssuer(ctx, opts) {
3836
3973
  function getErrorURL(ctx, error, description) {
3837
3974
  return formatErrorURL(ctx.context.options.onAPIError?.errorURL || `${ctx.context.baseURL}/error`, error, description);
3838
3975
  }
3976
+ /**
3977
+ * Finds the matching entry in a client's registered redirect_uris for a
3978
+ * requested redirect_uri. Honors RFC 8252 §7.3 loopback port variance for
3979
+ * the full 127.0.0.0/8 range and [::1], matching on scheme+host+path+query
3980
+ * and ignoring port. DNS names like "localhost" are excluded per §8.3.
3981
+ */
3982
+ function findRegisteredRedirectUri(registered, requested) {
3983
+ if (!registered || !requested) return void 0;
3984
+ let req;
3985
+ try {
3986
+ req = new URL(requested);
3987
+ } catch {}
3988
+ return registered.find((url) => {
3989
+ if (url === requested) return true;
3990
+ if (!req) return false;
3991
+ try {
3992
+ const reg = new URL(url);
3993
+ return isLoopbackIP(reg.hostname) && reg.hostname === req.hostname && reg.pathname === req.pathname && reg.protocol === req.protocol && reg.search === req.search;
3994
+ } catch {
3995
+ return false;
3996
+ }
3997
+ });
3998
+ }
3999
+ /**
4000
+ * Loads the client, verifies it's enabled, and returns the requested
4001
+ * redirect_uri when it matches a registered entry. Returns null whenever the
4002
+ * RP cannot be safely reached, so callers can fall back to the server error
4003
+ * page (avoiding open-redirect risk on validation failures).
4004
+ */
4005
+ async function resolveTrustedRedirectUri(ctx, opts, clientId, redirectUri) {
4006
+ if (!clientId || !redirectUri) return null;
4007
+ let client;
4008
+ try {
4009
+ client = await getClient(ctx, opts, clientId);
4010
+ } catch {
4011
+ return null;
4012
+ }
4013
+ if (!client || client.disabled) return null;
4014
+ return findRegisteredRedirectUri(client.redirectUris, redirectUri) ? redirectUri : null;
4015
+ }
4016
+ /**
4017
+ * `redirectOnError` callback for `/oauth2/authorize`. Per RFC 6749 §4.1.2.1,
4018
+ * authorize errors MUST be delivered to the client's `redirect_uri` with
4019
+ * `error`, `error_description`, `state`, and (RFC 9207) `iss`. The clause
4020
+ * carves out one case: a missing/invalid `redirect_uri` or `client_id` MUST
4021
+ * NOT redirect to the requested URI. We implement the carve-out via
4022
+ * `resolveTrustedRedirectUri`, falling back to the server error page.
4023
+ *
4024
+ * Channel (query vs fragment) follows OIDC Core §5 via `deriveResponseMode`.
4025
+ */
4026
+ function authorizeRedirectOnError(opts) {
4027
+ return async ({ error, error_description, ctx }) => {
4028
+ const raw = ctx.query ?? {};
4029
+ const trusted = await resolveTrustedRedirectUri(ctx, opts, typeof raw.client_id === "string" ? raw.client_id : void 0, typeof raw.redirect_uri === "string" ? raw.redirect_uri : void 0);
4030
+ if (trusted) return handleRedirect(ctx, formatErrorURL(trusted, error, error_description, typeof raw.state === "string" ? raw.state : void 0, getIssuer(ctx, opts), deriveResponseMode(raw)));
4031
+ return handleRedirect(ctx, getErrorURL(ctx, error, error_description));
4032
+ };
4033
+ }
3839
4034
  async function authorizeEndpoint(ctx, opts, settings) {
3840
4035
  if (opts.grantTypes && !opts.grantTypes.includes("authorization_code")) throw new APIError$1("NOT_FOUND");
3841
4036
  if (!ctx.request) throw new APIError$1("UNAUTHORIZED", {
@@ -3866,15 +4061,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
3866
4061
  const client = await getClient(ctx, opts, query.client_id);
3867
4062
  if (!client) return handleRedirect(ctx, getErrorURL(ctx, "invalid_client", "client_id is required"));
3868
4063
  if (client.disabled) return handleRedirect(ctx, getErrorURL(ctx, "client_disabled", "client is disabled"));
3869
- if (!client.redirectUris?.find((url) => {
3870
- if (url === query.redirect_uri) return true;
3871
- try {
3872
- const registered = new URL(url);
3873
- const requested = new URL(query.redirect_uri);
3874
- if ((registered.hostname === "127.0.0.1" || registered.hostname === "[::1]") && registered.hostname === requested.hostname && registered.pathname === requested.pathname && registered.protocol === requested.protocol && registered.search === requested.search) return true;
3875
- } catch {}
3876
- return false;
3877
- }) || !query.redirect_uri) return handleRedirect(ctx, getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
4064
+ if (!findRegisteredRedirectUri(client.redirectUris, query.redirect_uri) || !query.redirect_uri) return handleRedirect(ctx, getErrorURL(ctx, "invalid_redirect", "invalid redirect uri"));
3878
4065
  let requestedScopes = query.scope?.split(" ").filter((s) => s);
3879
4066
  if (requestedScopes) {
3880
4067
  const validScopes = new Set(client.scopes ?? opts.scopes);
@@ -3921,7 +4108,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
3921
4108
  });
3922
4109
  if (signupRedirect) {
3923
4110
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "interaction_required", "End-User interaction is required");
3924
- return redirectWithPromptCode(ctx, opts, "create", typeof signupRedirect === "string" ? signupRedirect : void 0);
4111
+ return redirectWithPromptCode(ctx, opts, "create", { page: typeof signupRedirect === "string" ? signupRedirect : void 0 });
3925
4112
  }
3926
4113
  }
3927
4114
  if (!settings?.postLogin && opts.postLogin) {
@@ -3935,7 +4122,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
3935
4122
  return redirectWithPromptCode(ctx, opts, "post_login");
3936
4123
  }
3937
4124
  }
3938
- if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent");
4125
+ if (promptSet?.has("consent")) return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
3939
4126
  const referenceId = await opts.postLogin?.consentReferenceId?.({
3940
4127
  user: session.user,
3941
4128
  session: session.session,
@@ -3968,7 +4155,7 @@ async function authorizeEndpoint(ctx, opts, settings) {
3968
4155
  });
3969
4156
  if (!consent || !requestedScopes.every((val) => consent.scopes.includes(val))) {
3970
4157
  if (promptNone) return redirectWithPromptNoneError(ctx, opts, query, "consent_required", "End-User consent is required");
3971
- return redirectWithPromptCode(ctx, opts, "consent");
4158
+ return redirectWithPromptCode(ctx, opts, "consent", { sessionId: session.session.id });
3972
4159
  }
3973
4160
  return redirectWithAuthorizationCode(ctx, opts, {
3974
4161
  query,
@@ -4015,8 +4202,8 @@ async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
4015
4202
  redirectUriWithCode.searchParams.set("iss", getIssuer(ctx, opts));
4016
4203
  return handleRedirect(ctx, redirectUriWithCode.toString());
4017
4204
  }
4018
- async function redirectWithPromptCode(ctx, opts, type, page) {
4019
- const queryParams = await signParams(ctx, opts);
4205
+ async function redirectWithPromptCode(ctx, opts, type, options) {
4206
+ const queryParams = await signParams(ctx, opts, { postLoginClearedForSession: type === "consent" && opts.postLogin ? options?.sessionId : void 0 });
4020
4207
  let path = opts.loginPage;
4021
4208
  if (type === "select_account") path = opts.selectAccount?.page ?? opts.loginPage;
4022
4209
  else if (type === "post_login") {
@@ -4024,12 +4211,16 @@ async function redirectWithPromptCode(ctx, opts, type, page) {
4024
4211
  path = opts.postLogin?.page;
4025
4212
  } else if (type === "consent") path = opts.consentPage;
4026
4213
  else if (type === "create") path = opts.signup?.page ?? opts.loginPage;
4027
- return handleRedirect(ctx, `${page ?? path}?${queryParams}`);
4214
+ return handleRedirect(ctx, `${options?.page ?? path}?${queryParams}`);
4028
4215
  }
4029
- async function signParams(ctx, opts) {
4030
- const exp = Math.floor(Date.now() / 1e3) + (opts.codeExpiresIn ?? 600);
4216
+ async function signParams(ctx, opts, flags) {
4217
+ const issuedAt = Date.now();
4218
+ const exp = Math.floor(issuedAt / 1e3) + (opts.codeExpiresIn ?? 600);
4031
4219
  const params = serializeAuthorizationQuery(ctx.query);
4032
4220
  params.set("exp", String(exp));
4221
+ params.set(signedQueryIssuedAtParam, String(issuedAt));
4222
+ params.delete(postLoginClearedParam);
4223
+ if (flags?.postLoginClearedForSession) params.set(postLoginClearedParam, flags.postLoginClearedForSession);
4033
4224
  const signature = await makeSignature(params.toString(), ctx.context.secret);
4034
4225
  params.append("sig", signature);
4035
4226
  return params.toString();