@shware/security 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -430,6 +430,37 @@ export async function onAuthorized(request: Request, { attributes }: Authorizati
430
430
  - They come from the browser that started the flow: trust them like a cookie — fine for
431
431
  attribution or a post-login destination, not for authorization decisions.
432
432
 
433
+ #### Carrying data back to the app (`redirectParams`)
434
+
435
+ `attributes` stay on the server. What the page needs back goes in `redirectParams`: query
436
+ parameters added to the flow's redirect to `successUri` and to `errorUri` alike. For example, the
437
+ page that opened a sign-in popup can tag the attempt, and match the result against it when the
438
+ popup reports back without `window.opener` (cut off by a provider's
439
+ `Cross-Origin-Opener-Policy`):
440
+
441
+ ```ts
442
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
443
+
444
+ app.get(PATH.OAUTH2_AUTHORIZATION, (c) => {
445
+ // From the browser: checked here, since an oversized value is an invariant failure (a 500).
446
+ const attempt = c.req.query('attempt');
447
+ return auth.oauth2Authorization(c.req.raw, {
448
+ redirectParams: { attempt: attempt && UUID.test(attempt) ? attempt : undefined },
449
+ });
450
+ });
451
+ // → https://app.example.com/?status=success&provider=google&attempt=…
452
+ ```
453
+
454
+ - Stored with the authorization request like `attributes`, so every redirect of that flow carries
455
+ them, success or error, except the two that cannot find the request (no session, or no
456
+ authorization request in it).
457
+ - The names the redirects use themselves — `status`, `provider`, `error`, `error_description`,
458
+ `reason` — are dropped, even on a redirect that leaves one out (a cancel has no `reason`): the
459
+ page would read it as the SDK's. A refusal's metadata wins over a parameter of the same name.
460
+ - String values only; `undefined` values are dropped; at most 2 KB serialized, checked as an
461
+ invariant like `attributes`: validate a value taken from the request before passing it.
462
+ - They reach the browser and its history: put nothing secret in them.
463
+
433
464
  ### Native login
434
465
 
435
466
  Mobile apps sign in with the platform SDK and post the result to
@@ -37,15 +37,42 @@ const CALLBACK_ERROR_TYPES = /* @__PURE__ */ new Set([
37
37
  "access_denied",
38
38
  "temporarily_unavailable"
39
39
  ]);
40
- function authorizationAttributes(attributes) {
40
+ /**
41
+ * The host's values for the flow (`attributes`, `redirectParams`), `undefined` ones dropped; `name`
42
+ * is what the size invariant calls them.
43
+ */
44
+ function authorizationAttributes(attributes, name = "attributes") {
41
45
  if (!attributes) return void 0;
42
46
  const defined = {};
43
47
  for (const [key, value] of Object.entries(attributes)) if (value !== void 0) defined[key] = value;
44
48
  if (Object.keys(defined).length === 0) return void 0;
45
49
  const size = require_auth_attributes.attributesSize(defined);
46
- (0, _shware_utils.invariant)(size <= require_auth_attributes.MAX_ATTRIBUTES_BYTES, `oauth2 authorization attributes are ${size} bytes, at most ${require_auth_attributes.MAX_ATTRIBUTES_BYTES} allowed`);
50
+ (0, _shware_utils.invariant)(size <= require_auth_attributes.MAX_ATTRIBUTES_BYTES, `oauth2 authorization ${name} are ${size} bytes, at most ${require_auth_attributes.MAX_ATTRIBUTES_BYTES} allowed`);
47
51
  return defined;
48
52
  }
53
+ /**
54
+ * What the callback's redirects say themselves. A host's `redirectParams` never carry these, even
55
+ * on a redirect that leaves one out (a cancel has no `reason`): the page would read it as the SDK's.
56
+ */
57
+ const REDIRECT_PARAMS = /* @__PURE__ */ new Set([
58
+ "status",
59
+ "provider",
60
+ "error",
61
+ "error_description",
62
+ "reason"
63
+ ]);
64
+ /** The host's `redirectParams` without the names the redirects keep for themselves. */
65
+ function redirectParams(params) {
66
+ if (!params) return void 0;
67
+ const own = Object.entries(params).filter(([key]) => !REDIRECT_PARAMS.has(key));
68
+ return authorizationAttributes(Object.fromEntries(own), "redirectParams");
69
+ }
70
+ /** `base` with `params` added, none replacing one it carries itself. */
71
+ function withParams(base, params = {}) {
72
+ const uri = new URL(base);
73
+ for (const [key, value] of Object.entries(params)) if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);
74
+ return uri;
75
+ }
49
76
  /** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */
50
77
  function isFormPost(request) {
51
78
  const contentType = request.headers.get("content-type")?.toLowerCase();
@@ -186,11 +213,16 @@ var Auth = class {
186
213
  throw error;
187
214
  }
188
215
  }
189
- /** The error redirect of the callback flow; `refusal`'s reason and metadata ride along. */
190
- redirect(error, description, refusal) {
216
+ /**
217
+ * The error redirect of the callback flow: `refusal`'s reason and metadata ride along, and the
218
+ * flow's `redirectParams` once its authorization request is known.
219
+ */
220
+ redirect(error, { description, refusal, params } = {}) {
191
221
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
192
- const uri = new URL(this.oauth2Client.errorUri);
193
- for (const [key, value] of Object.entries(refusal?.metadata ?? {})) if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);
222
+ const uri = withParams(this.oauth2Client.errorUri, {
223
+ ...params,
224
+ ...refusal?.metadata
225
+ });
194
226
  uri.searchParams.set("error", error);
195
227
  if (description) uri.searchParams.set("error_description", description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));
196
228
  if (refusal) uri.searchParams.set("reason", refusal.reason);
@@ -293,6 +325,7 @@ var Auth = class {
293
325
  oauth2Authorization = (request, options) => this.guard(async () => {
294
326
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
295
327
  const attributes = authorizationAttributes(options?.attributes);
328
+ const params = redirectParams(options?.redirectParams);
296
329
  const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
297
330
  const { provider } = require_utils_http.param(request, PATH.OAUTH2_AUTHORIZATION);
298
331
  const state = (0, node_crypto.randomUUID)();
@@ -316,7 +349,8 @@ var Auth = class {
316
349
  authorizationRequestUri: uri.href,
317
350
  additionalParameters: pkce,
318
351
  ...nonce ? { nonce } : {},
319
- ...attributes ? { attributes } : {}
352
+ ...attributes ? { attributes } : {},
353
+ ...params ? { redirectParams: params } : {}
320
354
  };
321
355
  session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, JSON.stringify(authorizationRequest));
322
356
  await this.repository.save(session);
@@ -332,23 +366,42 @@ var Auth = class {
332
366
  (0, _shware_utils.invariant)(this.oauth2Client, "oauth2Client is not initialized");
333
367
  const { mark, setTiming } = require_utils_timing.timing({ enabled: this.timing });
334
368
  const session = await this.loadSession(request);
335
- if (!session) return this.redirect("invalid_request", "session not found");
369
+ if (!session) return this.redirect("invalid_request", { description: "session not found" });
336
370
  mark("load_session");
337
371
  const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
338
- if (!json) return this.redirect("invalid_request", "authorization request not found");
372
+ if (!json) return this.redirect("invalid_request", { description: "authorization request not found" });
339
373
  const cached = JSON.parse(json);
374
+ const params = cached.redirectParams;
340
375
  const { provider } = require_utils_http.param(request, PATH.LOGIN_OAUTH2_CODE);
341
376
  const data = isFormPost(request) ? Object.fromEntries(new URLSearchParams(await request.text())) : require_utils_http.query(request);
342
377
  const parsed = require_oauth2_client.oauth2RedirectQuerySchema.safeParse(data);
343
- if (!parsed.success) return this.redirect("invalid_request", "invalid redirect query");
378
+ if (!parsed.success) return this.redirect("invalid_request", {
379
+ description: "invalid redirect query",
380
+ params
381
+ });
344
382
  if (parsed.data.error) {
345
- if (parsed.data.state && parsed.data.state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
346
- return this.redirect(safeErrorType(parsed.data.error, "server_error"), parsed.data.error_description);
383
+ if (parsed.data.state && parsed.data.state !== cached.state) return this.redirect("invalid_request", {
384
+ description: "redirect state mismatch",
385
+ params
386
+ });
387
+ return this.redirect(safeErrorType(parsed.data.error, "server_error"), {
388
+ description: parsed.data.error_description,
389
+ params
390
+ });
347
391
  }
348
392
  const { code, state, user } = parsed.data;
349
- if (!code || !state) return this.redirect("invalid_request", "invalid redirect query");
350
- if (state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
351
- if (provider !== cached.provider) return this.redirect("invalid_request", "redirect provider mismatch");
393
+ if (!code || !state) return this.redirect("invalid_request", {
394
+ description: "invalid redirect query",
395
+ params
396
+ });
397
+ if (state !== cached.state) return this.redirect("invalid_request", {
398
+ description: "redirect state mismatch",
399
+ params
400
+ });
401
+ if (provider !== cached.provider) return this.redirect("invalid_request", {
402
+ description: "redirect provider mismatch",
403
+ params
404
+ });
352
405
  mark("validate_session");
353
406
  let signedIn;
354
407
  try {
@@ -385,16 +438,19 @@ var Auth = class {
385
438
  if (error instanceof require_oauth2_error.OAuth2Error) {
386
439
  console.warn("OAuth2 callback failed", error);
387
440
  const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : "server_error";
388
- return this.redirect(type);
441
+ return this.redirect(type, { params });
389
442
  }
390
443
  if (error instanceof require_auth_error.AuthenticationError) {
391
444
  console.warn("OAuth2 sign-in refused", error.reason, error.metadata);
392
- return this.redirect("access_denied", void 0, error);
445
+ return this.redirect("access_denied", {
446
+ refusal: error,
447
+ params
448
+ });
393
449
  }
394
450
  console.error("OAuth2 callback failed", error);
395
- return this.redirect("server_error");
451
+ return this.redirect("server_error", { params });
396
452
  }
397
- const uri = new URL(this.oauth2Client.successUri);
453
+ const uri = withParams(this.oauth2Client.successUri, params);
398
454
  uri.searchParams.set("status", "success");
399
455
  uri.searchParams.set("provider", provider);
400
456
  const response = new Response(null, {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["isKnownOAuth2ErrorType","attributesSize","MAX_ATTRIBUTES_BYTES","extend","googleOneTapSchema","attributesSchema","object","string","regex","literal","EmailVerificationCodes","DesktopCodes","OAuth2Client","getCookie","PRINCIPAL_NAME_INDEX_NAME","errorResponse","randomUUID","randomBytes","createHash","AuthenticationError","param","timing","query","oauth2RedirectQuerySchema","OAuth2Error","readJson","bodyAttributes","invalidArgument","readForm","parse","sendEmailVerificationCodeSchema","verifyTurnstileToken","loginEmailSchema","unauthenticated"],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomBytes, randomUUID } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport { extend, literal, object, regex, string } from 'zod/mini';\nimport type { Principal } from '../core/index';\nimport { loginEmailSchema, sendEmailVerificationCodeSchema } from '../email/schema';\nimport { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport { OAuth2Error, type OAuth2ErrorType, isKnownOAuth2ErrorType } from '../oauth2/error';\nimport type {\n NativeCredential,\n OAuth2AuthorizationRequest,\n PkceParameters,\n StandardClaims,\n} from '../oauth2/types';\nimport { PRINCIPAL_NAME_INDEX_NAME } from '../session/common';\nimport type { KVRepository, Session, SessionRepository } from '../session/types';\nimport {\n type CookieOptions,\n deleteCookie,\n getCookie,\n param,\n query,\n setCookie,\n} from '../utils/http';\nimport { timing } from '../utils/timing';\nimport { parse, readForm, readJson, verifyTurnstileToken } from '../utils/valid';\nimport {\n MAX_ATTRIBUTES_BYTES,\n attributesSchema,\n attributesSize,\n bodyAttributes,\n} from './attributes';\nimport { DesktopCodes } from './desktop-codes';\nimport { type EmailSender, EmailVerificationCodes } from './email-codes';\nimport { AuthenticationError } from './error';\nimport { errorResponse, invalidArgument, unauthenticated } from './respond';\nimport type {\n AuthConfig,\n AuthService,\n Authorization,\n AuthorizedHandler,\n LoggedHandler,\n LogoutOptions,\n OAuth2AuthorizationOptions,\n} from './types';\n\n// PKCE per RFC 7636: code_verifier / code_challenge are 43-128 chars from\n// the unreserved set. We restrict to base64url (drops \".\" and \"~\") since\n// that's what S256 produces and what callers should be sending; rejecting\n// the rest shrinks the accepted input shape without losing legit clients.\n// A 32-byte random verifier base64url-encodes to exactly 43 chars.\nconst PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;\n\n// A native app's `code_verifier` goes on to the provider, which checks it against the challenge\n// the app sent: accept the whole RFC 7636 unreserved set here, as the provider does.\nconst CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;\n\n/** The header a session id travels in besides the cookie, with `session.header` on. */\nconst SESSION_HEADER = 'X-Auth-Token';\n\n/** Seconds an OAuth2 state or nonce may be used within. */\nconst OAUTH2_PARAMETER_TTL = 10 * 60;\n\n// error_description forwarded to errorUri comes from the provider or an error message: cap it so\n// the redirect URL stays short and an attacker-crafted callback cannot stuff the page with text.\nconst MAX_ERROR_DESCRIPTION_LENGTH = 256;\n\n/** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */\nfunction safeErrorType(error: string, fallback: OAuth2ErrorType): OAuth2ErrorType {\n return isKnownOAuth2ErrorType(error) ? error : fallback;\n}\n\n/**\n * The codes a failure past validation keeps on errorUri, each one the user can act on: the code\n * expired or was used (start again), they declined, or the provider is briefly down. Every other\n * failure there (a rejected client, a user info request that failed) is the app's or the\n * provider's problem, which the user sees as `server_error`.\n */\nconst CALLBACK_ERROR_TYPES: ReadonlySet<OAuth2ErrorType> = new Set([\n 'invalid_grant',\n 'access_denied',\n 'temporarily_unavailable',\n]);\n\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes']\n): Record<string, string> | undefined {\n if (!attributes) return undefined;\n const defined: Record<string, string> = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined) defined[key] = value;\n }\n if (Object.keys(defined).length === 0) return undefined;\n // The host's own values, written with the session: oversized is a bug in the host, not a 400.\n const size = attributesSize(defined);\n invariant(\n size <= MAX_ATTRIBUTES_BYTES,\n `oauth2 authorization attributes are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\nfunction isFormPost(request: Request) {\n const contentType = request.headers.get('content-type')?.toLowerCase();\n return (\n request.method === 'POST' && !!contentType?.startsWith('application/x-www-form-urlencoded')\n );\n}\n\n/** Google's own form post carries no attributes; an app posting JSON may add them. */\nconst oneTapBodySchema = extend(googleOneTapSchema, { attributes: attributesSchema });\n\nconst desktopAuthorizeSchema = object({\n code_challenge: string().check(regex(PKCE_FORMAT)),\n code_challenge_method: literal('S256'),\n});\n\nconst desktopExchangeSchema = object({\n code: string(),\n code_verifier: string().check(regex(PKCE_FORMAT)),\n});\n\nexport const PATH = {\n CSRF: '/csrf',\n LOGOUT: '/logout',\n LOGGED: '/logged',\n\n OAUTH2_STATE: '/oauth2/state/:provider',\n OAUTH2_NONCE: '/oauth2/nonce/:provider',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:provider',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:provider',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:provider',\n LOGIN_OAUTH2_ONETAP: '/login/oauth2/onetap/google',\n\n LOGIN_EMAIL: '/login/email',\n SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\n\n // Desktop / native app sign-in via web loopback (RFC 8252 + OAuth 2.1 Auth Code Flow):\n // 1. user already authenticated with cookie session → POST DESKTOP_AUTHORIZE → { code }\n // 2. web redirects to desktop's loopback callback with the code\n // 3. desktop renderer POSTs DESKTOP_EXCHANGE with the code → server returns\n // Set-Cookie carrying a fresh session, single-use code is invalidated\n // The two-step flow keeps the actual session id off the redirect URL — only a\n // 5-minute single-use code transits via the loopback.\n DESKTOP_AUTHORIZE: '/auth/desktop/authorize',\n DESKTOP_EXCHANGE: '/auth/desktop/exchange',\n\n CLEANUP_EXPIRED_SESSIONS: '/sessions/expired/cleanup',\n} as const;\n\n/**\n * The login routes and session helpers. Every route is one straight line: read and validate the\n * request, check the credential, call the host's `onAuthorized`, sign in. Failures are thrown and\n * answered in one place per kind of route: a JSON route runs under `guard`, which maps a\n * `RequestError`, an `AuthenticationError` or an `OAuth2Error` to its google.rpc body (see\n * `respond.ts`); the redirect callback maps the same errors to `errorUri` parameters (`redirect`).\n */\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly kv: KVRepository;\n private readonly repository: SessionRepository;\n private readonly turnstileSecretKey?: string;\n private readonly emailCodes: EmailVerificationCodes;\n private readonly desktopCodes: DesktopCodes;\n private readonly oauth2Client: OAuth2Client | null;\n /** Provider keys whose logins may come without a nonce (`oauth2.nonce.optional`). */\n private readonly nonceOptional: ReadonlySet<string>;\n private readonly touchInterval: number;\n private readonly sessionHeader: boolean;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\n oauth2,\n cookie,\n session,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const maxAttempts = verificationCode?.maxAttempts ?? 5;\n const resendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(maxAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(resendCooldown >= 1, 'verificationCode.resendCooldown must be at least 1 second');\n this.emailCodes = new EmailVerificationCodes(kvRepository, { maxAttempts, resendCooldown });\n this.desktopCodes = new DesktopCodes(kvRepository);\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n this.nonceOptional = new Set(oauth2?.nonce?.optional);\n this.touchInterval = (session?.touchInterval ?? 3600) * 1000;\n this.sessionHeader = session?.header ?? false;\n invariant(this.touchInterval >= 0, 'session.touchInterval must not be negative');\n }\n\n // ---- sessions and responses ------------------------------------------------------------------\n\n private getSessionId(request: Request): string | undefined {\n const fromCookie = getCookie(request, this.cookieName);\n if (fromCookie) return fromCookie;\n return (this.sessionHeader && request.headers.get(SESSION_HEADER)) || undefined;\n }\n\n /** The session the request carries, if it exists. */\n private async loadSession(request: Request): Promise<Session | null> {\n const sessionId = this.getSessionId(request);\n return sessionId ? this.repository.findById(sessionId) : null;\n }\n\n /** The signed-in principal of a session, if any. */\n private principalOf(session: Session): Principal | null {\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n return name ? { name: String(name) } : null;\n }\n\n /**\n * A new session signed in as `principal`. With `carryOver`, what the host kept in the pre-login\n * session moves to the new one; the authorization request (state, PKCE verifier, flow\n * attributes) is single-use and was consumed, and a principal left there from an earlier\n * sign-in is replaced. The new id is never the one the browser brought, which may have been\n * planted by an attacker who started the flow (session fixation).\n */\n private async signIn(principal: Principal, carryOver?: Session): Promise<Session> {\n const session = this.repository.createSession();\n for (const attribute of carryOver?.getAttributeNames() ?? []) {\n if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;\n if (attribute === PRINCIPAL_NAME_INDEX_NAME) continue;\n const value = carryOver?.getAttribute(attribute);\n if (value !== null && value !== undefined) session.setAttribute(attribute, value);\n }\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n return session;\n }\n\n /**\n * `response` carrying `session`: its cookie, renewed for the whole inactive interval, and with\n * `session.header` on, the `X-Auth-Token` header a client without a cookie jar reads.\n */\n private withSession(response: Response, session: Session, cookie?: CookieOptions): Response {\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n ...cookie,\n maxAge,\n });\n if (this.sessionHeader) response.headers.set(SESSION_HEADER, session.getId());\n return response;\n }\n\n /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */\n private async guard(run: () => Promise<Response>): Promise<Response> {\n try {\n return await run();\n } catch (error) {\n const response = errorResponse(error);\n if (response) return response;\n throw error;\n }\n }\n\n /** The error redirect of the callback flow; `refusal`'s reason and metadata ride along. */\n private redirect(\n error: OAuth2ErrorType,\n description?: string,\n refusal?: AuthenticationError\n ): Response {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const uri = new URL(this.oauth2Client.errorUri);\n // Metadata never replaces a parameter: not one errorUri carries itself, nor one set below.\n for (const [key, value] of Object.entries(refusal?.metadata ?? {})) {\n if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);\n }\n uri.searchParams.set('error', error);\n if (description) {\n uri.searchParams.set('error_description', description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));\n }\n if (refusal) uri.searchParams.set('reason', refusal.reason);\n return Response.redirect(uri.href, 302);\n }\n\n // ---- basic -----------------------------------------------------------------------------------\n\n csrf = async (_request: Request): Promise<Response> => {\n const token = randomUUID();\n const response = Response.json({\n token,\n parameterName: '_csrf',\n headerName: 'X-XSRF-TOKEN',\n });\n\n // Double Submit Cookie Protection\n setCookie(response, 'XSRF-TOKEN', token, {\n path: '/',\n secure: true,\n httpOnly: false,\n sameSite: 'none',\n domain: this.cookieOptions.domain,\n });\n return response;\n };\n\n logout = async (request: Request, options?: LogoutOptions): Promise<Response> => {\n const scope = options?.scope ?? 'local';\n const sessionId = this.getSessionId(request);\n // `global` and `others` reach the principal's other sessions through the one the request\n // carries; a request without a signed-in session has no principal to widen to, and ends as\n // `local` would.\n if (scope !== 'local' && sessionId) {\n const session = await this.repository.findById(sessionId);\n const principal = session && this.principalOf(session);\n if (principal) {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const id of sessions.keys()) {\n if (id !== sessionId) await this.repository.deleteById(id);\n }\n }\n }\n const response = Response.json({});\n if (scope === 'others') return response;\n if (sessionId) await this.repository.deleteById(sessionId);\n // A Set-Cookie only replaces the stored cookie when Domain and Path match (RFC 6265),\n // so clear it with the same attributes it was set with.\n deleteCookie(response, this.cookieName, this.cookieOptions);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n const session = await this.loadSession(request);\n if (!session) return Response.json({ data: false });\n\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n const principal = this.principalOf(session);\n if (principal) await onLogged?.(principal);\n return this.withSession(Response.json({ data: principal !== null }), session);\n };\n\n // ---- oauth2 ----------------------------------------------------------------------------------\n\n private createPkceParameters(): PkceParameters {\n // 32 random bytes: a 43-char base64url verifier, the RFC 7636 minimum (a UUID is 36).\n const code_verifier = randomBytes(32).toString('base64url');\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n private getOauth2NonceKey(nonce: string) {\n return `oauth2:nonce:${nonce}`;\n }\n\n /**\n * Spends a nonce `oauth2Nonce` issued for `provider`: `NONCE_MISMATCH` for one that was not,\n * or was used already. An id token is replayable until it expires; the nonce it carries makes\n * it sign in once.\n */\n private async consumeNonce(nonce: string, provider: string): Promise<void> {\n const key = this.getOauth2NonceKey(nonce);\n if ((await this.kv.getItem(key)) !== provider) throw new AuthenticationError('NONCE_MISMATCH');\n await this.kv.removeItem(key);\n }\n\n /**\n * The nonce check of the JSON logins (One Tap, native), where the client obtained the id token\n * and the nonce came from `oauth2Nonce`: spends the one the token carries, and refuses a token\n * without one when the provider's flow echoes nonces (`required`) and the host has not listed the\n * provider as optional. An id token is replayable until it expires; the nonce makes it sign in\n * once.\n */\n private async checkIssuedNonce(\n userInfo: Authorization['userInfo'],\n provider: string,\n required: boolean\n ): Promise<boolean> {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce === 'string' && nonce) {\n await this.consumeNonce(nonce, provider);\n return true;\n }\n if (required && !this.nonceOptional.has(provider)) {\n throw new AuthenticationError('NONCE_MISSING');\n }\n return false;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), provider, OAUTH2_PARAMETER_TTL);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, OAUTH2_PARAMETER_TTL);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n // Sent only to a provider whose id token echoes it: the callback then requires it back, so\n // a code that was swapped in for another sign-in's yields a token the session refuses.\n const nonce = (await this.oauth2Client.nonceSupport(provider)).web ? randomUUID() : undefined;\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, state, pkce, nonce });\n let session = await this.loadSession(request);\n if (!session) {\n // A session made for the flow only has to outlive the state: an abandoned flow must not\n // keep a row for the default interval. Sign-in moves to a new session, with the default.\n session = this.repository.createSession();\n session.setMaxInactiveInterval(OAUTH2_PARAMETER_TTL);\n }\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(nonce ? { nonce } : {}),\n ...(attributes ? { attributes } : {}),\n };\n session.setAttribute(\n this.ATTR_OAUTH2_AUTHORIZATION_REQUEST,\n JSON.stringify(authorizationRequest)\n );\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, session);\n });\n\n loginOAuth2Code = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. the session the flow started on, and the authorization request it stored\n const session = await this.loadSession(request);\n if (!session) return this.redirect('invalid_request', 'session not found');\n mark('load_session');\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) return this.redirect('invalid_request', 'authorization request not found');\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 2. the provider's answer: query, or a form for Apple's response_mode=form_post\n const { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n const data = isFormPost(request)\n ? Object.fromEntries(new URLSearchParams(await request.text()))\n : query(request);\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) return this.redirect('invalid_request', 'invalid redirect query');\n // The provider answered with an error instead of a code (the user pressed Cancel →\n // access_denied, invalid_scope, server_error, ...). Forward it rather than reporting a\n // malformed query. This runs before the state check on purpose: providers echo state on\n // error responses, but not all reliably do, and a forged error callback can only make the\n // victim's popup show an error — nothing is exchanged or signed in. A present state that does\n // not match still wins, since that is the more accurate (and more alarming) diagnosis.\n if (parsed.data.error) {\n if (parsed.data.state && parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n return this.redirect(\n safeErrorType(parsed.data.error, 'server_error'),\n parsed.data.error_description\n );\n }\n const { code, state, user } = parsed.data;\n if (!code || !state) return this.redirect('invalid_request', 'invalid redirect query');\n if (state !== cached.state) return this.redirect('invalid_request', 'redirect state mismatch');\n if (provider !== cached.provider) {\n return this.redirect('invalid_request', 'redirect provider mismatch');\n }\n mark('validate_session');\n\n // 3. exchange the code, load the user, sign in. Everything here can fail (an expired or reused\n // code, a network error, the host's handler or session store). The callback runs in a popup\n // the user is looking at, so every failure must land on errorUri, never as a JSON error.\n let signedIn: Session;\n try {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code,\n pkce: cached.additionalParameters,\n });\n // Apple posts the user's name alongside the code, once: hand it to the provider with the token.\n const userInfo = await this.oauth2Client.getUserInfo({\n provider,\n clientId,\n token,\n response: user ? { user } : undefined,\n });\n mark('exchange_code');\n // The id token must carry the nonce this flow sent (stored with the state, not in KV: it\n // is bound to this session's request). A provider that gave no id token has nothing to\n // carry it, as has a request stored before nonces were sent.\n if (cached.nonce && token.id_token) {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce !== 'string' || !nonce) throw new AuthenticationError('NONCE_MISSING');\n if (nonce !== cached.nonce) throw new AuthenticationError('NONCE_MISMATCH');\n mark('check_nonce');\n }\n\n const principal = await handler(request, {\n provider,\n clientId,\n userInfo,\n token,\n attributes: cached.attributes ?? {},\n });\n mark('save_principal');\n\n signedIn = await this.signIn(principal, session);\n await this.repository.deleteById(session.getId());\n mark('update_session');\n } catch (error) {\n // The details stay in the logs: an OAuth2Error message can carry a client id or a\n // provider's response, which has no place in a URL the browser keeps in its history.\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 callback failed', error);\n const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : 'server_error';\n return this.redirect(type);\n }\n // A refusal is something the user can act on, so its reason and metadata reach the page,\n // as they reach a JSON client through the ErrorInfo.\n if (error instanceof AuthenticationError) {\n console.warn('OAuth2 sign-in refused', error.reason, error.metadata);\n return this.redirect('access_denied', undefined, error);\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error');\n }\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, signedIn);\n };\n\n /** The shared tail of the JSON login routes: the host's handler, then a signed-in session. */\n private async authorize(\n request: Request,\n handler: AuthorizedHandler,\n authorization: Authorization,\n setTiming: (response: Response) => void\n ): Promise<Response> {\n const principal = await handler(request, authorization);\n const session = await this.signIn(principal);\n const response = Response.json(principal);\n setTiming(response);\n return this.withSession(response, session);\n }\n\n loginOAuth2Native = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const body = await readJson(request);\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\n // Only the credentials go on to the provider client.\n const { attributes: _, ...credentials } = body as NativeCredential & { attributes?: unknown };\n const { code_verifier } = credentials as { code_verifier?: unknown };\n if (\n code_verifier !== undefined &&\n (typeof code_verifier !== 'string' || !CODE_VERIFIER_FORMAT.test(code_verifier))\n ) {\n throw invalidArgument(\n 'INVALID_CODE_VERIFIER',\n 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]'\n );\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\n credentials,\n });\n mark('login_oauth2_native');\n\n // Required when the provider's native id token echoes a nonce and the login produced one\n // (Facebook's access_token login has none to carry it).\n const required = (await this.oauth2Client.nonceSupport(provider)).native && !!token.id_token;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n loginOAuth2Onetap = (\n request: Request,\n handler: AuthorizedHandler,\n provider = 'google'\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // Google's own form post, or an app's JSON.\n const contentType = request.headers.get('content-type') ?? '';\n const body = contentType.includes('application/json')\n ? await readJson(request)\n : await readForm(request);\n const { attributes = {}, ...parsed } = parse(oneTapBodySchema, body);\n\n // Both the credential and the code are issued to the provider's web client.\n let clientId: string;\n let token: Authorization['token'];\n let userInfo: Authorization['userInfo'];\n if (parsed.credential) {\n ({ clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n }));\n mark('verify_credential');\n\n // The credential must carry a single-use nonce from `oauth2Nonce`, which the front end\n // passed to `prompt`. This replaces Google's `g_csrf_token` double-submit cookie, which\n // FedCM mode no longer sets.\n const required = (await this.oauth2Client.nonceSupport(provider)).web;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n } else if (parsed.code) {\n ({ clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n }));\n userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n } else {\n throw invalidArgument('INVALID_ARGUMENT', 'credential or code is required');\n }\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n // ---- email -----------------------------------------------------------------------------------\n\n sendEmailVerificationCode = (request: Request, sender: EmailSender): Promise<Response> =>\n this.guard(async () => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n const { email, turnstileToken } = parse(\n sendEmailVerificationCodeSchema,\n await readJson(request)\n );\n await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n await this.emailCodes.send(email, sender);\n return Response.json({});\n });\n\n verifyEmailCode = (email: string, verificationCode: string): Promise<void> =>\n this.emailCodes.verify(email, verificationCode);\n\n loginEmail = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n const { setTiming } = timing({ enabled: this.timing });\n const {\n email,\n verificationCode,\n attributes = {},\n } = parse(loginEmailSchema, await readJson(request));\n await this.emailCodes.verify(email, verificationCode);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n return this.authorize(\n request,\n handler,\n { provider: 'EMAIL', userInfo: { claims, data: {} }, attributes },\n setTiming\n );\n });\n\n // ---- desktop ---------------------------------------------------------------------------------\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already completed normal\n * sign-in on the web (so a cookie session exists); this issues a 5-minute single-use code that\n * the web page hands off to the desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier` before opening the\n * browser, derives `code_challenge = base64url(sha256(verifier))`, and threads the challenge\n * through the URL → web page → here. The challenge is pinned to the code; `desktopExchange`\n * enforces that the caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet capture on loopback)\n * from exchanging it.\n *\n * Requires an authenticated cookie session: anonymous callers get 401. Throttling / abuse\n * protection on this endpoint is the API layer's job (rate-limit per principal); the SDK does\n * not gate it because the upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const principal = await this.getPrincipal(request);\n if (!principal) throw unauthenticated('Sign in first');\n const { code_challenge } = parse(desktopAuthorizeSchema, await readJson(request));\n const code = await this.desktopCodes.issue(principal, code_challenge);\n return Response.json({ code });\n });\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop client hands back the auth\n * code it received via the loopback callback, and we mint a fresh session for it, independent\n * of the web cookie session that originally authorized the code, so logging out on either side\n * does not cascade to the other.\n *\n * Cookie attributes: `SameSite=None; Secure` so the cookie survives cross-site fetches from the\n * desktop renderer's `file://` / `app://` origin. The configured `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`); the override is targeted at the\n * SameSite/Secure pair only. In a non-HTTPS dev backend this combination is rejected by\n * Chromium, see the desktop sign-in PRD for workarounds.\n *\n * No CSRF check needed: the request carries no cookie, the auth code itself is the credential,\n * and the code's single-use removal blocks replay.\n */\n desktopExchange = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const { code, code_verifier } = parse(desktopExchangeSchema, await readJson(request));\n const principal = await this.desktopCodes.redeem(code, code_verifier);\n const session = await this.signIn(principal);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n return this.withSession(Response.json({ ok: true }), session, {\n sameSite: 'none',\n secure: true,\n });\n });\n\n // ---- session management ----------------------------------------------------------------------\n\n kick = async (principal: Principal): Promise<void> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const sessionId of sessions.keys()) {\n await this.repository.deleteById(sessionId);\n }\n };\n\n /**\n * Sliding expiration: an authenticated request renews the session, as `logged` does, so a user\n * who keeps using the app is never signed out after `maxInactiveInterval` of not calling one\n * particular endpoint. No extra read: the session was just loaded to authenticate, and its\n * `lastAccessedTime` is compared in memory; writes are spaced by `session.touchInterval` (see\n * `AuthConfig`), so a session accessed again within the interval is not rewritten.\n */\n private async touch(session: Session) {\n const now = Date.now();\n if (now - session.getLastAccessedTime() < this.touchInterval) return;\n session.setLastAccessedTime(now);\n await this.repository.save(session);\n }\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const session = await this.loadSession(request);\n const principal = session && this.principalOf(session);\n if (!principal) return null;\n await this.touch(session);\n return principal;\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n return (await this.getPrincipal(request)) !== null;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n type Result = T extends true ? Session : Session | null;\n const session = await this.loadSession(request);\n if (session || !create) return session as Result;\n const created = this.repository.createSession();\n await this.repository.save(created);\n return created;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\n };\n\n listSessions = async (principal: Principal): Promise<Session[]> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n return Array.from(sessions.values());\n };\n\n cleanupExpiredSessions = async (cleanupCount?: number): Promise<Response> => {\n await this.repository.cleanupExpiredSessions(cleanupCount);\n return Response.json({});\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["isKnownOAuth2ErrorType","attributesSize","MAX_ATTRIBUTES_BYTES","extend","googleOneTapSchema","attributesSchema","object","string","regex","literal","EmailVerificationCodes","DesktopCodes","OAuth2Client","getCookie","PRINCIPAL_NAME_INDEX_NAME","errorResponse","randomUUID","randomBytes","createHash","AuthenticationError","param","timing","query","oauth2RedirectQuerySchema","OAuth2Error","readJson","bodyAttributes","invalidArgument","readForm","parse","sendEmailVerificationCodeSchema","verifyTurnstileToken","loginEmailSchema","unauthenticated"],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomBytes, randomUUID } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport { extend, literal, object, regex, string } from 'zod/mini';\nimport type { Principal } from '../core/index';\nimport { loginEmailSchema, sendEmailVerificationCodeSchema } from '../email/schema';\nimport { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport { OAuth2Error, type OAuth2ErrorType, isKnownOAuth2ErrorType } from '../oauth2/error';\nimport type {\n NativeCredential,\n OAuth2AuthorizationRequest,\n PkceParameters,\n StandardClaims,\n} from '../oauth2/types';\nimport { PRINCIPAL_NAME_INDEX_NAME } from '../session/common';\nimport type { KVRepository, Session, SessionRepository } from '../session/types';\nimport {\n type CookieOptions,\n deleteCookie,\n getCookie,\n param,\n query,\n setCookie,\n} from '../utils/http';\nimport { timing } from '../utils/timing';\nimport { parse, readForm, readJson, verifyTurnstileToken } from '../utils/valid';\nimport {\n MAX_ATTRIBUTES_BYTES,\n attributesSchema,\n attributesSize,\n bodyAttributes,\n} from './attributes';\nimport { DesktopCodes } from './desktop-codes';\nimport { type EmailSender, EmailVerificationCodes } from './email-codes';\nimport { AuthenticationError } from './error';\nimport { errorResponse, invalidArgument, unauthenticated } from './respond';\nimport type {\n AuthConfig,\n AuthService,\n Authorization,\n AuthorizedHandler,\n LoggedHandler,\n LogoutOptions,\n OAuth2AuthorizationOptions,\n} from './types';\n\n// PKCE per RFC 7636: code_verifier / code_challenge are 43-128 chars from\n// the unreserved set. We restrict to base64url (drops \".\" and \"~\") since\n// that's what S256 produces and what callers should be sending; rejecting\n// the rest shrinks the accepted input shape without losing legit clients.\n// A 32-byte random verifier base64url-encodes to exactly 43 chars.\nconst PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;\n\n// A native app's `code_verifier` goes on to the provider, which checks it against the challenge\n// the app sent: accept the whole RFC 7636 unreserved set here, as the provider does.\nconst CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;\n\n/** The header a session id travels in besides the cookie, with `session.header` on. */\nconst SESSION_HEADER = 'X-Auth-Token';\n\n/** Seconds an OAuth2 state or nonce may be used within. */\nconst OAUTH2_PARAMETER_TTL = 10 * 60;\n\n// error_description forwarded to errorUri comes from the provider or an error message: cap it so\n// the redirect URL stays short and an attacker-crafted callback cannot stuff the page with text.\nconst MAX_ERROR_DESCRIPTION_LENGTH = 256;\n\n/** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */\nfunction safeErrorType(error: string, fallback: OAuth2ErrorType): OAuth2ErrorType {\n return isKnownOAuth2ErrorType(error) ? error : fallback;\n}\n\n/**\n * The codes a failure past validation keeps on errorUri, each one the user can act on: the code\n * expired or was used (start again), they declined, or the provider is briefly down. Every other\n * failure there (a rejected client, a user info request that failed) is the app's or the\n * provider's problem, which the user sees as `server_error`.\n */\nconst CALLBACK_ERROR_TYPES: ReadonlySet<OAuth2ErrorType> = new Set([\n 'invalid_grant',\n 'access_denied',\n 'temporarily_unavailable',\n]);\n\n/**\n * The host's values for the flow (`attributes`, `redirectParams`), `undefined` ones dropped; `name`\n * is what the size invariant calls them.\n */\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes'],\n name = 'attributes'\n): Record<string, string> | undefined {\n if (!attributes) return undefined;\n const defined: Record<string, string> = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined) defined[key] = value;\n }\n if (Object.keys(defined).length === 0) return undefined;\n // The host's own values, written with the session: oversized is a bug in the host, not a 400.\n const size = attributesSize(defined);\n invariant(\n size <= MAX_ATTRIBUTES_BYTES,\n `oauth2 authorization ${name} are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\ninterface RedirectDetails {\n description?: string;\n refusal?: AuthenticationError;\n /** The flow's `redirectParams`. */\n params?: Record<string, string>;\n}\n\n/**\n * What the callback's redirects say themselves. A host's `redirectParams` never carry these, even\n * on a redirect that leaves one out (a cancel has no `reason`): the page would read it as the SDK's.\n */\nconst REDIRECT_PARAMS = new Set(['status', 'provider', 'error', 'error_description', 'reason']);\n\n/** The host's `redirectParams` without the names the redirects keep for themselves. */\nfunction redirectParams(params: OAuth2AuthorizationOptions['redirectParams']) {\n if (!params) return undefined;\n const own = Object.entries(params).filter(([key]) => !REDIRECT_PARAMS.has(key));\n return authorizationAttributes(Object.fromEntries(own), 'redirectParams');\n}\n\n/** `base` with `params` added, none replacing one it carries itself. */\nfunction withParams(base: string, params: Record<string, string> = {}) {\n const uri = new URL(base);\n for (const [key, value] of Object.entries(params)) {\n if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);\n }\n return uri;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\nfunction isFormPost(request: Request) {\n const contentType = request.headers.get('content-type')?.toLowerCase();\n return (\n request.method === 'POST' && !!contentType?.startsWith('application/x-www-form-urlencoded')\n );\n}\n\n/** Google's own form post carries no attributes; an app posting JSON may add them. */\nconst oneTapBodySchema = extend(googleOneTapSchema, { attributes: attributesSchema });\n\nconst desktopAuthorizeSchema = object({\n code_challenge: string().check(regex(PKCE_FORMAT)),\n code_challenge_method: literal('S256'),\n});\n\nconst desktopExchangeSchema = object({\n code: string(),\n code_verifier: string().check(regex(PKCE_FORMAT)),\n});\n\nexport const PATH = {\n CSRF: '/csrf',\n LOGOUT: '/logout',\n LOGGED: '/logged',\n\n OAUTH2_STATE: '/oauth2/state/:provider',\n OAUTH2_NONCE: '/oauth2/nonce/:provider',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:provider',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:provider',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:provider',\n LOGIN_OAUTH2_ONETAP: '/login/oauth2/onetap/google',\n\n LOGIN_EMAIL: '/login/email',\n SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\n\n // Desktop / native app sign-in via web loopback (RFC 8252 + OAuth 2.1 Auth Code Flow):\n // 1. user already authenticated with cookie session → POST DESKTOP_AUTHORIZE → { code }\n // 2. web redirects to desktop's loopback callback with the code\n // 3. desktop renderer POSTs DESKTOP_EXCHANGE with the code → server returns\n // Set-Cookie carrying a fresh session, single-use code is invalidated\n // The two-step flow keeps the actual session id off the redirect URL — only a\n // 5-minute single-use code transits via the loopback.\n DESKTOP_AUTHORIZE: '/auth/desktop/authorize',\n DESKTOP_EXCHANGE: '/auth/desktop/exchange',\n\n CLEANUP_EXPIRED_SESSIONS: '/sessions/expired/cleanup',\n} as const;\n\n/**\n * The login routes and session helpers. Every route is one straight line: read and validate the\n * request, check the credential, call the host's `onAuthorized`, sign in. Failures are thrown and\n * answered in one place per kind of route: a JSON route runs under `guard`, which maps a\n * `RequestError`, an `AuthenticationError` or an `OAuth2Error` to its google.rpc body (see\n * `respond.ts`); the redirect callback maps the same errors to `errorUri` parameters (`redirect`).\n */\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly kv: KVRepository;\n private readonly repository: SessionRepository;\n private readonly turnstileSecretKey?: string;\n private readonly emailCodes: EmailVerificationCodes;\n private readonly desktopCodes: DesktopCodes;\n private readonly oauth2Client: OAuth2Client | null;\n /** Provider keys whose logins may come without a nonce (`oauth2.nonce.optional`). */\n private readonly nonceOptional: ReadonlySet<string>;\n private readonly touchInterval: number;\n private readonly sessionHeader: boolean;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\n oauth2,\n cookie,\n session,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const maxAttempts = verificationCode?.maxAttempts ?? 5;\n const resendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(maxAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(resendCooldown >= 1, 'verificationCode.resendCooldown must be at least 1 second');\n this.emailCodes = new EmailVerificationCodes(kvRepository, { maxAttempts, resendCooldown });\n this.desktopCodes = new DesktopCodes(kvRepository);\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n this.nonceOptional = new Set(oauth2?.nonce?.optional);\n this.touchInterval = (session?.touchInterval ?? 3600) * 1000;\n this.sessionHeader = session?.header ?? false;\n invariant(this.touchInterval >= 0, 'session.touchInterval must not be negative');\n }\n\n // ---- sessions and responses ------------------------------------------------------------------\n\n private getSessionId(request: Request): string | undefined {\n const fromCookie = getCookie(request, this.cookieName);\n if (fromCookie) return fromCookie;\n return (this.sessionHeader && request.headers.get(SESSION_HEADER)) || undefined;\n }\n\n /** The session the request carries, if it exists. */\n private async loadSession(request: Request): Promise<Session | null> {\n const sessionId = this.getSessionId(request);\n return sessionId ? this.repository.findById(sessionId) : null;\n }\n\n /** The signed-in principal of a session, if any. */\n private principalOf(session: Session): Principal | null {\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n return name ? { name: String(name) } : null;\n }\n\n /**\n * A new session signed in as `principal`. With `carryOver`, what the host kept in the pre-login\n * session moves to the new one; the authorization request (state, PKCE verifier, flow\n * attributes) is single-use and was consumed, and a principal left there from an earlier\n * sign-in is replaced. The new id is never the one the browser brought, which may have been\n * planted by an attacker who started the flow (session fixation).\n */\n private async signIn(principal: Principal, carryOver?: Session): Promise<Session> {\n const session = this.repository.createSession();\n for (const attribute of carryOver?.getAttributeNames() ?? []) {\n if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;\n if (attribute === PRINCIPAL_NAME_INDEX_NAME) continue;\n const value = carryOver?.getAttribute(attribute);\n if (value !== null && value !== undefined) session.setAttribute(attribute, value);\n }\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n return session;\n }\n\n /**\n * `response` carrying `session`: its cookie, renewed for the whole inactive interval, and with\n * `session.header` on, the `X-Auth-Token` header a client without a cookie jar reads.\n */\n private withSession(response: Response, session: Session, cookie?: CookieOptions): Response {\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n ...cookie,\n maxAge,\n });\n if (this.sessionHeader) response.headers.set(SESSION_HEADER, session.getId());\n return response;\n }\n\n /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */\n private async guard(run: () => Promise<Response>): Promise<Response> {\n try {\n return await run();\n } catch (error) {\n const response = errorResponse(error);\n if (response) return response;\n throw error;\n }\n }\n\n /**\n * The error redirect of the callback flow: `refusal`'s reason and metadata ride along, and the\n * flow's `redirectParams` once its authorization request is known.\n */\n private redirect(\n error: OAuth2ErrorType,\n { description, refusal, params }: RedirectDetails = {}\n ): Response {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n // A refusal's metadata wins over the host's parameters of the same name: it is about this answer.\n const uri = withParams(this.oauth2Client.errorUri, { ...params, ...refusal?.metadata });\n uri.searchParams.set('error', error);\n if (description) {\n uri.searchParams.set('error_description', description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));\n }\n if (refusal) uri.searchParams.set('reason', refusal.reason);\n return Response.redirect(uri.href, 302);\n }\n\n // ---- basic -----------------------------------------------------------------------------------\n\n csrf = async (_request: Request): Promise<Response> => {\n const token = randomUUID();\n const response = Response.json({\n token,\n parameterName: '_csrf',\n headerName: 'X-XSRF-TOKEN',\n });\n\n // Double Submit Cookie Protection\n setCookie(response, 'XSRF-TOKEN', token, {\n path: '/',\n secure: true,\n httpOnly: false,\n sameSite: 'none',\n domain: this.cookieOptions.domain,\n });\n return response;\n };\n\n logout = async (request: Request, options?: LogoutOptions): Promise<Response> => {\n const scope = options?.scope ?? 'local';\n const sessionId = this.getSessionId(request);\n // `global` and `others` reach the principal's other sessions through the one the request\n // carries; a request without a signed-in session has no principal to widen to, and ends as\n // `local` would.\n if (scope !== 'local' && sessionId) {\n const session = await this.repository.findById(sessionId);\n const principal = session && this.principalOf(session);\n if (principal) {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const id of sessions.keys()) {\n if (id !== sessionId) await this.repository.deleteById(id);\n }\n }\n }\n const response = Response.json({});\n if (scope === 'others') return response;\n if (sessionId) await this.repository.deleteById(sessionId);\n // A Set-Cookie only replaces the stored cookie when Domain and Path match (RFC 6265),\n // so clear it with the same attributes it was set with.\n deleteCookie(response, this.cookieName, this.cookieOptions);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n const session = await this.loadSession(request);\n if (!session) return Response.json({ data: false });\n\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n const principal = this.principalOf(session);\n if (principal) await onLogged?.(principal);\n return this.withSession(Response.json({ data: principal !== null }), session);\n };\n\n // ---- oauth2 ----------------------------------------------------------------------------------\n\n private createPkceParameters(): PkceParameters {\n // 32 random bytes: a 43-char base64url verifier, the RFC 7636 minimum (a UUID is 36).\n const code_verifier = randomBytes(32).toString('base64url');\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n private getOauth2NonceKey(nonce: string) {\n return `oauth2:nonce:${nonce}`;\n }\n\n /**\n * Spends a nonce `oauth2Nonce` issued for `provider`: `NONCE_MISMATCH` for one that was not,\n * or was used already. An id token is replayable until it expires; the nonce it carries makes\n * it sign in once.\n */\n private async consumeNonce(nonce: string, provider: string): Promise<void> {\n const key = this.getOauth2NonceKey(nonce);\n if ((await this.kv.getItem(key)) !== provider) throw new AuthenticationError('NONCE_MISMATCH');\n await this.kv.removeItem(key);\n }\n\n /**\n * The nonce check of the JSON logins (One Tap, native), where the client obtained the id token\n * and the nonce came from `oauth2Nonce`: spends the one the token carries, and refuses a token\n * without one when the provider's flow echoes nonces (`required`) and the host has not listed the\n * provider as optional. An id token is replayable until it expires; the nonce makes it sign in\n * once.\n */\n private async checkIssuedNonce(\n userInfo: Authorization['userInfo'],\n provider: string,\n required: boolean\n ): Promise<boolean> {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce === 'string' && nonce) {\n await this.consumeNonce(nonce, provider);\n return true;\n }\n if (required && !this.nonceOptional.has(provider)) {\n throw new AuthenticationError('NONCE_MISSING');\n }\n return false;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), provider, OAUTH2_PARAMETER_TTL);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, OAUTH2_PARAMETER_TTL);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const params = redirectParams(options?.redirectParams);\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n // Sent only to a provider whose id token echoes it: the callback then requires it back, so\n // a code that was swapped in for another sign-in's yields a token the session refuses.\n const nonce = (await this.oauth2Client.nonceSupport(provider)).web ? randomUUID() : undefined;\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, state, pkce, nonce });\n let session = await this.loadSession(request);\n if (!session) {\n // A session made for the flow only has to outlive the state: an abandoned flow must not\n // keep a row for the default interval. Sign-in moves to a new session, with the default.\n session = this.repository.createSession();\n session.setMaxInactiveInterval(OAUTH2_PARAMETER_TTL);\n }\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(nonce ? { nonce } : {}),\n ...(attributes ? { attributes } : {}),\n ...(params ? { redirectParams: params } : {}),\n };\n session.setAttribute(\n this.ATTR_OAUTH2_AUTHORIZATION_REQUEST,\n JSON.stringify(authorizationRequest)\n );\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, session);\n });\n\n loginOAuth2Code = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. the session the flow started on, and the authorization request it stored\n const session = await this.loadSession(request);\n if (!session) return this.redirect('invalid_request', { description: 'session not found' });\n mark('load_session');\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', { description: 'authorization request not found' });\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n // From here on the redirect back to the app is this flow's, with the host's parameters.\n const params = cached.redirectParams;\n\n // 2. the provider's answer: query, or a form for Apple's response_mode=form_post\n const { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n const data = isFormPost(request)\n ? Object.fromEntries(new URLSearchParams(await request.text()))\n : query(request);\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', { description: 'invalid redirect query', params });\n }\n // The provider answered with an error instead of a code (the user pressed Cancel →\n // access_denied, invalid_scope, server_error, ...). Forward it rather than reporting a\n // malformed query. This runs before the state check on purpose: providers echo state on\n // error responses, but not all reliably do, and a forged error callback can only make the\n // victim's popup show an error — nothing is exchanged or signed in. A present state that does\n // not match still wins, since that is the more accurate (and more alarming) diagnosis.\n if (parsed.data.error) {\n if (parsed.data.state && parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', { description: 'redirect state mismatch', params });\n }\n return this.redirect(safeErrorType(parsed.data.error, 'server_error'), {\n description: parsed.data.error_description,\n params,\n });\n }\n const { code, state, user } = parsed.data;\n if (!code || !state) {\n return this.redirect('invalid_request', { description: 'invalid redirect query', params });\n }\n if (state !== cached.state) {\n return this.redirect('invalid_request', { description: 'redirect state mismatch', params });\n }\n if (provider !== cached.provider) {\n return this.redirect('invalid_request', {\n description: 'redirect provider mismatch',\n params,\n });\n }\n mark('validate_session');\n\n // 3. exchange the code, load the user, sign in. Everything here can fail (an expired or reused\n // code, a network error, the host's handler or session store). The callback runs in a popup\n // the user is looking at, so every failure must land on errorUri, never as a JSON error.\n let signedIn: Session;\n try {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code,\n pkce: cached.additionalParameters,\n });\n // Apple posts the user's name alongside the code, once: hand it to the provider with the token.\n const userInfo = await this.oauth2Client.getUserInfo({\n provider,\n clientId,\n token,\n response: user ? { user } : undefined,\n });\n mark('exchange_code');\n // The id token must carry the nonce this flow sent (stored with the state, not in KV: it\n // is bound to this session's request). A provider that gave no id token has nothing to\n // carry it, as has a request stored before nonces were sent.\n if (cached.nonce && token.id_token) {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce !== 'string' || !nonce) throw new AuthenticationError('NONCE_MISSING');\n if (nonce !== cached.nonce) throw new AuthenticationError('NONCE_MISMATCH');\n mark('check_nonce');\n }\n\n const principal = await handler(request, {\n provider,\n clientId,\n userInfo,\n token,\n attributes: cached.attributes ?? {},\n });\n mark('save_principal');\n\n signedIn = await this.signIn(principal, session);\n await this.repository.deleteById(session.getId());\n mark('update_session');\n } catch (error) {\n // The details stay in the logs: an OAuth2Error message can carry a client id or a\n // provider's response, which has no place in a URL the browser keeps in its history.\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 callback failed', error);\n const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : 'server_error';\n return this.redirect(type, { params });\n }\n // A refusal is something the user can act on, so its reason and metadata reach the page,\n // as they reach a JSON client through the ErrorInfo.\n if (error instanceof AuthenticationError) {\n console.warn('OAuth2 sign-in refused', error.reason, error.metadata);\n return this.redirect('access_denied', { refusal: error, params });\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error', { params });\n }\n\n const uri = withParams(this.oauth2Client.successUri, params);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, signedIn);\n };\n\n /** The shared tail of the JSON login routes: the host's handler, then a signed-in session. */\n private async authorize(\n request: Request,\n handler: AuthorizedHandler,\n authorization: Authorization,\n setTiming: (response: Response) => void\n ): Promise<Response> {\n const principal = await handler(request, authorization);\n const session = await this.signIn(principal);\n const response = Response.json(principal);\n setTiming(response);\n return this.withSession(response, session);\n }\n\n loginOAuth2Native = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const body = await readJson(request);\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\n // Only the credentials go on to the provider client.\n const { attributes: _, ...credentials } = body as NativeCredential & { attributes?: unknown };\n const { code_verifier } = credentials as { code_verifier?: unknown };\n if (\n code_verifier !== undefined &&\n (typeof code_verifier !== 'string' || !CODE_VERIFIER_FORMAT.test(code_verifier))\n ) {\n throw invalidArgument(\n 'INVALID_CODE_VERIFIER',\n 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]'\n );\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\n credentials,\n });\n mark('login_oauth2_native');\n\n // Required when the provider's native id token echoes a nonce and the login produced one\n // (Facebook's access_token login has none to carry it).\n const required = (await this.oauth2Client.nonceSupport(provider)).native && !!token.id_token;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n loginOAuth2Onetap = (\n request: Request,\n handler: AuthorizedHandler,\n provider = 'google'\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // Google's own form post, or an app's JSON.\n const contentType = request.headers.get('content-type') ?? '';\n const body = contentType.includes('application/json')\n ? await readJson(request)\n : await readForm(request);\n const { attributes = {}, ...parsed } = parse(oneTapBodySchema, body);\n\n // Both the credential and the code are issued to the provider's web client.\n let clientId: string;\n let token: Authorization['token'];\n let userInfo: Authorization['userInfo'];\n if (parsed.credential) {\n ({ clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n }));\n mark('verify_credential');\n\n // The credential must carry a single-use nonce from `oauth2Nonce`, which the front end\n // passed to `prompt`. This replaces Google's `g_csrf_token` double-submit cookie, which\n // FedCM mode no longer sets.\n const required = (await this.oauth2Client.nonceSupport(provider)).web;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n } else if (parsed.code) {\n ({ clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n }));\n userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n } else {\n throw invalidArgument('INVALID_ARGUMENT', 'credential or code is required');\n }\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n // ---- email -----------------------------------------------------------------------------------\n\n sendEmailVerificationCode = (request: Request, sender: EmailSender): Promise<Response> =>\n this.guard(async () => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n const { email, turnstileToken } = parse(\n sendEmailVerificationCodeSchema,\n await readJson(request)\n );\n await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n await this.emailCodes.send(email, sender);\n return Response.json({});\n });\n\n verifyEmailCode = (email: string, verificationCode: string): Promise<void> =>\n this.emailCodes.verify(email, verificationCode);\n\n loginEmail = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n const { setTiming } = timing({ enabled: this.timing });\n const {\n email,\n verificationCode,\n attributes = {},\n } = parse(loginEmailSchema, await readJson(request));\n await this.emailCodes.verify(email, verificationCode);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n return this.authorize(\n request,\n handler,\n { provider: 'EMAIL', userInfo: { claims, data: {} }, attributes },\n setTiming\n );\n });\n\n // ---- desktop ---------------------------------------------------------------------------------\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already completed normal\n * sign-in on the web (so a cookie session exists); this issues a 5-minute single-use code that\n * the web page hands off to the desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier` before opening the\n * browser, derives `code_challenge = base64url(sha256(verifier))`, and threads the challenge\n * through the URL → web page → here. The challenge is pinned to the code; `desktopExchange`\n * enforces that the caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet capture on loopback)\n * from exchanging it.\n *\n * Requires an authenticated cookie session: anonymous callers get 401. Throttling / abuse\n * protection on this endpoint is the API layer's job (rate-limit per principal); the SDK does\n * not gate it because the upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const principal = await this.getPrincipal(request);\n if (!principal) throw unauthenticated('Sign in first');\n const { code_challenge } = parse(desktopAuthorizeSchema, await readJson(request));\n const code = await this.desktopCodes.issue(principal, code_challenge);\n return Response.json({ code });\n });\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop client hands back the auth\n * code it received via the loopback callback, and we mint a fresh session for it, independent\n * of the web cookie session that originally authorized the code, so logging out on either side\n * does not cascade to the other.\n *\n * Cookie attributes: `SameSite=None; Secure` so the cookie survives cross-site fetches from the\n * desktop renderer's `file://` / `app://` origin. The configured `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`); the override is targeted at the\n * SameSite/Secure pair only. In a non-HTTPS dev backend this combination is rejected by\n * Chromium, see the desktop sign-in PRD for workarounds.\n *\n * No CSRF check needed: the request carries no cookie, the auth code itself is the credential,\n * and the code's single-use removal blocks replay.\n */\n desktopExchange = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const { code, code_verifier } = parse(desktopExchangeSchema, await readJson(request));\n const principal = await this.desktopCodes.redeem(code, code_verifier);\n const session = await this.signIn(principal);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n return this.withSession(Response.json({ ok: true }), session, {\n sameSite: 'none',\n secure: true,\n });\n });\n\n // ---- session management ----------------------------------------------------------------------\n\n kick = async (principal: Principal): Promise<void> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const sessionId of sessions.keys()) {\n await this.repository.deleteById(sessionId);\n }\n };\n\n /**\n * Sliding expiration: an authenticated request renews the session, as `logged` does, so a user\n * who keeps using the app is never signed out after `maxInactiveInterval` of not calling one\n * particular endpoint. No extra read: the session was just loaded to authenticate, and its\n * `lastAccessedTime` is compared in memory; writes are spaced by `session.touchInterval` (see\n * `AuthConfig`), so a session accessed again within the interval is not rewritten.\n */\n private async touch(session: Session) {\n const now = Date.now();\n if (now - session.getLastAccessedTime() < this.touchInterval) return;\n session.setLastAccessedTime(now);\n await this.repository.save(session);\n }\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const session = await this.loadSession(request);\n const principal = session && this.principalOf(session);\n if (!principal) return null;\n await this.touch(session);\n return principal;\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n return (await this.getPrincipal(request)) !== null;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n type Result = T extends true ? Session : Session | null;\n const session = await this.loadSession(request);\n if (session || !create) return session as Result;\n const created = this.repository.createSession();\n await this.repository.save(created);\n return created;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\n };\n\n listSessions = async (principal: Principal): Promise<Session[]> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n return Array.from(sessions.values());\n };\n\n cleanupExpiredSessions = async (cleanupCount?: number): Promise<Response> => {\n await this.repository.cleanupExpiredSessions(cleanupCount);\n return Response.json({});\n };\n}\n"],"mappings":"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"}
@@ -61,7 +61,10 @@ export declare class Auth implements AuthService {
61
61
  private withSession;
62
62
  /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */
63
63
  private guard;
64
- /** The error redirect of the callback flow; `refusal`'s reason and metadata ride along. */
64
+ /**
65
+ * The error redirect of the callback flow: `refusal`'s reason and metadata ride along, and the
66
+ * flow's `redirectParams` once its authorization request is known.
67
+ */
65
68
  private redirect;
66
69
  csrf: (_request: Request) => Promise<Response>;
67
70
  logout: (request: Request, options?: LogoutOptions) => Promise<Response>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/auth/index.ts"],"mappings":";;;;qBA0Ha;WACX;WACA;WACA;WAEA;WACA;WACA;WACA;WACA;WACA;WAEA;WACA;WASA;WACA;WAEA;;;;;;;;;qBAUW,gBAAgB;mBACV;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;mBAEA;mBACA;mBACA;mBACA;EAEjB,cACE,mBACA,cACA,oBACA,kBACA,QACA,QACA,SACA,UACC;UA6BK;;UAOM;;UAMN;;;;;;;;UAYM;;;;;UAiBN;;UAYM;;UAWN;EAqBR,OAAI,UAAoB,YAAU,QAAQ;EAmB1C,SAAM,SAAmB,SAAO,UAAY,kBAAgB,QAAQ;EAyBpE,SAAM,SAAmB,SAAO,WAAa,kBAAgB,QAAQ;UAc7D;UAWA;UAIA;;;;;;UASM;;;;;;;;UAaA;EAgBd,cAAW,SAAmB,YAAU,QAAQ;EAOhD,cAAW,SAAmB,YAAU,QAAQ;EAOhD,sBAAmB,SACR,SAAO,UACN,+BACT,QAAQ;EAyCX,kBAAe,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;;UA6GlE;EAad,oBAAiB,SAAa,SAAO,SAAW,sBAAoB,QAAQ;EAuC5E,oBAAiB,SACN,SAAO,SACP,mBAAiB,sBAEzB,QAAQ;EAgDX,4BAAyB,SAAa,SAAO,QAAU,gBAAc,QAAQ;EAY7E,kBAAe,eAAiB,6BAA6B;EAG7D,aAAU,SAAa,SAAO,SAAW,sBAAoB,QAAQ;;;;;;;;;;;;;;;;;EAoCrE,mBAAgB,SAAa,YAAU,QAAQ;;;;;;;;;;;;;;;;EAwB/C,kBAAe,SAAa,YAAU,QAAQ;EAe9C,OAAI,WAAqB,cAAY;;;;;;;;UAcvB;EAOd,eAAY,SAAmB,YAAU,QAAQ;EAQjD,kBAAe,SAAmB,YAAU;EAI5C,aAAoB,mBAAiB,SAC1B,SAAO,QACR,MACP,QAAQ,iBAAiB,UAAU;EAStC,gBAAa,sBAA8B;EAI3C,eAAY,WAAqB,cAAY,QAAQ;EAKrD,yBAAsB,0BAAkC,QAAQ"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/auth/index.ts"],"mappings":";;;;qBA4Ja;WACX;WACA;WACA;WAEA;WACA;WACA;WACA;WACA;WACA;WAEA;WACA;WASA;WACA;WAEA;;;;;;;;;qBAUW,gBAAgB;mBACV;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;mBAEA;mBACA;mBACA;mBACA;EAEjB,cACE,mBACA,cACA,oBACA,kBACA,QACA,QACA,SACA,UACC;UA6BK;;UAOM;;UAMN;;;;;;;;UAYM;;;;;UAiBN;;UAYM;;;;;UAcN;EAiBR,OAAI,UAAoB,YAAU,QAAQ;EAmB1C,SAAM,SAAmB,SAAO,UAAY,kBAAgB,QAAQ;EAyBpE,SAAM,SAAmB,SAAO,WAAa,kBAAgB,QAAQ;UAc7D;UAWA;UAIA;;;;;;UASM;;;;;;;;UAaA;EAgBd,cAAW,SAAmB,YAAU,QAAQ;EAOhD,cAAW,SAAmB,YAAU,QAAQ;EAOhD,sBAAmB,SACR,SAAO,UACN,+BACT,QAAQ;EA2CX,kBAAe,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;;UA0HlE;EAad,oBAAiB,SAAa,SAAO,SAAW,sBAAoB,QAAQ;EAuC5E,oBAAiB,SACN,SAAO,SACP,mBAAiB,sBAEzB,QAAQ;EAgDX,4BAAyB,SAAa,SAAO,QAAU,gBAAc,QAAQ;EAY7E,kBAAe,eAAiB,6BAA6B;EAG7D,aAAU,SAAa,SAAO,SAAW,sBAAoB,QAAQ;;;;;;;;;;;;;;;;;EAoCrE,mBAAgB,SAAa,YAAU,QAAQ;;;;;;;;;;;;;;;;EAwB/C,kBAAe,SAAa,YAAU,QAAQ;EAe9C,OAAI,WAAqB,cAAY;;;;;;;;UAcvB;EAOd,eAAY,SAAmB,YAAU,QAAQ;EAQjD,kBAAe,SAAmB,YAAU;EAI5C,aAAoB,mBAAiB,SAC1B,SAAO,QACR,MACP,QAAQ,iBAAiB,UAAU;EAStC,gBAAa,sBAA8B;EAI3C,eAAY,WAAqB,cAAY,QAAQ;EAKrD,yBAAsB,0BAAkC,QAAQ"}
@@ -61,7 +61,10 @@ export declare class Auth implements AuthService {
61
61
  private withSession;
62
62
  /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */
63
63
  private guard;
64
- /** The error redirect of the callback flow; `refusal`'s reason and metadata ride along. */
64
+ /**
65
+ * The error redirect of the callback flow: `refusal`'s reason and metadata ride along, and the
66
+ * flow's `redirectParams` once its authorization request is known.
67
+ */
65
68
  private redirect;
66
69
  csrf: (_request: Request) => Promise<Response>;
67
70
  logout: (request: Request, options?: LogoutOptions) => Promise<Response>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/auth/index.ts"],"mappings":";;;;qBA0Ha;WACX;WACA;WACA;WAEA;WACA;WACA;WACA;WACA;WACA;WAEA;WACA;WASA;WACA;WAEA;;;;;;;;;qBAUW,gBAAgB;mBACV;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;mBAEA;mBACA;mBACA;mBACA;EAEjB,cACE,mBACA,cACA,oBACA,kBACA,QACA,QACA,SACA,UACC;UA6BK;;UAOM;;UAMN;;;;;;;;UAYM;;;;;UAiBN;;UAYM;;UAWN;EAqBR,OAAI,UAAoB,YAAU,QAAQ;EAmB1C,SAAM,SAAmB,SAAO,UAAY,kBAAgB,QAAQ;EAyBpE,SAAM,SAAmB,SAAO,WAAa,kBAAgB,QAAQ;UAc7D;UAWA;UAIA;;;;;;UASM;;;;;;;;UAaA;EAgBd,cAAW,SAAmB,YAAU,QAAQ;EAOhD,cAAW,SAAmB,YAAU,QAAQ;EAOhD,sBAAmB,SACR,SAAO,UACN,+BACT,QAAQ;EAyCX,kBAAe,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;;UA6GlE;EAad,oBAAiB,SAAa,SAAO,SAAW,sBAAoB,QAAQ;EAuC5E,oBAAiB,SACN,SAAO,SACP,mBAAiB,sBAEzB,QAAQ;EAgDX,4BAAyB,SAAa,SAAO,QAAU,gBAAc,QAAQ;EAY7E,kBAAe,eAAiB,6BAA6B;EAG7D,aAAU,SAAa,SAAO,SAAW,sBAAoB,QAAQ;;;;;;;;;;;;;;;;;EAoCrE,mBAAgB,SAAa,YAAU,QAAQ;;;;;;;;;;;;;;;;EAwB/C,kBAAe,SAAa,YAAU,QAAQ;EAe9C,OAAI,WAAqB,cAAY;;;;;;;;UAcvB;EAOd,eAAY,SAAmB,YAAU,QAAQ;EAQjD,kBAAe,SAAmB,YAAU;EAI5C,aAAoB,mBAAiB,SAC1B,SAAO,QACR,MACP,QAAQ,iBAAiB,UAAU;EAStC,gBAAa,sBAA8B;EAI3C,eAAY,WAAqB,cAAY,QAAQ;EAKrD,yBAAsB,0BAAkC,QAAQ"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/auth/index.ts"],"mappings":";;;;qBA4Ja;WACX;WACA;WACA;WAEA;WACA;WACA;WACA;WACA;WACA;WAEA;WACA;WASA;WACA;WAEA;;;;;;;;;qBAUW,gBAAgB;mBACV;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;mBAEA;mBACA;mBACA;mBACA;EAEjB,cACE,mBACA,cACA,oBACA,kBACA,QACA,QACA,SACA,UACC;UA6BK;;UAOM;;UAMN;;;;;;;;UAYM;;;;;UAiBN;;UAYM;;;;;UAcN;EAiBR,OAAI,UAAoB,YAAU,QAAQ;EAmB1C,SAAM,SAAmB,SAAO,UAAY,kBAAgB,QAAQ;EAyBpE,SAAM,SAAmB,SAAO,WAAa,kBAAgB,QAAQ;UAc7D;UAWA;UAIA;;;;;;UASM;;;;;;;;UAaA;EAgBd,cAAW,SAAmB,YAAU,QAAQ;EAOhD,cAAW,SAAmB,YAAU,QAAQ;EAOhD,sBAAmB,SACR,SAAO,UACN,+BACT,QAAQ;EA2CX,kBAAe,SAAmB,SAAO,SAAW,sBAAoB,QAAQ;;UA0HlE;EAad,oBAAiB,SAAa,SAAO,SAAW,sBAAoB,QAAQ;EAuC5E,oBAAiB,SACN,SAAO,SACP,mBAAiB,sBAEzB,QAAQ;EAgDX,4BAAyB,SAAa,SAAO,QAAU,gBAAc,QAAQ;EAY7E,kBAAe,eAAiB,6BAA6B;EAG7D,aAAU,SAAa,SAAO,SAAW,sBAAoB,QAAQ;;;;;;;;;;;;;;;;;EAoCrE,mBAAgB,SAAa,YAAU,QAAQ;;;;;;;;;;;;;;;;EAwB/C,kBAAe,SAAa,YAAU,QAAQ;EAe9C,OAAI,WAAqB,cAAY;;;;;;;;UAcvB;EAOd,eAAY,SAAmB,YAAU,QAAQ;EAQjD,kBAAe,SAAmB,YAAU;EAI5C,aAAoB,mBAAiB,SAC1B,SAAO,QACR,MACP,QAAQ,iBAAiB,UAAU;EAStC,gBAAa,sBAA8B;EAI3C,eAAY,WAAqB,cAAY,QAAQ;EAKrD,yBAAsB,0BAAkC,QAAQ"}
@@ -36,15 +36,42 @@ const CALLBACK_ERROR_TYPES = /* @__PURE__ */ new Set([
36
36
  "access_denied",
37
37
  "temporarily_unavailable"
38
38
  ]);
39
- function authorizationAttributes(attributes) {
39
+ /**
40
+ * The host's values for the flow (`attributes`, `redirectParams`), `undefined` ones dropped; `name`
41
+ * is what the size invariant calls them.
42
+ */
43
+ function authorizationAttributes(attributes, name = "attributes") {
40
44
  if (!attributes) return void 0;
41
45
  const defined = {};
42
46
  for (const [key, value] of Object.entries(attributes)) if (value !== void 0) defined[key] = value;
43
47
  if (Object.keys(defined).length === 0) return void 0;
44
48
  const size = attributesSize(defined);
45
- invariant(size <= MAX_ATTRIBUTES_BYTES, `oauth2 authorization attributes are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`);
49
+ invariant(size <= MAX_ATTRIBUTES_BYTES, `oauth2 authorization ${name} are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`);
46
50
  return defined;
47
51
  }
52
+ /**
53
+ * What the callback's redirects say themselves. A host's `redirectParams` never carry these, even
54
+ * on a redirect that leaves one out (a cancel has no `reason`): the page would read it as the SDK's.
55
+ */
56
+ const REDIRECT_PARAMS = /* @__PURE__ */ new Set([
57
+ "status",
58
+ "provider",
59
+ "error",
60
+ "error_description",
61
+ "reason"
62
+ ]);
63
+ /** The host's `redirectParams` without the names the redirects keep for themselves. */
64
+ function redirectParams(params) {
65
+ if (!params) return void 0;
66
+ const own = Object.entries(params).filter(([key]) => !REDIRECT_PARAMS.has(key));
67
+ return authorizationAttributes(Object.fromEntries(own), "redirectParams");
68
+ }
69
+ /** `base` with `params` added, none replacing one it carries itself. */
70
+ function withParams(base, params = {}) {
71
+ const uri = new URL(base);
72
+ for (const [key, value] of Object.entries(params)) if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);
73
+ return uri;
74
+ }
48
75
  /** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */
49
76
  function isFormPost(request) {
50
77
  const contentType = request.headers.get("content-type")?.toLowerCase();
@@ -185,11 +212,16 @@ var Auth = class {
185
212
  throw error;
186
213
  }
187
214
  }
188
- /** The error redirect of the callback flow; `refusal`'s reason and metadata ride along. */
189
- redirect(error, description, refusal) {
215
+ /**
216
+ * The error redirect of the callback flow: `refusal`'s reason and metadata ride along, and the
217
+ * flow's `redirectParams` once its authorization request is known.
218
+ */
219
+ redirect(error, { description, refusal, params } = {}) {
190
220
  invariant(this.oauth2Client, "oauth2Client is not initialized");
191
- const uri = new URL(this.oauth2Client.errorUri);
192
- for (const [key, value] of Object.entries(refusal?.metadata ?? {})) if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);
221
+ const uri = withParams(this.oauth2Client.errorUri, {
222
+ ...params,
223
+ ...refusal?.metadata
224
+ });
193
225
  uri.searchParams.set("error", error);
194
226
  if (description) uri.searchParams.set("error_description", description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));
195
227
  if (refusal) uri.searchParams.set("reason", refusal.reason);
@@ -292,6 +324,7 @@ var Auth = class {
292
324
  oauth2Authorization = (request, options) => this.guard(async () => {
293
325
  invariant(this.oauth2Client, "oauth2Client is not initialized");
294
326
  const attributes = authorizationAttributes(options?.attributes);
327
+ const params = redirectParams(options?.redirectParams);
295
328
  const { mark, setTiming } = timing({ enabled: this.timing });
296
329
  const { provider } = param(request, PATH.OAUTH2_AUTHORIZATION);
297
330
  const state = randomUUID();
@@ -315,7 +348,8 @@ var Auth = class {
315
348
  authorizationRequestUri: uri.href,
316
349
  additionalParameters: pkce,
317
350
  ...nonce ? { nonce } : {},
318
- ...attributes ? { attributes } : {}
351
+ ...attributes ? { attributes } : {},
352
+ ...params ? { redirectParams: params } : {}
319
353
  };
320
354
  session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, JSON.stringify(authorizationRequest));
321
355
  await this.repository.save(session);
@@ -331,23 +365,42 @@ var Auth = class {
331
365
  invariant(this.oauth2Client, "oauth2Client is not initialized");
332
366
  const { mark, setTiming } = timing({ enabled: this.timing });
333
367
  const session = await this.loadSession(request);
334
- if (!session) return this.redirect("invalid_request", "session not found");
368
+ if (!session) return this.redirect("invalid_request", { description: "session not found" });
335
369
  mark("load_session");
336
370
  const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
337
- if (!json) return this.redirect("invalid_request", "authorization request not found");
371
+ if (!json) return this.redirect("invalid_request", { description: "authorization request not found" });
338
372
  const cached = JSON.parse(json);
373
+ const params = cached.redirectParams;
339
374
  const { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);
340
375
  const data = isFormPost(request) ? Object.fromEntries(new URLSearchParams(await request.text())) : query(request);
341
376
  const parsed = oauth2RedirectQuerySchema.safeParse(data);
342
- if (!parsed.success) return this.redirect("invalid_request", "invalid redirect query");
377
+ if (!parsed.success) return this.redirect("invalid_request", {
378
+ description: "invalid redirect query",
379
+ params
380
+ });
343
381
  if (parsed.data.error) {
344
- if (parsed.data.state && parsed.data.state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
345
- return this.redirect(safeErrorType(parsed.data.error, "server_error"), parsed.data.error_description);
382
+ if (parsed.data.state && parsed.data.state !== cached.state) return this.redirect("invalid_request", {
383
+ description: "redirect state mismatch",
384
+ params
385
+ });
386
+ return this.redirect(safeErrorType(parsed.data.error, "server_error"), {
387
+ description: parsed.data.error_description,
388
+ params
389
+ });
346
390
  }
347
391
  const { code, state, user } = parsed.data;
348
- if (!code || !state) return this.redirect("invalid_request", "invalid redirect query");
349
- if (state !== cached.state) return this.redirect("invalid_request", "redirect state mismatch");
350
- if (provider !== cached.provider) return this.redirect("invalid_request", "redirect provider mismatch");
392
+ if (!code || !state) return this.redirect("invalid_request", {
393
+ description: "invalid redirect query",
394
+ params
395
+ });
396
+ if (state !== cached.state) return this.redirect("invalid_request", {
397
+ description: "redirect state mismatch",
398
+ params
399
+ });
400
+ if (provider !== cached.provider) return this.redirect("invalid_request", {
401
+ description: "redirect provider mismatch",
402
+ params
403
+ });
351
404
  mark("validate_session");
352
405
  let signedIn;
353
406
  try {
@@ -384,16 +437,19 @@ var Auth = class {
384
437
  if (error instanceof OAuth2Error) {
385
438
  console.warn("OAuth2 callback failed", error);
386
439
  const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : "server_error";
387
- return this.redirect(type);
440
+ return this.redirect(type, { params });
388
441
  }
389
442
  if (error instanceof AuthenticationError) {
390
443
  console.warn("OAuth2 sign-in refused", error.reason, error.metadata);
391
- return this.redirect("access_denied", void 0, error);
444
+ return this.redirect("access_denied", {
445
+ refusal: error,
446
+ params
447
+ });
392
448
  }
393
449
  console.error("OAuth2 callback failed", error);
394
- return this.redirect("server_error");
450
+ return this.redirect("server_error", { params });
395
451
  }
396
- const uri = new URL(this.oauth2Client.successUri);
452
+ const uri = withParams(this.oauth2Client.successUri, params);
397
453
  uri.searchParams.set("status", "success");
398
454
  uri.searchParams.set("provider", provider);
399
455
  const response = new Response(null, {
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomBytes, randomUUID } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport { extend, literal, object, regex, string } from 'zod/mini';\nimport type { Principal } from '../core/index';\nimport { loginEmailSchema, sendEmailVerificationCodeSchema } from '../email/schema';\nimport { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport { OAuth2Error, type OAuth2ErrorType, isKnownOAuth2ErrorType } from '../oauth2/error';\nimport type {\n NativeCredential,\n OAuth2AuthorizationRequest,\n PkceParameters,\n StandardClaims,\n} from '../oauth2/types';\nimport { PRINCIPAL_NAME_INDEX_NAME } from '../session/common';\nimport type { KVRepository, Session, SessionRepository } from '../session/types';\nimport {\n type CookieOptions,\n deleteCookie,\n getCookie,\n param,\n query,\n setCookie,\n} from '../utils/http';\nimport { timing } from '../utils/timing';\nimport { parse, readForm, readJson, verifyTurnstileToken } from '../utils/valid';\nimport {\n MAX_ATTRIBUTES_BYTES,\n attributesSchema,\n attributesSize,\n bodyAttributes,\n} from './attributes';\nimport { DesktopCodes } from './desktop-codes';\nimport { type EmailSender, EmailVerificationCodes } from './email-codes';\nimport { AuthenticationError } from './error';\nimport { errorResponse, invalidArgument, unauthenticated } from './respond';\nimport type {\n AuthConfig,\n AuthService,\n Authorization,\n AuthorizedHandler,\n LoggedHandler,\n LogoutOptions,\n OAuth2AuthorizationOptions,\n} from './types';\n\n// PKCE per RFC 7636: code_verifier / code_challenge are 43-128 chars from\n// the unreserved set. We restrict to base64url (drops \".\" and \"~\") since\n// that's what S256 produces and what callers should be sending; rejecting\n// the rest shrinks the accepted input shape without losing legit clients.\n// A 32-byte random verifier base64url-encodes to exactly 43 chars.\nconst PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;\n\n// A native app's `code_verifier` goes on to the provider, which checks it against the challenge\n// the app sent: accept the whole RFC 7636 unreserved set here, as the provider does.\nconst CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;\n\n/** The header a session id travels in besides the cookie, with `session.header` on. */\nconst SESSION_HEADER = 'X-Auth-Token';\n\n/** Seconds an OAuth2 state or nonce may be used within. */\nconst OAUTH2_PARAMETER_TTL = 10 * 60;\n\n// error_description forwarded to errorUri comes from the provider or an error message: cap it so\n// the redirect URL stays short and an attacker-crafted callback cannot stuff the page with text.\nconst MAX_ERROR_DESCRIPTION_LENGTH = 256;\n\n/** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */\nfunction safeErrorType(error: string, fallback: OAuth2ErrorType): OAuth2ErrorType {\n return isKnownOAuth2ErrorType(error) ? error : fallback;\n}\n\n/**\n * The codes a failure past validation keeps on errorUri, each one the user can act on: the code\n * expired or was used (start again), they declined, or the provider is briefly down. Every other\n * failure there (a rejected client, a user info request that failed) is the app's or the\n * provider's problem, which the user sees as `server_error`.\n */\nconst CALLBACK_ERROR_TYPES: ReadonlySet<OAuth2ErrorType> = new Set([\n 'invalid_grant',\n 'access_denied',\n 'temporarily_unavailable',\n]);\n\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes']\n): Record<string, string> | undefined {\n if (!attributes) return undefined;\n const defined: Record<string, string> = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined) defined[key] = value;\n }\n if (Object.keys(defined).length === 0) return undefined;\n // The host's own values, written with the session: oversized is a bug in the host, not a 400.\n const size = attributesSize(defined);\n invariant(\n size <= MAX_ATTRIBUTES_BYTES,\n `oauth2 authorization attributes are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\nfunction isFormPost(request: Request) {\n const contentType = request.headers.get('content-type')?.toLowerCase();\n return (\n request.method === 'POST' && !!contentType?.startsWith('application/x-www-form-urlencoded')\n );\n}\n\n/** Google's own form post carries no attributes; an app posting JSON may add them. */\nconst oneTapBodySchema = extend(googleOneTapSchema, { attributes: attributesSchema });\n\nconst desktopAuthorizeSchema = object({\n code_challenge: string().check(regex(PKCE_FORMAT)),\n code_challenge_method: literal('S256'),\n});\n\nconst desktopExchangeSchema = object({\n code: string(),\n code_verifier: string().check(regex(PKCE_FORMAT)),\n});\n\nexport const PATH = {\n CSRF: '/csrf',\n LOGOUT: '/logout',\n LOGGED: '/logged',\n\n OAUTH2_STATE: '/oauth2/state/:provider',\n OAUTH2_NONCE: '/oauth2/nonce/:provider',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:provider',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:provider',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:provider',\n LOGIN_OAUTH2_ONETAP: '/login/oauth2/onetap/google',\n\n LOGIN_EMAIL: '/login/email',\n SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\n\n // Desktop / native app sign-in via web loopback (RFC 8252 + OAuth 2.1 Auth Code Flow):\n // 1. user already authenticated with cookie session → POST DESKTOP_AUTHORIZE → { code }\n // 2. web redirects to desktop's loopback callback with the code\n // 3. desktop renderer POSTs DESKTOP_EXCHANGE with the code → server returns\n // Set-Cookie carrying a fresh session, single-use code is invalidated\n // The two-step flow keeps the actual session id off the redirect URL — only a\n // 5-minute single-use code transits via the loopback.\n DESKTOP_AUTHORIZE: '/auth/desktop/authorize',\n DESKTOP_EXCHANGE: '/auth/desktop/exchange',\n\n CLEANUP_EXPIRED_SESSIONS: '/sessions/expired/cleanup',\n} as const;\n\n/**\n * The login routes and session helpers. Every route is one straight line: read and validate the\n * request, check the credential, call the host's `onAuthorized`, sign in. Failures are thrown and\n * answered in one place per kind of route: a JSON route runs under `guard`, which maps a\n * `RequestError`, an `AuthenticationError` or an `OAuth2Error` to its google.rpc body (see\n * `respond.ts`); the redirect callback maps the same errors to `errorUri` parameters (`redirect`).\n */\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly kv: KVRepository;\n private readonly repository: SessionRepository;\n private readonly turnstileSecretKey?: string;\n private readonly emailCodes: EmailVerificationCodes;\n private readonly desktopCodes: DesktopCodes;\n private readonly oauth2Client: OAuth2Client | null;\n /** Provider keys whose logins may come without a nonce (`oauth2.nonce.optional`). */\n private readonly nonceOptional: ReadonlySet<string>;\n private readonly touchInterval: number;\n private readonly sessionHeader: boolean;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\n oauth2,\n cookie,\n session,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const maxAttempts = verificationCode?.maxAttempts ?? 5;\n const resendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(maxAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(resendCooldown >= 1, 'verificationCode.resendCooldown must be at least 1 second');\n this.emailCodes = new EmailVerificationCodes(kvRepository, { maxAttempts, resendCooldown });\n this.desktopCodes = new DesktopCodes(kvRepository);\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n this.nonceOptional = new Set(oauth2?.nonce?.optional);\n this.touchInterval = (session?.touchInterval ?? 3600) * 1000;\n this.sessionHeader = session?.header ?? false;\n invariant(this.touchInterval >= 0, 'session.touchInterval must not be negative');\n }\n\n // ---- sessions and responses ------------------------------------------------------------------\n\n private getSessionId(request: Request): string | undefined {\n const fromCookie = getCookie(request, this.cookieName);\n if (fromCookie) return fromCookie;\n return (this.sessionHeader && request.headers.get(SESSION_HEADER)) || undefined;\n }\n\n /** The session the request carries, if it exists. */\n private async loadSession(request: Request): Promise<Session | null> {\n const sessionId = this.getSessionId(request);\n return sessionId ? this.repository.findById(sessionId) : null;\n }\n\n /** The signed-in principal of a session, if any. */\n private principalOf(session: Session): Principal | null {\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n return name ? { name: String(name) } : null;\n }\n\n /**\n * A new session signed in as `principal`. With `carryOver`, what the host kept in the pre-login\n * session moves to the new one; the authorization request (state, PKCE verifier, flow\n * attributes) is single-use and was consumed, and a principal left there from an earlier\n * sign-in is replaced. The new id is never the one the browser brought, which may have been\n * planted by an attacker who started the flow (session fixation).\n */\n private async signIn(principal: Principal, carryOver?: Session): Promise<Session> {\n const session = this.repository.createSession();\n for (const attribute of carryOver?.getAttributeNames() ?? []) {\n if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;\n if (attribute === PRINCIPAL_NAME_INDEX_NAME) continue;\n const value = carryOver?.getAttribute(attribute);\n if (value !== null && value !== undefined) session.setAttribute(attribute, value);\n }\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n return session;\n }\n\n /**\n * `response` carrying `session`: its cookie, renewed for the whole inactive interval, and with\n * `session.header` on, the `X-Auth-Token` header a client without a cookie jar reads.\n */\n private withSession(response: Response, session: Session, cookie?: CookieOptions): Response {\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n ...cookie,\n maxAge,\n });\n if (this.sessionHeader) response.headers.set(SESSION_HEADER, session.getId());\n return response;\n }\n\n /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */\n private async guard(run: () => Promise<Response>): Promise<Response> {\n try {\n return await run();\n } catch (error) {\n const response = errorResponse(error);\n if (response) return response;\n throw error;\n }\n }\n\n /** The error redirect of the callback flow; `refusal`'s reason and metadata ride along. */\n private redirect(\n error: OAuth2ErrorType,\n description?: string,\n refusal?: AuthenticationError\n ): Response {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const uri = new URL(this.oauth2Client.errorUri);\n // Metadata never replaces a parameter: not one errorUri carries itself, nor one set below.\n for (const [key, value] of Object.entries(refusal?.metadata ?? {})) {\n if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);\n }\n uri.searchParams.set('error', error);\n if (description) {\n uri.searchParams.set('error_description', description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));\n }\n if (refusal) uri.searchParams.set('reason', refusal.reason);\n return Response.redirect(uri.href, 302);\n }\n\n // ---- basic -----------------------------------------------------------------------------------\n\n csrf = async (_request: Request): Promise<Response> => {\n const token = randomUUID();\n const response = Response.json({\n token,\n parameterName: '_csrf',\n headerName: 'X-XSRF-TOKEN',\n });\n\n // Double Submit Cookie Protection\n setCookie(response, 'XSRF-TOKEN', token, {\n path: '/',\n secure: true,\n httpOnly: false,\n sameSite: 'none',\n domain: this.cookieOptions.domain,\n });\n return response;\n };\n\n logout = async (request: Request, options?: LogoutOptions): Promise<Response> => {\n const scope = options?.scope ?? 'local';\n const sessionId = this.getSessionId(request);\n // `global` and `others` reach the principal's other sessions through the one the request\n // carries; a request without a signed-in session has no principal to widen to, and ends as\n // `local` would.\n if (scope !== 'local' && sessionId) {\n const session = await this.repository.findById(sessionId);\n const principal = session && this.principalOf(session);\n if (principal) {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const id of sessions.keys()) {\n if (id !== sessionId) await this.repository.deleteById(id);\n }\n }\n }\n const response = Response.json({});\n if (scope === 'others') return response;\n if (sessionId) await this.repository.deleteById(sessionId);\n // A Set-Cookie only replaces the stored cookie when Domain and Path match (RFC 6265),\n // so clear it with the same attributes it was set with.\n deleteCookie(response, this.cookieName, this.cookieOptions);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n const session = await this.loadSession(request);\n if (!session) return Response.json({ data: false });\n\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n const principal = this.principalOf(session);\n if (principal) await onLogged?.(principal);\n return this.withSession(Response.json({ data: principal !== null }), session);\n };\n\n // ---- oauth2 ----------------------------------------------------------------------------------\n\n private createPkceParameters(): PkceParameters {\n // 32 random bytes: a 43-char base64url verifier, the RFC 7636 minimum (a UUID is 36).\n const code_verifier = randomBytes(32).toString('base64url');\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n private getOauth2NonceKey(nonce: string) {\n return `oauth2:nonce:${nonce}`;\n }\n\n /**\n * Spends a nonce `oauth2Nonce` issued for `provider`: `NONCE_MISMATCH` for one that was not,\n * or was used already. An id token is replayable until it expires; the nonce it carries makes\n * it sign in once.\n */\n private async consumeNonce(nonce: string, provider: string): Promise<void> {\n const key = this.getOauth2NonceKey(nonce);\n if ((await this.kv.getItem(key)) !== provider) throw new AuthenticationError('NONCE_MISMATCH');\n await this.kv.removeItem(key);\n }\n\n /**\n * The nonce check of the JSON logins (One Tap, native), where the client obtained the id token\n * and the nonce came from `oauth2Nonce`: spends the one the token carries, and refuses a token\n * without one when the provider's flow echoes nonces (`required`) and the host has not listed the\n * provider as optional. An id token is replayable until it expires; the nonce makes it sign in\n * once.\n */\n private async checkIssuedNonce(\n userInfo: Authorization['userInfo'],\n provider: string,\n required: boolean\n ): Promise<boolean> {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce === 'string' && nonce) {\n await this.consumeNonce(nonce, provider);\n return true;\n }\n if (required && !this.nonceOptional.has(provider)) {\n throw new AuthenticationError('NONCE_MISSING');\n }\n return false;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), provider, OAUTH2_PARAMETER_TTL);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, OAUTH2_PARAMETER_TTL);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n // Sent only to a provider whose id token echoes it: the callback then requires it back, so\n // a code that was swapped in for another sign-in's yields a token the session refuses.\n const nonce = (await this.oauth2Client.nonceSupport(provider)).web ? randomUUID() : undefined;\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, state, pkce, nonce });\n let session = await this.loadSession(request);\n if (!session) {\n // A session made for the flow only has to outlive the state: an abandoned flow must not\n // keep a row for the default interval. Sign-in moves to a new session, with the default.\n session = this.repository.createSession();\n session.setMaxInactiveInterval(OAUTH2_PARAMETER_TTL);\n }\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(nonce ? { nonce } : {}),\n ...(attributes ? { attributes } : {}),\n };\n session.setAttribute(\n this.ATTR_OAUTH2_AUTHORIZATION_REQUEST,\n JSON.stringify(authorizationRequest)\n );\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, session);\n });\n\n loginOAuth2Code = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. the session the flow started on, and the authorization request it stored\n const session = await this.loadSession(request);\n if (!session) return this.redirect('invalid_request', 'session not found');\n mark('load_session');\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) return this.redirect('invalid_request', 'authorization request not found');\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 2. the provider's answer: query, or a form for Apple's response_mode=form_post\n const { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n const data = isFormPost(request)\n ? Object.fromEntries(new URLSearchParams(await request.text()))\n : query(request);\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) return this.redirect('invalid_request', 'invalid redirect query');\n // The provider answered with an error instead of a code (the user pressed Cancel →\n // access_denied, invalid_scope, server_error, ...). Forward it rather than reporting a\n // malformed query. This runs before the state check on purpose: providers echo state on\n // error responses, but not all reliably do, and a forged error callback can only make the\n // victim's popup show an error — nothing is exchanged or signed in. A present state that does\n // not match still wins, since that is the more accurate (and more alarming) diagnosis.\n if (parsed.data.error) {\n if (parsed.data.state && parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n return this.redirect(\n safeErrorType(parsed.data.error, 'server_error'),\n parsed.data.error_description\n );\n }\n const { code, state, user } = parsed.data;\n if (!code || !state) return this.redirect('invalid_request', 'invalid redirect query');\n if (state !== cached.state) return this.redirect('invalid_request', 'redirect state mismatch');\n if (provider !== cached.provider) {\n return this.redirect('invalid_request', 'redirect provider mismatch');\n }\n mark('validate_session');\n\n // 3. exchange the code, load the user, sign in. Everything here can fail (an expired or reused\n // code, a network error, the host's handler or session store). The callback runs in a popup\n // the user is looking at, so every failure must land on errorUri, never as a JSON error.\n let signedIn: Session;\n try {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code,\n pkce: cached.additionalParameters,\n });\n // Apple posts the user's name alongside the code, once: hand it to the provider with the token.\n const userInfo = await this.oauth2Client.getUserInfo({\n provider,\n clientId,\n token,\n response: user ? { user } : undefined,\n });\n mark('exchange_code');\n // The id token must carry the nonce this flow sent (stored with the state, not in KV: it\n // is bound to this session's request). A provider that gave no id token has nothing to\n // carry it, as has a request stored before nonces were sent.\n if (cached.nonce && token.id_token) {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce !== 'string' || !nonce) throw new AuthenticationError('NONCE_MISSING');\n if (nonce !== cached.nonce) throw new AuthenticationError('NONCE_MISMATCH');\n mark('check_nonce');\n }\n\n const principal = await handler(request, {\n provider,\n clientId,\n userInfo,\n token,\n attributes: cached.attributes ?? {},\n });\n mark('save_principal');\n\n signedIn = await this.signIn(principal, session);\n await this.repository.deleteById(session.getId());\n mark('update_session');\n } catch (error) {\n // The details stay in the logs: an OAuth2Error message can carry a client id or a\n // provider's response, which has no place in a URL the browser keeps in its history.\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 callback failed', error);\n const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : 'server_error';\n return this.redirect(type);\n }\n // A refusal is something the user can act on, so its reason and metadata reach the page,\n // as they reach a JSON client through the ErrorInfo.\n if (error instanceof AuthenticationError) {\n console.warn('OAuth2 sign-in refused', error.reason, error.metadata);\n return this.redirect('access_denied', undefined, error);\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error');\n }\n\n const uri = new URL(this.oauth2Client.successUri);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, signedIn);\n };\n\n /** The shared tail of the JSON login routes: the host's handler, then a signed-in session. */\n private async authorize(\n request: Request,\n handler: AuthorizedHandler,\n authorization: Authorization,\n setTiming: (response: Response) => void\n ): Promise<Response> {\n const principal = await handler(request, authorization);\n const session = await this.signIn(principal);\n const response = Response.json(principal);\n setTiming(response);\n return this.withSession(response, session);\n }\n\n loginOAuth2Native = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const body = await readJson(request);\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\n // Only the credentials go on to the provider client.\n const { attributes: _, ...credentials } = body as NativeCredential & { attributes?: unknown };\n const { code_verifier } = credentials as { code_verifier?: unknown };\n if (\n code_verifier !== undefined &&\n (typeof code_verifier !== 'string' || !CODE_VERIFIER_FORMAT.test(code_verifier))\n ) {\n throw invalidArgument(\n 'INVALID_CODE_VERIFIER',\n 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]'\n );\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\n credentials,\n });\n mark('login_oauth2_native');\n\n // Required when the provider's native id token echoes a nonce and the login produced one\n // (Facebook's access_token login has none to carry it).\n const required = (await this.oauth2Client.nonceSupport(provider)).native && !!token.id_token;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n loginOAuth2Onetap = (\n request: Request,\n handler: AuthorizedHandler,\n provider = 'google'\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // Google's own form post, or an app's JSON.\n const contentType = request.headers.get('content-type') ?? '';\n const body = contentType.includes('application/json')\n ? await readJson(request)\n : await readForm(request);\n const { attributes = {}, ...parsed } = parse(oneTapBodySchema, body);\n\n // Both the credential and the code are issued to the provider's web client.\n let clientId: string;\n let token: Authorization['token'];\n let userInfo: Authorization['userInfo'];\n if (parsed.credential) {\n ({ clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n }));\n mark('verify_credential');\n\n // The credential must carry a single-use nonce from `oauth2Nonce`, which the front end\n // passed to `prompt`. This replaces Google's `g_csrf_token` double-submit cookie, which\n // FedCM mode no longer sets.\n const required = (await this.oauth2Client.nonceSupport(provider)).web;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n } else if (parsed.code) {\n ({ clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n }));\n userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n } else {\n throw invalidArgument('INVALID_ARGUMENT', 'credential or code is required');\n }\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n // ---- email -----------------------------------------------------------------------------------\n\n sendEmailVerificationCode = (request: Request, sender: EmailSender): Promise<Response> =>\n this.guard(async () => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n const { email, turnstileToken } = parse(\n sendEmailVerificationCodeSchema,\n await readJson(request)\n );\n await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n await this.emailCodes.send(email, sender);\n return Response.json({});\n });\n\n verifyEmailCode = (email: string, verificationCode: string): Promise<void> =>\n this.emailCodes.verify(email, verificationCode);\n\n loginEmail = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n const { setTiming } = timing({ enabled: this.timing });\n const {\n email,\n verificationCode,\n attributes = {},\n } = parse(loginEmailSchema, await readJson(request));\n await this.emailCodes.verify(email, verificationCode);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n return this.authorize(\n request,\n handler,\n { provider: 'EMAIL', userInfo: { claims, data: {} }, attributes },\n setTiming\n );\n });\n\n // ---- desktop ---------------------------------------------------------------------------------\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already completed normal\n * sign-in on the web (so a cookie session exists); this issues a 5-minute single-use code that\n * the web page hands off to the desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier` before opening the\n * browser, derives `code_challenge = base64url(sha256(verifier))`, and threads the challenge\n * through the URL → web page → here. The challenge is pinned to the code; `desktopExchange`\n * enforces that the caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet capture on loopback)\n * from exchanging it.\n *\n * Requires an authenticated cookie session: anonymous callers get 401. Throttling / abuse\n * protection on this endpoint is the API layer's job (rate-limit per principal); the SDK does\n * not gate it because the upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const principal = await this.getPrincipal(request);\n if (!principal) throw unauthenticated('Sign in first');\n const { code_challenge } = parse(desktopAuthorizeSchema, await readJson(request));\n const code = await this.desktopCodes.issue(principal, code_challenge);\n return Response.json({ code });\n });\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop client hands back the auth\n * code it received via the loopback callback, and we mint a fresh session for it, independent\n * of the web cookie session that originally authorized the code, so logging out on either side\n * does not cascade to the other.\n *\n * Cookie attributes: `SameSite=None; Secure` so the cookie survives cross-site fetches from the\n * desktop renderer's `file://` / `app://` origin. The configured `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`); the override is targeted at the\n * SameSite/Secure pair only. In a non-HTTPS dev backend this combination is rejected by\n * Chromium, see the desktop sign-in PRD for workarounds.\n *\n * No CSRF check needed: the request carries no cookie, the auth code itself is the credential,\n * and the code's single-use removal blocks replay.\n */\n desktopExchange = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const { code, code_verifier } = parse(desktopExchangeSchema, await readJson(request));\n const principal = await this.desktopCodes.redeem(code, code_verifier);\n const session = await this.signIn(principal);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n return this.withSession(Response.json({ ok: true }), session, {\n sameSite: 'none',\n secure: true,\n });\n });\n\n // ---- session management ----------------------------------------------------------------------\n\n kick = async (principal: Principal): Promise<void> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const sessionId of sessions.keys()) {\n await this.repository.deleteById(sessionId);\n }\n };\n\n /**\n * Sliding expiration: an authenticated request renews the session, as `logged` does, so a user\n * who keeps using the app is never signed out after `maxInactiveInterval` of not calling one\n * particular endpoint. No extra read: the session was just loaded to authenticate, and its\n * `lastAccessedTime` is compared in memory; writes are spaced by `session.touchInterval` (see\n * `AuthConfig`), so a session accessed again within the interval is not rewritten.\n */\n private async touch(session: Session) {\n const now = Date.now();\n if (now - session.getLastAccessedTime() < this.touchInterval) return;\n session.setLastAccessedTime(now);\n await this.repository.save(session);\n }\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const session = await this.loadSession(request);\n const principal = session && this.principalOf(session);\n if (!principal) return null;\n await this.touch(session);\n return principal;\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n return (await this.getPrincipal(request)) !== null;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n type Result = T extends true ? Session : Session | null;\n const session = await this.loadSession(request);\n if (session || !create) return session as Result;\n const created = this.repository.createSession();\n await this.repository.save(created);\n return created;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\n };\n\n listSessions = async (principal: Principal): Promise<Session[]> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n return Array.from(sessions.values());\n };\n\n cleanupExpiredSessions = async (cleanupCount?: number): Promise<Response> => {\n await this.repository.cleanupExpiredSessions(cleanupCount);\n return Response.json({});\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomBytes, randomUUID } from 'node:crypto';\nimport { invariant } from '@shware/utils';\nimport { extend, literal, object, regex, string } from 'zod/mini';\nimport type { Principal } from '../core/index';\nimport { loginEmailSchema, sendEmailVerificationCodeSchema } from '../email/schema';\nimport { OAuth2Client, googleOneTapSchema, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport { OAuth2Error, type OAuth2ErrorType, isKnownOAuth2ErrorType } from '../oauth2/error';\nimport type {\n NativeCredential,\n OAuth2AuthorizationRequest,\n PkceParameters,\n StandardClaims,\n} from '../oauth2/types';\nimport { PRINCIPAL_NAME_INDEX_NAME } from '../session/common';\nimport type { KVRepository, Session, SessionRepository } from '../session/types';\nimport {\n type CookieOptions,\n deleteCookie,\n getCookie,\n param,\n query,\n setCookie,\n} from '../utils/http';\nimport { timing } from '../utils/timing';\nimport { parse, readForm, readJson, verifyTurnstileToken } from '../utils/valid';\nimport {\n MAX_ATTRIBUTES_BYTES,\n attributesSchema,\n attributesSize,\n bodyAttributes,\n} from './attributes';\nimport { DesktopCodes } from './desktop-codes';\nimport { type EmailSender, EmailVerificationCodes } from './email-codes';\nimport { AuthenticationError } from './error';\nimport { errorResponse, invalidArgument, unauthenticated } from './respond';\nimport type {\n AuthConfig,\n AuthService,\n Authorization,\n AuthorizedHandler,\n LoggedHandler,\n LogoutOptions,\n OAuth2AuthorizationOptions,\n} from './types';\n\n// PKCE per RFC 7636: code_verifier / code_challenge are 43-128 chars from\n// the unreserved set. We restrict to base64url (drops \".\" and \"~\") since\n// that's what S256 produces and what callers should be sending; rejecting\n// the rest shrinks the accepted input shape without losing legit clients.\n// A 32-byte random verifier base64url-encodes to exactly 43 chars.\nconst PKCE_FORMAT = /^[A-Za-z0-9_-]{43,128}$/;\n\n// A native app's `code_verifier` goes on to the provider, which checks it against the challenge\n// the app sent: accept the whole RFC 7636 unreserved set here, as the provider does.\nconst CODE_VERIFIER_FORMAT = /^[A-Za-z0-9._~-]{43,128}$/;\n\n/** The header a session id travels in besides the cookie, with `session.header` on. */\nconst SESSION_HEADER = 'X-Auth-Token';\n\n/** Seconds an OAuth2 state or nonce may be used within. */\nconst OAUTH2_PARAMETER_TTL = 10 * 60;\n\n// error_description forwarded to errorUri comes from the provider or an error message: cap it so\n// the redirect URL stays short and an attacker-crafted callback cannot stuff the page with text.\nconst MAX_ERROR_DESCRIPTION_LENGTH = 256;\n\n/** An error code safe to put on errorUri: a registered one as is, anything else as `fallback`. */\nfunction safeErrorType(error: string, fallback: OAuth2ErrorType): OAuth2ErrorType {\n return isKnownOAuth2ErrorType(error) ? error : fallback;\n}\n\n/**\n * The codes a failure past validation keeps on errorUri, each one the user can act on: the code\n * expired or was used (start again), they declined, or the provider is briefly down. Every other\n * failure there (a rejected client, a user info request that failed) is the app's or the\n * provider's problem, which the user sees as `server_error`.\n */\nconst CALLBACK_ERROR_TYPES: ReadonlySet<OAuth2ErrorType> = new Set([\n 'invalid_grant',\n 'access_denied',\n 'temporarily_unavailable',\n]);\n\n/**\n * The host's values for the flow (`attributes`, `redirectParams`), `undefined` ones dropped; `name`\n * is what the size invariant calls them.\n */\nfunction authorizationAttributes(\n attributes: OAuth2AuthorizationOptions['attributes'],\n name = 'attributes'\n): Record<string, string> | undefined {\n if (!attributes) return undefined;\n const defined: Record<string, string> = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined) defined[key] = value;\n }\n if (Object.keys(defined).length === 0) return undefined;\n // The host's own values, written with the session: oversized is a bug in the host, not a 400.\n const size = attributesSize(defined);\n invariant(\n size <= MAX_ATTRIBUTES_BYTES,\n `oauth2 authorization ${name} are ${size} bytes, at most ${MAX_ATTRIBUTES_BYTES} allowed`\n );\n return defined;\n}\n\ninterface RedirectDetails {\n description?: string;\n refusal?: AuthenticationError;\n /** The flow's `redirectParams`. */\n params?: Record<string, string>;\n}\n\n/**\n * What the callback's redirects say themselves. A host's `redirectParams` never carry these, even\n * on a redirect that leaves one out (a cancel has no `reason`): the page would read it as the SDK's.\n */\nconst REDIRECT_PARAMS = new Set(['status', 'provider', 'error', 'error_description', 'reason']);\n\n/** The host's `redirectParams` without the names the redirects keep for themselves. */\nfunction redirectParams(params: OAuth2AuthorizationOptions['redirectParams']) {\n if (!params) return undefined;\n const own = Object.entries(params).filter(([key]) => !REDIRECT_PARAMS.has(key));\n return authorizationAttributes(Object.fromEntries(own), 'redirectParams');\n}\n\n/** `base` with `params` added, none replacing one it carries itself. */\nfunction withParams(base: string, params: Record<string, string> = {}) {\n const uri = new URL(base);\n for (const [key, value] of Object.entries(params)) {\n if (!uri.searchParams.has(key)) uri.searchParams.set(key, value);\n }\n return uri;\n}\n\n/** Sign in with Apple answers with `response_mode=form_post`; a charset parameter may follow. */\nfunction isFormPost(request: Request) {\n const contentType = request.headers.get('content-type')?.toLowerCase();\n return (\n request.method === 'POST' && !!contentType?.startsWith('application/x-www-form-urlencoded')\n );\n}\n\n/** Google's own form post carries no attributes; an app posting JSON may add them. */\nconst oneTapBodySchema = extend(googleOneTapSchema, { attributes: attributesSchema });\n\nconst desktopAuthorizeSchema = object({\n code_challenge: string().check(regex(PKCE_FORMAT)),\n code_challenge_method: literal('S256'),\n});\n\nconst desktopExchangeSchema = object({\n code: string(),\n code_verifier: string().check(regex(PKCE_FORMAT)),\n});\n\nexport const PATH = {\n CSRF: '/csrf',\n LOGOUT: '/logout',\n LOGGED: '/logged',\n\n OAUTH2_STATE: '/oauth2/state/:provider',\n OAUTH2_NONCE: '/oauth2/nonce/:provider',\n OAUTH2_AUTHORIZATION: '/oauth2/authorization/:provider',\n LOGIN_OAUTH2_CODE: '/login/oauth2/code/:provider',\n LOGIN_OAUTH2_NATIVE: '/login/oauth2/native/:provider',\n LOGIN_OAUTH2_ONETAP: '/login/oauth2/onetap/google',\n\n LOGIN_EMAIL: '/login/email',\n SEND_EMAIL_VERIFICATION_CODE: '/auth/sendEmailVerificationCode',\n\n // Desktop / native app sign-in via web loopback (RFC 8252 + OAuth 2.1 Auth Code Flow):\n // 1. user already authenticated with cookie session → POST DESKTOP_AUTHORIZE → { code }\n // 2. web redirects to desktop's loopback callback with the code\n // 3. desktop renderer POSTs DESKTOP_EXCHANGE with the code → server returns\n // Set-Cookie carrying a fresh session, single-use code is invalidated\n // The two-step flow keeps the actual session id off the redirect URL — only a\n // 5-minute single-use code transits via the loopback.\n DESKTOP_AUTHORIZE: '/auth/desktop/authorize',\n DESKTOP_EXCHANGE: '/auth/desktop/exchange',\n\n CLEANUP_EXPIRED_SESSIONS: '/sessions/expired/cleanup',\n} as const;\n\n/**\n * The login routes and session helpers. Every route is one straight line: read and validate the\n * request, check the credential, call the host's `onAuthorized`, sign in. Failures are thrown and\n * answered in one place per kind of route: a JSON route runs under `guard`, which maps a\n * `RequestError`, an `AuthenticationError` or an `OAuth2Error` to its google.rpc body (see\n * `respond.ts`); the redirect callback maps the same errors to `errorUri` parameters (`redirect`).\n */\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly kv: KVRepository;\n private readonly repository: SessionRepository;\n private readonly turnstileSecretKey?: string;\n private readonly emailCodes: EmailVerificationCodes;\n private readonly desktopCodes: DesktopCodes;\n private readonly oauth2Client: OAuth2Client | null;\n /** Provider keys whose logins may come without a nonce (`oauth2.nonce.optional`). */\n private readonly nonceOptional: ReadonlySet<string>;\n private readonly touchInterval: number;\n private readonly sessionHeader: boolean;\n private readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({\n sessionRepository,\n kvRepository,\n turnstileSecretKey,\n verificationCode,\n oauth2,\n cookie,\n session,\n timing,\n }: AuthConfig) {\n this.timing = timing ?? false;\n this.kv = kvRepository;\n this.repository = sessionRepository;\n this.turnstileSecretKey = turnstileSecretKey;\n const maxAttempts = verificationCode?.maxAttempts ?? 5;\n const resendCooldown = verificationCode?.resendCooldown ?? 60;\n invariant(maxAttempts >= 1, 'verificationCode.maxAttempts must be at least 1');\n invariant(resendCooldown >= 1, 'verificationCode.resendCooldown must be at least 1 second');\n this.emailCodes = new EmailVerificationCodes(kvRepository, { maxAttempts, resendCooldown });\n this.desktopCodes = new DesktopCodes(kvRepository);\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n this.nonceOptional = new Set(oauth2?.nonce?.optional);\n this.touchInterval = (session?.touchInterval ?? 3600) * 1000;\n this.sessionHeader = session?.header ?? false;\n invariant(this.touchInterval >= 0, 'session.touchInterval must not be negative');\n }\n\n // ---- sessions and responses ------------------------------------------------------------------\n\n private getSessionId(request: Request): string | undefined {\n const fromCookie = getCookie(request, this.cookieName);\n if (fromCookie) return fromCookie;\n return (this.sessionHeader && request.headers.get(SESSION_HEADER)) || undefined;\n }\n\n /** The session the request carries, if it exists. */\n private async loadSession(request: Request): Promise<Session | null> {\n const sessionId = this.getSessionId(request);\n return sessionId ? this.repository.findById(sessionId) : null;\n }\n\n /** The signed-in principal of a session, if any. */\n private principalOf(session: Session): Principal | null {\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n return name ? { name: String(name) } : null;\n }\n\n /**\n * A new session signed in as `principal`. With `carryOver`, what the host kept in the pre-login\n * session moves to the new one; the authorization request (state, PKCE verifier, flow\n * attributes) is single-use and was consumed, and a principal left there from an earlier\n * sign-in is replaced. The new id is never the one the browser brought, which may have been\n * planted by an attacker who started the flow (session fixation).\n */\n private async signIn(principal: Principal, carryOver?: Session): Promise<Session> {\n const session = this.repository.createSession();\n for (const attribute of carryOver?.getAttributeNames() ?? []) {\n if (attribute === this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) continue;\n if (attribute === PRINCIPAL_NAME_INDEX_NAME) continue;\n const value = carryOver?.getAttribute(attribute);\n if (value !== null && value !== undefined) session.setAttribute(attribute, value);\n }\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n return session;\n }\n\n /**\n * `response` carrying `session`: its cookie, renewed for the whole inactive interval, and with\n * `session.header` on, the `X-Auth-Token` header a client without a cookie jar reads.\n */\n private withSession(response: Response, session: Session, cookie?: CookieOptions): Response {\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), {\n ...this.cookieOptions,\n ...cookie,\n maxAge,\n });\n if (this.sessionHeader) response.headers.set(SESSION_HEADER, session.getId());\n return response;\n }\n\n /** Runs a JSON route: a failure it knows is its response, anything else is the host's. */\n private async guard(run: () => Promise<Response>): Promise<Response> {\n try {\n return await run();\n } catch (error) {\n const response = errorResponse(error);\n if (response) return response;\n throw error;\n }\n }\n\n /**\n * The error redirect of the callback flow: `refusal`'s reason and metadata ride along, and the\n * flow's `redirectParams` once its authorization request is known.\n */\n private redirect(\n error: OAuth2ErrorType,\n { description, refusal, params }: RedirectDetails = {}\n ): Response {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n // A refusal's metadata wins over the host's parameters of the same name: it is about this answer.\n const uri = withParams(this.oauth2Client.errorUri, { ...params, ...refusal?.metadata });\n uri.searchParams.set('error', error);\n if (description) {\n uri.searchParams.set('error_description', description.slice(0, MAX_ERROR_DESCRIPTION_LENGTH));\n }\n if (refusal) uri.searchParams.set('reason', refusal.reason);\n return Response.redirect(uri.href, 302);\n }\n\n // ---- basic -----------------------------------------------------------------------------------\n\n csrf = async (_request: Request): Promise<Response> => {\n const token = randomUUID();\n const response = Response.json({\n token,\n parameterName: '_csrf',\n headerName: 'X-XSRF-TOKEN',\n });\n\n // Double Submit Cookie Protection\n setCookie(response, 'XSRF-TOKEN', token, {\n path: '/',\n secure: true,\n httpOnly: false,\n sameSite: 'none',\n domain: this.cookieOptions.domain,\n });\n return response;\n };\n\n logout = async (request: Request, options?: LogoutOptions): Promise<Response> => {\n const scope = options?.scope ?? 'local';\n const sessionId = this.getSessionId(request);\n // `global` and `others` reach the principal's other sessions through the one the request\n // carries; a request without a signed-in session has no principal to widen to, and ends as\n // `local` would.\n if (scope !== 'local' && sessionId) {\n const session = await this.repository.findById(sessionId);\n const principal = session && this.principalOf(session);\n if (principal) {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const id of sessions.keys()) {\n if (id !== sessionId) await this.repository.deleteById(id);\n }\n }\n }\n const response = Response.json({});\n if (scope === 'others') return response;\n if (sessionId) await this.repository.deleteById(sessionId);\n // A Set-Cookie only replaces the stored cookie when Domain and Path match (RFC 6265),\n // so clear it with the same attributes it was set with.\n deleteCookie(response, this.cookieName, this.cookieOptions);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n const session = await this.loadSession(request);\n if (!session) return Response.json({ data: false });\n\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n const principal = this.principalOf(session);\n if (principal) await onLogged?.(principal);\n return this.withSession(Response.json({ data: principal !== null }), session);\n };\n\n // ---- oauth2 ----------------------------------------------------------------------------------\n\n private createPkceParameters(): PkceParameters {\n // 32 random bytes: a 43-char base64url verifier, the RFC 7636 minimum (a UUID is 36).\n const code_verifier = randomBytes(32).toString('base64url');\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n private getOauth2NonceKey(nonce: string) {\n return `oauth2:nonce:${nonce}`;\n }\n\n /**\n * Spends a nonce `oauth2Nonce` issued for `provider`: `NONCE_MISMATCH` for one that was not,\n * or was used already. An id token is replayable until it expires; the nonce it carries makes\n * it sign in once.\n */\n private async consumeNonce(nonce: string, provider: string): Promise<void> {\n const key = this.getOauth2NonceKey(nonce);\n if ((await this.kv.getItem(key)) !== provider) throw new AuthenticationError('NONCE_MISMATCH');\n await this.kv.removeItem(key);\n }\n\n /**\n * The nonce check of the JSON logins (One Tap, native), where the client obtained the id token\n * and the nonce came from `oauth2Nonce`: spends the one the token carries, and refuses a token\n * without one when the provider's flow echoes nonces (`required`) and the host has not listed the\n * provider as optional. An id token is replayable until it expires; the nonce makes it sign in\n * once.\n */\n private async checkIssuedNonce(\n userInfo: Authorization['userInfo'],\n provider: string,\n required: boolean\n ): Promise<boolean> {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce === 'string' && nonce) {\n await this.consumeNonce(nonce, provider);\n return true;\n }\n if (required && !this.nonceOptional.has(provider)) {\n throw new AuthenticationError('NONCE_MISSING');\n }\n return false;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_STATE);\n const state = randomUUID();\n await this.kv.setItem(this.getOauth2StateKey(state), provider, OAUTH2_PARAMETER_TTL);\n return Response.json({ state });\n };\n\n oauth2Nonce = async (request: Request): Promise<Response> => {\n const { provider } = param(request, PATH.OAUTH2_NONCE);\n const nonce = randomUUID();\n await this.kv.setItem(this.getOauth2NonceKey(nonce), provider, OAUTH2_PARAMETER_TTL);\n return Response.json({ nonce });\n };\n\n oauth2Authorization = (\n request: Request,\n options?: OAuth2AuthorizationOptions\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const attributes = authorizationAttributes(options?.attributes);\n const params = redirectParams(options?.redirectParams);\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n // Sent only to a provider whose id token echoes it: the callback then requires it back, so\n // a code that was swapped in for another sign-in's yields a token the session refuses.\n const nonce = (await this.oauth2Client.nonceSupport(provider)).web ? randomUUID() : undefined;\n const uri = await this.oauth2Client.createAuthorizationUri({ provider, state, pkce, nonce });\n let session = await this.loadSession(request);\n if (!session) {\n // A session made for the flow only has to outlive the state: an abandoned flow must not\n // keep a row for the default interval. Sign-in moves to a new session, with the default.\n session = this.repository.createSession();\n session.setMaxInactiveInterval(OAUTH2_PARAMETER_TTL);\n }\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n provider,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n ...(nonce ? { nonce } : {}),\n ...(attributes ? { attributes } : {}),\n ...(params ? { redirectParams: params } : {}),\n };\n session.setAttribute(\n this.ATTR_OAUTH2_AUTHORIZATION_REQUEST,\n JSON.stringify(authorizationRequest)\n );\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, session);\n });\n\n loginOAuth2Code = async (request: Request, handler: AuthorizedHandler): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. the session the flow started on, and the authorization request it stored\n const session = await this.loadSession(request);\n if (!session) return this.redirect('invalid_request', { description: 'session not found' });\n mark('load_session');\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', { description: 'authorization request not found' });\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n // From here on the redirect back to the app is this flow's, with the host's parameters.\n const params = cached.redirectParams;\n\n // 2. the provider's answer: query, or a form for Apple's response_mode=form_post\n const { provider } = param(request, PATH.LOGIN_OAUTH2_CODE);\n const data = isFormPost(request)\n ? Object.fromEntries(new URLSearchParams(await request.text()))\n : query(request);\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', { description: 'invalid redirect query', params });\n }\n // The provider answered with an error instead of a code (the user pressed Cancel →\n // access_denied, invalid_scope, server_error, ...). Forward it rather than reporting a\n // malformed query. This runs before the state check on purpose: providers echo state on\n // error responses, but not all reliably do, and a forged error callback can only make the\n // victim's popup show an error — nothing is exchanged or signed in. A present state that does\n // not match still wins, since that is the more accurate (and more alarming) diagnosis.\n if (parsed.data.error) {\n if (parsed.data.state && parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', { description: 'redirect state mismatch', params });\n }\n return this.redirect(safeErrorType(parsed.data.error, 'server_error'), {\n description: parsed.data.error_description,\n params,\n });\n }\n const { code, state, user } = parsed.data;\n if (!code || !state) {\n return this.redirect('invalid_request', { description: 'invalid redirect query', params });\n }\n if (state !== cached.state) {\n return this.redirect('invalid_request', { description: 'redirect state mismatch', params });\n }\n if (provider !== cached.provider) {\n return this.redirect('invalid_request', {\n description: 'redirect provider mismatch',\n params,\n });\n }\n mark('validate_session');\n\n // 3. exchange the code, load the user, sign in. Everything here can fail (an expired or reused\n // code, a network error, the host's handler or session store). The callback runs in a popup\n // the user is looking at, so every failure must land on errorUri, never as a JSON error.\n let signedIn: Session;\n try {\n const { clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code,\n pkce: cached.additionalParameters,\n });\n // Apple posts the user's name alongside the code, once: hand it to the provider with the token.\n const userInfo = await this.oauth2Client.getUserInfo({\n provider,\n clientId,\n token,\n response: user ? { user } : undefined,\n });\n mark('exchange_code');\n // The id token must carry the nonce this flow sent (stored with the state, not in KV: it\n // is bound to this session's request). A provider that gave no id token has nothing to\n // carry it, as has a request stored before nonces were sent.\n if (cached.nonce && token.id_token) {\n const { nonce } = userInfo.data as { nonce?: unknown };\n if (typeof nonce !== 'string' || !nonce) throw new AuthenticationError('NONCE_MISSING');\n if (nonce !== cached.nonce) throw new AuthenticationError('NONCE_MISMATCH');\n mark('check_nonce');\n }\n\n const principal = await handler(request, {\n provider,\n clientId,\n userInfo,\n token,\n attributes: cached.attributes ?? {},\n });\n mark('save_principal');\n\n signedIn = await this.signIn(principal, session);\n await this.repository.deleteById(session.getId());\n mark('update_session');\n } catch (error) {\n // The details stay in the logs: an OAuth2Error message can carry a client id or a\n // provider's response, which has no place in a URL the browser keeps in its history.\n if (error instanceof OAuth2Error) {\n console.warn('OAuth2 callback failed', error);\n const type = CALLBACK_ERROR_TYPES.has(error.error) ? error.error : 'server_error';\n return this.redirect(type, { params });\n }\n // A refusal is something the user can act on, so its reason and metadata reach the page,\n // as they reach a JSON client through the ErrorInfo.\n if (error instanceof AuthenticationError) {\n console.warn('OAuth2 sign-in refused', error.reason, error.metadata);\n return this.redirect('access_denied', { refusal: error, params });\n }\n console.error('OAuth2 callback failed', error);\n return this.redirect('server_error', { params });\n }\n\n const uri = withParams(this.oauth2Client.successUri, params);\n uri.searchParams.set('status', 'success');\n uri.searchParams.set('provider', provider);\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n setTiming(response);\n return this.withSession(response, signedIn);\n };\n\n /** The shared tail of the JSON login routes: the host's handler, then a signed-in session. */\n private async authorize(\n request: Request,\n handler: AuthorizedHandler,\n authorization: Authorization,\n setTiming: (response: Response) => void\n ): Promise<Response> {\n const principal = await handler(request, authorization);\n const session = await this.signIn(principal);\n const response = Response.json(principal);\n setTiming(response);\n return this.withSession(response, session);\n }\n\n loginOAuth2Native = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { provider } = param(request, PATH.LOGIN_OAUTH2_NATIVE);\n const body = await readJson(request);\n // Whatever the app sends besides its credentials for `onAuthorized` (a referral code, ...).\n const attributes = bodyAttributes(body);\n // Only the credentials go on to the provider client.\n const { attributes: _, ...credentials } = body as NativeCredential & { attributes?: unknown };\n const { code_verifier } = credentials as { code_verifier?: unknown };\n if (\n code_verifier !== undefined &&\n (typeof code_verifier !== 'string' || !CODE_VERIFIER_FORMAT.test(code_verifier))\n ) {\n throw invalidArgument(\n 'INVALID_CODE_VERIFIER',\n 'code_verifier must be 43-128 characters from [A-Za-z0-9-._~]'\n );\n }\n const { clientId, userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n provider,\n credentials,\n });\n mark('login_oauth2_native');\n\n // Required when the provider's native id token echoes a nonce and the login produced one\n // (Facebook's access_token login has none to carry it).\n const required = (await this.oauth2Client.nonceSupport(provider)).native && !!token.id_token;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n loginOAuth2Onetap = (\n request: Request,\n handler: AuthorizedHandler,\n provider = 'google'\n ): Promise<Response> =>\n this.guard(async () => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // Google's own form post, or an app's JSON.\n const contentType = request.headers.get('content-type') ?? '';\n const body = contentType.includes('application/json')\n ? await readJson(request)\n : await readForm(request);\n const { attributes = {}, ...parsed } = parse(oneTapBodySchema, body);\n\n // Both the credential and the code are issued to the provider's web client.\n let clientId: string;\n let token: Authorization['token'];\n let userInfo: Authorization['userInfo'];\n if (parsed.credential) {\n ({ clientId, token, userInfo } = await this.oauth2Client.getTokenInfo({\n provider,\n credential: parsed.credential,\n }));\n mark('verify_credential');\n\n // The credential must carry a single-use nonce from `oauth2Nonce`, which the front end\n // passed to `prompt`. This replaces Google's `g_csrf_token` double-submit cookie, which\n // FedCM mode no longer sets.\n const required = (await this.oauth2Client.nonceSupport(provider)).web;\n if (await this.checkIssuedNonce(userInfo, provider, required)) mark('check_nonce');\n } else if (parsed.code) {\n ({ clientId, token } = await this.oauth2Client.exchangeAuthorizationCode({\n provider,\n code: parsed.code,\n }));\n userInfo = await this.oauth2Client.getUserInfo({ provider, clientId, token });\n } else {\n throw invalidArgument('INVALID_ARGUMENT', 'credential or code is required');\n }\n\n return this.authorize(\n request,\n handler,\n { provider, clientId, userInfo, token, attributes },\n setTiming\n );\n });\n\n // ---- email -----------------------------------------------------------------------------------\n\n sendEmailVerificationCode = (request: Request, sender: EmailSender): Promise<Response> =>\n this.guard(async () => {\n invariant(this.turnstileSecretKey, 'turnstileSecretKey is not configured');\n const { email, turnstileToken } = parse(\n sendEmailVerificationCodeSchema,\n await readJson(request)\n );\n await verifyTurnstileToken(this.turnstileSecretKey, turnstileToken);\n await this.emailCodes.send(email, sender);\n return Response.json({});\n });\n\n verifyEmailCode = (email: string, verificationCode: string): Promise<void> =>\n this.emailCodes.verify(email, verificationCode);\n\n loginEmail = (request: Request, handler: AuthorizedHandler): Promise<Response> =>\n this.guard(async () => {\n const { setTiming } = timing({ enabled: this.timing });\n const {\n email,\n verificationCode,\n attributes = {},\n } = parse(loginEmailSchema, await readJson(request));\n await this.emailCodes.verify(email, verificationCode);\n const claims: StandardClaims = { sub: email, email, email_verified: true };\n return this.authorize(\n request,\n handler,\n { provider: 'EMAIL', userInfo: { claims, data: {} }, attributes },\n setTiming\n );\n });\n\n // ---- desktop ---------------------------------------------------------------------------------\n\n /**\n * Step 1 of the desktop / native app sign-in handshake. The caller already completed normal\n * sign-in on the web (so a cookie session exists); this issues a 5-minute single-use code that\n * the web page hands off to the desktop client via a loopback redirect.\n *\n * PKCE (RFC 7636, S256 only): the desktop generates a random `code_verifier` before opening the\n * browser, derives `code_challenge = base64url(sha256(verifier))`, and threads the challenge\n * through the URL → web page → here. The challenge is pinned to the code; `desktopExchange`\n * enforces that the caller knows the matching verifier. This blocks an attacker who only\n * intercepts the auth code (browser history, malicious extension, packet capture on loopback)\n * from exchanging it.\n *\n * Requires an authenticated cookie session: anonymous callers get 401. Throttling / abuse\n * protection on this endpoint is the API layer's job (rate-limit per principal); the SDK does\n * not gate it because the upstream sign-in flow already passed CAPTCHA / OAuth / verification.\n */\n desktopAuthorize = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const principal = await this.getPrincipal(request);\n if (!principal) throw unauthenticated('Sign in first');\n const { code_challenge } = parse(desktopAuthorizeSchema, await readJson(request));\n const code = await this.desktopCodes.issue(principal, code_challenge);\n return Response.json({ code });\n });\n\n /**\n * Step 2 of the desktop / native app sign-in handshake. The desktop client hands back the auth\n * code it received via the loopback callback, and we mint a fresh session for it, independent\n * of the web cookie session that originally authorized the code, so logging out on either side\n * does not cascade to the other.\n *\n * Cookie attributes: `SameSite=None; Secure` so the cookie survives cross-site fetches from the\n * desktop renderer's `file://` / `app://` origin. The configured `cookieOptions` provide\n * everything else (`domain`, `path`, `httpOnly`); the override is targeted at the\n * SameSite/Secure pair only. In a non-HTTPS dev backend this combination is rejected by\n * Chromium, see the desktop sign-in PRD for workarounds.\n *\n * No CSRF check needed: the request carries no cookie, the auth code itself is the credential,\n * and the code's single-use removal blocks replay.\n */\n desktopExchange = (request: Request): Promise<Response> =>\n this.guard(async () => {\n const { code, code_verifier } = parse(desktopExchangeSchema, await readJson(request));\n const principal = await this.desktopCodes.redeem(code, code_verifier);\n const session = await this.signIn(principal);\n session.setAttribute('client_type', 'desktop');\n await this.repository.save(session);\n return this.withSession(Response.json({ ok: true }), session, {\n sameSite: 'none',\n secure: true,\n });\n });\n\n // ---- session management ----------------------------------------------------------------------\n\n kick = async (principal: Principal): Promise<void> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n for (const sessionId of sessions.keys()) {\n await this.repository.deleteById(sessionId);\n }\n };\n\n /**\n * Sliding expiration: an authenticated request renews the session, as `logged` does, so a user\n * who keeps using the app is never signed out after `maxInactiveInterval` of not calling one\n * particular endpoint. No extra read: the session was just loaded to authenticate, and its\n * `lastAccessedTime` is compared in memory; writes are spaced by `session.touchInterval` (see\n * `AuthConfig`), so a session accessed again within the interval is not rewritten.\n */\n private async touch(session: Session) {\n const now = Date.now();\n if (now - session.getLastAccessedTime() < this.touchInterval) return;\n session.setLastAccessedTime(now);\n await this.repository.save(session);\n }\n\n getPrincipal = async (request: Request): Promise<Principal | null> => {\n const session = await this.loadSession(request);\n const principal = session && this.principalOf(session);\n if (!principal) return null;\n await this.touch(session);\n return principal;\n };\n\n isAuthenticated = async (request: Request): Promise<boolean> => {\n return (await this.getPrincipal(request)) !== null;\n };\n\n getSession = async <T extends boolean>(\n request: Request,\n create: T\n ): Promise<T extends true ? Session : Session | null> => {\n type Result = T extends true ? Session : Session | null;\n const session = await this.loadSession(request);\n if (session || !create) return session as Result;\n const created = this.repository.createSession();\n await this.repository.save(created);\n return created;\n };\n\n deleteSession = async (sessionId: string): Promise<void> => {\n await this.repository.deleteById(sessionId);\n };\n\n listSessions = async (principal: Principal): Promise<Session[]> => {\n const sessions = await this.repository.findByPrincipalName(principal.name);\n return Array.from(sessions.values());\n };\n\n cleanupExpiredSessions = async (cleanupCount?: number): Promise<Response> => {\n await this.repository.cleanupExpiredSessions(cleanupCount);\n return Response.json({});\n };\n}\n"],"mappings":"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"}
@@ -43,6 +43,17 @@ export interface OAuth2AuthorizationOptions {
43
43
  * attribution or where to go next, not for authorization decisions.
44
44
  */
45
45
  attributes?: Record<string, string | undefined>;
46
+ /**
47
+ * Query parameters added to this flow's redirect back to the app, on `successUri` and on
48
+ * `errorUri` alike, e.g. an id the page that opened a sign-in popup matches the result against.
49
+ * Unlike `attributes` they reach the browser. Stored with the authorization request, so a
50
+ * callback that cannot find it (no session) goes without them. The names the redirects use
51
+ * themselves (`status`, `provider`, `error`, `error_description`, `reason`) are dropped, even
52
+ * where a redirect leaves one out, and a refusal's metadata wins over a parameter it shares a name
53
+ * with. `undefined` values are dropped, and the same 2 KB limit applies, as an invariant: a value
54
+ * from the browser (a query parameter) is the host's to check first.
55
+ */
56
+ redirectParams?: Record<string, string | undefined>;
46
57
  }
47
58
  export interface AuthConfig {
48
59
  timing?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.cts","names":[],"sources":["../../src/auth/types.ts"],"mappings":";;;;;YAMY,iBAAiB,WAAW,cAAc;;;;;;;;;;;;;;;iBAerC;EACf;EACA;EACA,UAAU;EACV,QAAQ;EACR,YAAY;;;;;;;YAQF,qBACV,SAAS,SACT,eAAe,kBACZ,QAAQ;iBAEI;;;;;;;;;;;EAWf,aAAa;;iBAGE;EACf;EACA,cAAc;EACd,mBAAmB;EACnB,SAAS;IAAkB;;EAC3B;;;;;;;;;IASE;;;;;;;;;;;IAWA;;EAEF;;EAEA;;;;;IAKE;;;;;IAKA;;EAEF;IACE,SAAS;IACT;;;;;;;;;;MAUE;;;;;;;;;;YAWM;iBAEK;EACf,QAAQ;;iBAGO;EAEf,OAAO,SAAS,YAAY,QAAQ;EACpC,SAAS,SAAS,SAAS,UAAU,kBAAkB,QAAQ;EAC/D,SAAS,SAAS,SAAS,WAAW,kBAAkB,QAAQ;EAGhE,cAAc,SAAS,YAAY,QAAQ;EAC3C,cAAc,SAAS,YAAY,QAAQ;EAC3C,sBACE,SAAS,SACT,UAAU,+BACP,QAAQ;EACb,kBAAkB,SAAS,SAAS,cAAc,sBAAsB,QAAQ;EAChF,oBAAoB,SAAS,SAAS,cAAc,sBAAsB,QAAQ;EAClF,oBACE,SAAS,SACT,cAAc,mBACd,sBACG,QAAQ;EAGb,4BAA4B,SAAS,SAAS,QAAQ,gBAAgB,QAAQ;EAC9E,aAAa,SAAS,SAAS,cAAc,sBAAsB,QAAQ;;;;;;EAM3E,kBAAkB,eAAe,6BAA6B;EAG9D,mBAAmB,SAAS,YAAY,QAAQ;EAChD,kBAAkB,SAAS,YAAY,QAAQ;EAG/C,OAAO,WAAW,cAAc;EAChC,kBAAkB,SAAS,YAAY;EACvC,aAAa,mBACX,SAAS,SACT,QAAQ,MACL,QAAQ,iBAAiB,UAAU;EACxC,gBAAgB,sBAAsB;EACtC,eAAe,SAAS,YAAY,QAAQ;EAC5C,eAAe,WAAW,cAAc,QAAQ;EAChD,yBAAyB,0BAA0B,QAAQ"}
1
+ {"version":3,"file":"types.d.cts","names":[],"sources":["../../src/auth/types.ts"],"mappings":";;;;;YAMY,iBAAiB,WAAW,cAAc;;;;;;;;;;;;;;;iBAerC;EACf;EACA;EACA,UAAU;EACV,QAAQ;EACR,YAAY;;;;;;;YAQF,qBACV,SAAS,SACT,eAAe,kBACZ,QAAQ;iBAEI;;;;;;;;;;;EAWf,aAAa;;;;;;;;;;;EAWb,iBAAiB;;iBAGF;EACf;EACA,cAAc;EACd,mBAAmB;EACnB,SAAS;IAAkB;;EAC3B;;;;;;;;;IASE;;;;;;;;;;;IAWA;;EAEF;;EAEA;;;;;IAKE;;;;;IAKA;;EAEF;IACE,SAAS;IACT;;;;;;;;;;MAUE;;;;;;;;;;YAWM;iBAEK;EACf,QAAQ;;iBAGO;EAEf,OAAO,SAAS,YAAY,QAAQ;EACpC,SAAS,SAAS,SAAS,UAAU,kBAAkB,QAAQ;EAC/D,SAAS,SAAS,SAAS,WAAW,kBAAkB,QAAQ;EAGhE,cAAc,SAAS,YAAY,QAAQ;EAC3C,cAAc,SAAS,YAAY,QAAQ;EAC3C,sBACE,SAAS,SACT,UAAU,+BACP,QAAQ;EACb,kBAAkB,SAAS,SAAS,cAAc,sBAAsB,QAAQ;EAChF,oBAAoB,SAAS,SAAS,cAAc,sBAAsB,QAAQ;EAClF,oBACE,SAAS,SACT,cAAc,mBACd,sBACG,QAAQ;EAGb,4BAA4B,SAAS,SAAS,QAAQ,gBAAgB,QAAQ;EAC9E,aAAa,SAAS,SAAS,cAAc,sBAAsB,QAAQ;;;;;;EAM3E,kBAAkB,eAAe,6BAA6B;EAG9D,mBAAmB,SAAS,YAAY,QAAQ;EAChD,kBAAkB,SAAS,YAAY,QAAQ;EAG/C,OAAO,WAAW,cAAc;EAChC,kBAAkB,SAAS,YAAY;EACvC,aAAa,mBACX,SAAS,SACT,QAAQ,MACL,QAAQ,iBAAiB,UAAU;EACxC,gBAAgB,sBAAsB;EACtC,eAAe,SAAS,YAAY,QAAQ;EAC5C,eAAe,WAAW,cAAc,QAAQ;EAChD,yBAAyB,0BAA0B,QAAQ"}
@@ -43,6 +43,17 @@ export interface OAuth2AuthorizationOptions {
43
43
  * attribution or where to go next, not for authorization decisions.
44
44
  */
45
45
  attributes?: Record<string, string | undefined>;
46
+ /**
47
+ * Query parameters added to this flow's redirect back to the app, on `successUri` and on
48
+ * `errorUri` alike, e.g. an id the page that opened a sign-in popup matches the result against.
49
+ * Unlike `attributes` they reach the browser. Stored with the authorization request, so a
50
+ * callback that cannot find it (no session) goes without them. The names the redirects use
51
+ * themselves (`status`, `provider`, `error`, `error_description`, `reason`) are dropped, even
52
+ * where a redirect leaves one out, and a refusal's metadata wins over a parameter it shares a name
53
+ * with. `undefined` values are dropped, and the same 2 KB limit applies, as an invariant: a value
54
+ * from the browser (a query parameter) is the host's to check first.
55
+ */
56
+ redirectParams?: Record<string, string | undefined>;
46
57
  }
47
58
  export interface AuthConfig {
48
59
  timing?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../src/auth/types.ts"],"mappings":";;;;;YAMY,iBAAiB,WAAW,cAAc;;;;;;;;;;;;;;;iBAerC;EACf;EACA;EACA,UAAU;EACV,QAAQ;EACR,YAAY;;;;;;;YAQF,qBACV,SAAS,SACT,eAAe,kBACZ,QAAQ;iBAEI;;;;;;;;;;;EAWf,aAAa;;iBAGE;EACf;EACA,cAAc;EACd,mBAAmB;EACnB,SAAS;IAAkB;;EAC3B;;;;;;;;;IASE;;;;;;;;;;;IAWA;;EAEF;;EAEA;;;;;IAKE;;;;;IAKA;;EAEF;IACE,SAAS;IACT;;;;;;;;;;MAUE;;;;;;;;;;YAWM;iBAEK;EACf,QAAQ;;iBAGO;EAEf,OAAO,SAAS,YAAY,QAAQ;EACpC,SAAS,SAAS,SAAS,UAAU,kBAAkB,QAAQ;EAC/D,SAAS,SAAS,SAAS,WAAW,kBAAkB,QAAQ;EAGhE,cAAc,SAAS,YAAY,QAAQ;EAC3C,cAAc,SAAS,YAAY,QAAQ;EAC3C,sBACE,SAAS,SACT,UAAU,+BACP,QAAQ;EACb,kBAAkB,SAAS,SAAS,cAAc,sBAAsB,QAAQ;EAChF,oBAAoB,SAAS,SAAS,cAAc,sBAAsB,QAAQ;EAClF,oBACE,SAAS,SACT,cAAc,mBACd,sBACG,QAAQ;EAGb,4BAA4B,SAAS,SAAS,QAAQ,gBAAgB,QAAQ;EAC9E,aAAa,SAAS,SAAS,cAAc,sBAAsB,QAAQ;;;;;;EAM3E,kBAAkB,eAAe,6BAA6B;EAG9D,mBAAmB,SAAS,YAAY,QAAQ;EAChD,kBAAkB,SAAS,YAAY,QAAQ;EAG/C,OAAO,WAAW,cAAc;EAChC,kBAAkB,SAAS,YAAY;EACvC,aAAa,mBACX,SAAS,SACT,QAAQ,MACL,QAAQ,iBAAiB,UAAU;EACxC,gBAAgB,sBAAsB;EACtC,eAAe,SAAS,YAAY,QAAQ;EAC5C,eAAe,WAAW,cAAc,QAAQ;EAChD,yBAAyB,0BAA0B,QAAQ"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../src/auth/types.ts"],"mappings":";;;;;YAMY,iBAAiB,WAAW,cAAc;;;;;;;;;;;;;;;iBAerC;EACf;EACA;EACA,UAAU;EACV,QAAQ;EACR,YAAY;;;;;;;YAQF,qBACV,SAAS,SACT,eAAe,kBACZ,QAAQ;iBAEI;;;;;;;;;;;EAWf,aAAa;;;;;;;;;;;EAWb,iBAAiB;;iBAGF;EACf;EACA,cAAc;EACd,mBAAmB;EACnB,SAAS;IAAkB;;EAC3B;;;;;;;;;IASE;;;;;;;;;;;IAWA;;EAEF;;EAEA;;;;;IAKE;;;;;IAKA;;EAEF;IACE,SAAS;IACT;;;;;;;;;;MAUE;;;;;;;;;;YAWM;iBAEK;EACf,QAAQ;;iBAGO;EAEf,OAAO,SAAS,YAAY,QAAQ;EACpC,SAAS,SAAS,SAAS,UAAU,kBAAkB,QAAQ;EAC/D,SAAS,SAAS,SAAS,WAAW,kBAAkB,QAAQ;EAGhE,cAAc,SAAS,YAAY,QAAQ;EAC3C,cAAc,SAAS,YAAY,QAAQ;EAC3C,sBACE,SAAS,SACT,UAAU,+BACP,QAAQ;EACb,kBAAkB,SAAS,SAAS,cAAc,sBAAsB,QAAQ;EAChF,oBAAoB,SAAS,SAAS,cAAc,sBAAsB,QAAQ;EAClF,oBACE,SAAS,SACT,cAAc,mBACd,sBACG,QAAQ;EAGb,4BAA4B,SAAS,SAAS,QAAQ,gBAAgB,QAAQ;EAC9E,aAAa,SAAS,SAAS,cAAc,sBAAsB,QAAQ;;;;;;EAM3E,kBAAkB,eAAe,6BAA6B;EAG9D,mBAAmB,SAAS,YAAY,QAAQ;EAChD,kBAAkB,SAAS,YAAY,QAAQ;EAG/C,OAAO,WAAW,cAAc;EAChC,kBAAkB,SAAS,YAAY;EACvC,aAAa,mBACX,SAAS,SACT,QAAQ,MACL,QAAQ,iBAAiB,UAAU;EACxC,gBAAgB,sBAAsB;EACtC,eAAe,SAAS,YAAY,QAAQ;EAC5C,eAAe,WAAW,cAAc,QAAQ;EAChD,yBAAyB,0BAA0B,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.cjs","names":[],"sources":["../../src/oauth2/types.ts"],"sourcesContent":["// reference: https://datatracker.ietf.org/doc/html/rfc7636\nexport interface PkceParameters {\n code_verifier: string;\n code_challenge: string;\n code_challenge_method: 'S256' | 'plain';\n}\n\nexport interface OAuth2AuthorizationRequest {\n state: string;\n /** Key of the provider in `OAuth2ClientConfig.providers` the flow was started for. */\n provider: string;\n authorizationRequestUri: string;\n additionalParameters: PkceParameters;\n /**\n * The `nonce` sent with the authorization request when the provider echoes one in its id token\n * (`Provider.nonce.web`); the callback refuses an id token that does not carry it back. Absent\n * for a provider without one, and on requests stored before this field existed.\n */\n nonce?: string;\n /**\n * What the host attached when the flow started (`oauth2Authorization` options), kept on the\n * server and handed back to `onAuthorized` once the callback checks out. Never sent to the\n * provider, unlike `additionalParameters`. Absent on requests stored before this field existed.\n */\n attributes?: Record<string, string>;\n}\n\ninterface CreateAuthorizationUriParams {\n state: string;\n clientId: string;\n redirectUri: string;\n scope: string[] | undefined;\n pkce?: Omit<PkceParameters, 'code_verifier'>;\n /** OpenID Connect `nonce`, sent when `Provider.nonce.web` is set. */\n nonce?: string;\n}\n\nexport interface ExchangeCodeParams {\n code: string;\n clientId: string;\n /** Absent for public clients (Google's installed-app clients, PKCE-only clients). */\n clientSecret?: string;\n redirectUri: string;\n pkce?: Omit<PkceParameters, 'code_challenge' | 'code_challenge_method'>;\n}\n\nexport interface RefreshTokenParams {\n clientId: string;\n clientSecret?: string;\n refreshToken: string;\n}\n\nexport interface RevokeTokenParams {\n token: string;\n clientId: string;\n clientSecret?: string;\n}\n\nexport interface LoginOAuth2NativeParams {\n /**\n * The configured client the app signs in as (its `client_id`, or the provider's default native\n * client). Id tokens must be issued to it: it is the only audience accepted.\n */\n clientId: string;\n clientSecret?: string;\n redirectUri: string;\n /**\n * The PKCE verifier for the code exchange, taken from `credentials.code_verifier`: the app made\n * the challenge and keeps the verifier, so the server only ever has the verifier. Providers\n * whose token endpoint takes `code_verifier` send it; the others ignore it.\n */\n pkce?: Omit<PkceParameters, 'code_challenge' | 'code_challenge_method'>;\n /** What the app posted, without `code_verifier`, which arrives as `pkce`. */\n credentials: NativeCredential;\n}\n\n// reference: https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\nexport interface OAuth2Token {\n /**\n * REQUIRED.\n * The access token issued by the authorization server.\n */\n access_token: string;\n\n /**\n * REQUIRED.\n * The type of the token issued as described in Section 7.1. Value is case insensitive.\n */\n token_type: 'Bearer' | 'bearer' | 'mac' | (string & {});\n\n /**\n * RECOMMENDED.\n * The lifetime in seconds of the access token. For example, the value \"3600\" denotes that the\n * access token will expire in one hour from the time the response was generated. If omitted, the\n * authorization server SHOULD provide the expiration time via other means or document the default\n * value.\n */\n expires_in?: number;\n\n /**\n * OPTIONAL.\n * The refresh token, which can be used to obtain new access tokens using the same authorization\n * grant as described in Section 6.\n */\n refresh_token?: string;\n\n /**\n * OPTIONAL.\n * If identical to the scope requested by the client; otherwise, REQUIRED. The scope of the\n * access token as described by Section 3.3.\n */\n scope?: string;\n\n id_token?: string;\n\n [key: string]: unknown;\n}\n\nexport interface OidcToken extends OAuth2Token {\n id_token: string;\n}\n\nexport type NativeCredential = {\n state?: string;\n code?: string;\n client_id?: string;\n redirect_uri?: string;\n /**\n * The PKCE verifier (RFC 7636) for `code`, when the app sent a `code_challenge` with the\n * authorization request: 43-128 characters from `[A-Za-z0-9-._~]`. Sent on to the provider's\n * token endpoint, which refuses the code unless the verifier matches its challenge, so a code\n * intercepted on the redirect (a custom-scheme redirect any app can claim) is useless alone.\n * RFC 8252 §8.1 requires it for native apps, whose installed-app clients have no secret.\n */\n code_verifier?: string;\n id_token?: string;\n access_token?: string;\n /**\n * The user's name as the platform hands it to the app, for providers whose id token carries none\n * (Apple sends it on the first authorization only): iOS\n * `ASAuthorizationAppleIDCredential.fullName`, expo-apple-authentication's `credential.fullName`.\n * The snake_case fields above are OAuth / OIDC protocol parameters; this one is not, it is the\n * platform's object passed through unchanged, so it keeps the platform's own name and apps send\n * `fullName: credential.fullName` as is. Unsigned client input: the provider cleans each part and\n * ignores anything that is not a string. Parts with a standard claim become `given_name`,\n * `family_name`, `middle_name` and `nickname`; `namePrefix` / `nameSuffix` have none and are kept\n * in `userInfo.data.fullName` only.\n */\n fullName?: PersonNameComponents;\n};\n\n/** iOS `PersonNameComponents` as JSON; every part may be missing or `null`. */\nexport interface PersonNameComponents {\n givenName?: string | null;\n familyName?: string | null;\n middleName?: string | null;\n namePrefix?: string | null;\n nameSuffix?: string | null;\n nickname?: string | null;\n}\n\n/**\n * Fields of the authorization response besides `code` and `state` that a provider uses to fill in\n * the user info: Apple posts the user's name as `user` (JSON) with the first authorization only,\n * since its id token never carries one. Unsigned, so treated as untrusted input.\n */\nexport interface AuthorizationResponse {\n user?: string;\n}\n\nexport const OIDC_SCOPES = [\n 'openid',\n 'profile',\n 'email',\n 'phone',\n 'address',\n 'offline_access',\n] as const;\n\nexport type OidcScope = (typeof OIDC_SCOPES)[number];\n\n// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims\nexport interface StandardClaims {\n sub: string;\n name?: string;\n given_name?: string;\n family_name?: string;\n middle_name?: string;\n nickname?: string;\n preferred_username?: string;\n profile?: string;\n picture?: string;\n website?: string;\n email?: string;\n /**\n * Set only by providers that state it (Google, Apple, GitHub from the primary address's\n * `verified` flag, X when it returns `confirmed_email`); Facebook leaves it unset. Trust the\n * address only when this is `true`.\n */\n email_verified?: boolean;\n gender?: 'female' | 'male' | (string & {});\n birthdate?: string; // ISO 8601-1 [ISO8601‑1] YYYY-MM-DD format.\n zoneinfo?: string; // IANA Time Zone Database [IANA.time‑zones]\n locale?: string; // BCP47 [RFC5646] language tag.\n phone_number?: string; // E.164\n phone_number_verified?: boolean;\n address?: {\n formatted: string;\n street_address: string;\n locality: string;\n region: string;\n postal_code: string;\n country: string;\n }; // [RFC8259] JSON object\n updated_at?: number;\n}\n\nexport interface UserInfo<T = unknown> {\n claims: StandardClaims;\n data: T;\n}\n\nexport interface Provider {\n /** Authorization URI for the provider. */\n authorizationUri: string;\n\n /** Token URI for the provider. */\n tokenUri: string;\n\n /** Token revoke URI for the provider. */\n tokenRevokeUri?: string;\n\n /** Token refresh URI for the provider. */\n tokenRefreshUri?: string;\n\n /** User info URI for the provider. */\n userInfoUri?: string;\n\n /** User info authentication method for the provider. */\n userInfoAuthenticationMethod?: string;\n\n /**\n * Name of the attribute that will be used to extract the username from the call to 'userInfoUri'.\n * */\n userNameAttribute?: string;\n\n /** JWK set URI for the provider */\n jwkSetUri?: string;\n\n /**\n * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0 Authorization\n * Server Metadata endpoint defined by RFC 8414.\n * */\n issuerUri?: string;\n\n /** Default scopes for the provider. */\n defaultScope: string[];\n\n /**\n * Whether the provider's id token carries back the `nonce` of the request it was issued for\n * (OpenID Connect), per flow. `web`: the redirect flow and One Tap, where `Auth` sends a nonce\n * with the authorization request (or the front end passes an issued one to One Tap) and refuses\n * an id token without it. `native`: the app's sign-in, where the app binds a nonce issued by\n * `GET /oauth2/nonce/:provider` into the platform's request and the login refuses an id token\n * without it. A flow set here is protected by default; a host relaxes it per provider with\n * `oauth2.nonce.optional`. Absent for a plain OAuth2 provider, which has no id token.\n */\n nonce?: { web?: boolean; native?: boolean };\n\n /** step 1: generate authorization uri and redirect user to it */\n createAuthorizationUri: (params: CreateAuthorizationUriParams) => URL;\n\n /** step 2: exchange code for token */\n exchangeAuthorizationCode: (params: ExchangeCodeParams) => Promise<OAuth2Token>;\n\n /**\n * step 3: get user info. `context.clientId` is the client the token was issued to, the only\n * audience an id token may carry; `context.response` holds what the authorization response\n * carried besides the code, for the providers that send user data there (Apple).\n */\n getUserInfo: (token: OAuth2Token, context: UserInfoContext) => Promise<UserInfo>;\n\n /** others: refresh access token */\n refreshAccessToken?: (params: RefreshTokenParams) => Promise<OAuth2Token>;\n\n /** others: revoke token */\n revokeToken?: (params: RevokeTokenParams) => Promise<void>;\n\n loginOAuth2Native?: (\n params: LoginOAuth2NativeParams\n ) => Promise<{ token: OAuth2Token; userInfo: UserInfo }>;\n}\n\nexport interface UserInfoContext {\n clientId: string;\n response?: AuthorizationResponse;\n}\n\nexport interface OneTapProvider extends Provider {\n /** Verifies a One Tap / FedCM credential, which must be issued to `clientId` (the web client). */\n getTokenInfo: (\n id_token: string,\n clientId: string\n ) => Promise<{ token: OAuth2Token; userInfo: UserInfo }>;\n}\n\n/**\n * A client secret: a fixed string, or a function minting one for the client id it is called with\n * (Sign in with Apple's signed JWT, see `createAppleClientSecret`), called on every use so it can\n * cache and renew on its own.\n */\nexport type ClientSecret = string | ((clientId: string) => string | Promise<string>);\n\n/** The client a provider's server-side flows sign in as: the redirect flow and Google One Tap. */\nexport interface WebClient {\n clientId: string;\n /** Absent for a public client. */\n clientSecret?: ClientSecret;\n /** Default: `${baseUri}/login/oauth2/code/<provider key>`. */\n redirectUri?: string;\n /** Authorization scopes. Default: the provider's `defaultScope`. */\n scope?: string[];\n}\n\n/**\n * A client an app signs in as on the device (an iOS bundle id, a Google iOS / Android client) and\n * whose code or id token it posts to the native login route.\n */\nexport interface NativeClient {\n clientId: string;\n /** Absent for a public client (Google's installed-app clients have none). */\n clientSecret?: ClientSecret;\n /**\n * Redirect URI sent with the code exchange when the app sends no `redirect_uri` of its own.\n * Default: `${baseUri}/login/oauth2/code/<provider key>`.\n */\n redirectUri?: string;\n}\n\n/**\n * One identity source: every user it signs in has a `sub` unique within it, whichever client they\n * came through. The key in `OAuth2ClientConfig.providers` names it in the routes and in\n * `onAuthorized`. Two keys may share an implementation (two Google Cloud projects, one Okta per\n * tenant) when their subjects are separate namespaces.\n */\nexport interface ProviderConfig {\n /** The implementation: `apple`, `google`, `github`, ... from `@shware/security/oauth2/provider`. */\n provider: Provider;\n /** The redirect flow's and One Tap's client. Required by those flows. */\n web?: WebClient;\n /**\n * Clients the native login accepts besides `web`. An app names its client with `client_id`;\n * without one, the only native client is used, or `web` when there is none.\n */\n native?: NativeClient[];\n}\n\nexport type ProvidersConfig = { [key: string]: ProviderConfig | undefined };\n\nexport interface OAuth2ClientConfig {\n baseUri: string;\n errorUri: string;\n successUri: string;\n /** Providers by key; a function is called per request, for secrets read at request time. */\n providers: ProvidersConfig | (() => Promise<ProvidersConfig>);\n}\n"],"mappings":";;AA0KA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;AACF"}
1
+ {"version":3,"file":"types.cjs","names":[],"sources":["../../src/oauth2/types.ts"],"sourcesContent":["// reference: https://datatracker.ietf.org/doc/html/rfc7636\nexport interface PkceParameters {\n code_verifier: string;\n code_challenge: string;\n code_challenge_method: 'S256' | 'plain';\n}\n\nexport interface OAuth2AuthorizationRequest {\n state: string;\n /** Key of the provider in `OAuth2ClientConfig.providers` the flow was started for. */\n provider: string;\n authorizationRequestUri: string;\n additionalParameters: PkceParameters;\n /**\n * The `nonce` sent with the authorization request when the provider echoes one in its id token\n * (`Provider.nonce.web`); the callback refuses an id token that does not carry it back. Absent\n * for a provider without one, and on requests stored before this field existed.\n */\n nonce?: string;\n /**\n * What the host attached when the flow started (`oauth2Authorization` options), kept on the\n * server and handed back to `onAuthorized` once the callback checks out. Never sent to the\n * provider, unlike `additionalParameters`. Absent on requests stored before this field existed.\n */\n attributes?: Record<string, string>;\n /** Added to the callback's redirect back to the app (`oauth2Authorization` options). */\n redirectParams?: Record<string, string>;\n}\n\ninterface CreateAuthorizationUriParams {\n state: string;\n clientId: string;\n redirectUri: string;\n scope: string[] | undefined;\n pkce?: Omit<PkceParameters, 'code_verifier'>;\n /** OpenID Connect `nonce`, sent when `Provider.nonce.web` is set. */\n nonce?: string;\n}\n\nexport interface ExchangeCodeParams {\n code: string;\n clientId: string;\n /** Absent for public clients (Google's installed-app clients, PKCE-only clients). */\n clientSecret?: string;\n redirectUri: string;\n pkce?: Omit<PkceParameters, 'code_challenge' | 'code_challenge_method'>;\n}\n\nexport interface RefreshTokenParams {\n clientId: string;\n clientSecret?: string;\n refreshToken: string;\n}\n\nexport interface RevokeTokenParams {\n token: string;\n clientId: string;\n clientSecret?: string;\n}\n\nexport interface LoginOAuth2NativeParams {\n /**\n * The configured client the app signs in as (its `client_id`, or the provider's default native\n * client). Id tokens must be issued to it: it is the only audience accepted.\n */\n clientId: string;\n clientSecret?: string;\n redirectUri: string;\n /**\n * The PKCE verifier for the code exchange, taken from `credentials.code_verifier`: the app made\n * the challenge and keeps the verifier, so the server only ever has the verifier. Providers\n * whose token endpoint takes `code_verifier` send it; the others ignore it.\n */\n pkce?: Omit<PkceParameters, 'code_challenge' | 'code_challenge_method'>;\n /** What the app posted, without `code_verifier`, which arrives as `pkce`. */\n credentials: NativeCredential;\n}\n\n// reference: https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\nexport interface OAuth2Token {\n /**\n * REQUIRED.\n * The access token issued by the authorization server.\n */\n access_token: string;\n\n /**\n * REQUIRED.\n * The type of the token issued as described in Section 7.1. Value is case insensitive.\n */\n token_type: 'Bearer' | 'bearer' | 'mac' | (string & {});\n\n /**\n * RECOMMENDED.\n * The lifetime in seconds of the access token. For example, the value \"3600\" denotes that the\n * access token will expire in one hour from the time the response was generated. If omitted, the\n * authorization server SHOULD provide the expiration time via other means or document the default\n * value.\n */\n expires_in?: number;\n\n /**\n * OPTIONAL.\n * The refresh token, which can be used to obtain new access tokens using the same authorization\n * grant as described in Section 6.\n */\n refresh_token?: string;\n\n /**\n * OPTIONAL.\n * If identical to the scope requested by the client; otherwise, REQUIRED. The scope of the\n * access token as described by Section 3.3.\n */\n scope?: string;\n\n id_token?: string;\n\n [key: string]: unknown;\n}\n\nexport interface OidcToken extends OAuth2Token {\n id_token: string;\n}\n\nexport type NativeCredential = {\n state?: string;\n code?: string;\n client_id?: string;\n redirect_uri?: string;\n /**\n * The PKCE verifier (RFC 7636) for `code`, when the app sent a `code_challenge` with the\n * authorization request: 43-128 characters from `[A-Za-z0-9-._~]`. Sent on to the provider's\n * token endpoint, which refuses the code unless the verifier matches its challenge, so a code\n * intercepted on the redirect (a custom-scheme redirect any app can claim) is useless alone.\n * RFC 8252 §8.1 requires it for native apps, whose installed-app clients have no secret.\n */\n code_verifier?: string;\n id_token?: string;\n access_token?: string;\n /**\n * The user's name as the platform hands it to the app, for providers whose id token carries none\n * (Apple sends it on the first authorization only): iOS\n * `ASAuthorizationAppleIDCredential.fullName`, expo-apple-authentication's `credential.fullName`.\n * The snake_case fields above are OAuth / OIDC protocol parameters; this one is not, it is the\n * platform's object passed through unchanged, so it keeps the platform's own name and apps send\n * `fullName: credential.fullName` as is. Unsigned client input: the provider cleans each part and\n * ignores anything that is not a string. Parts with a standard claim become `given_name`,\n * `family_name`, `middle_name` and `nickname`; `namePrefix` / `nameSuffix` have none and are kept\n * in `userInfo.data.fullName` only.\n */\n fullName?: PersonNameComponents;\n};\n\n/** iOS `PersonNameComponents` as JSON; every part may be missing or `null`. */\nexport interface PersonNameComponents {\n givenName?: string | null;\n familyName?: string | null;\n middleName?: string | null;\n namePrefix?: string | null;\n nameSuffix?: string | null;\n nickname?: string | null;\n}\n\n/**\n * Fields of the authorization response besides `code` and `state` that a provider uses to fill in\n * the user info: Apple posts the user's name as `user` (JSON) with the first authorization only,\n * since its id token never carries one. Unsigned, so treated as untrusted input.\n */\nexport interface AuthorizationResponse {\n user?: string;\n}\n\nexport const OIDC_SCOPES = [\n 'openid',\n 'profile',\n 'email',\n 'phone',\n 'address',\n 'offline_access',\n] as const;\n\nexport type OidcScope = (typeof OIDC_SCOPES)[number];\n\n// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims\nexport interface StandardClaims {\n sub: string;\n name?: string;\n given_name?: string;\n family_name?: string;\n middle_name?: string;\n nickname?: string;\n preferred_username?: string;\n profile?: string;\n picture?: string;\n website?: string;\n email?: string;\n /**\n * Set only by providers that state it (Google, Apple, GitHub from the primary address's\n * `verified` flag, X when it returns `confirmed_email`); Facebook leaves it unset. Trust the\n * address only when this is `true`.\n */\n email_verified?: boolean;\n gender?: 'female' | 'male' | (string & {});\n birthdate?: string; // ISO 8601-1 [ISO8601‑1] YYYY-MM-DD format.\n zoneinfo?: string; // IANA Time Zone Database [IANA.time‑zones]\n locale?: string; // BCP47 [RFC5646] language tag.\n phone_number?: string; // E.164\n phone_number_verified?: boolean;\n address?: {\n formatted: string;\n street_address: string;\n locality: string;\n region: string;\n postal_code: string;\n country: string;\n }; // [RFC8259] JSON object\n updated_at?: number;\n}\n\nexport interface UserInfo<T = unknown> {\n claims: StandardClaims;\n data: T;\n}\n\nexport interface Provider {\n /** Authorization URI for the provider. */\n authorizationUri: string;\n\n /** Token URI for the provider. */\n tokenUri: string;\n\n /** Token revoke URI for the provider. */\n tokenRevokeUri?: string;\n\n /** Token refresh URI for the provider. */\n tokenRefreshUri?: string;\n\n /** User info URI for the provider. */\n userInfoUri?: string;\n\n /** User info authentication method for the provider. */\n userInfoAuthenticationMethod?: string;\n\n /**\n * Name of the attribute that will be used to extract the username from the call to 'userInfoUri'.\n * */\n userNameAttribute?: string;\n\n /** JWK set URI for the provider */\n jwkSetUri?: string;\n\n /**\n * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0 Authorization\n * Server Metadata endpoint defined by RFC 8414.\n * */\n issuerUri?: string;\n\n /** Default scopes for the provider. */\n defaultScope: string[];\n\n /**\n * Whether the provider's id token carries back the `nonce` of the request it was issued for\n * (OpenID Connect), per flow. `web`: the redirect flow and One Tap, where `Auth` sends a nonce\n * with the authorization request (or the front end passes an issued one to One Tap) and refuses\n * an id token without it. `native`: the app's sign-in, where the app binds a nonce issued by\n * `GET /oauth2/nonce/:provider` into the platform's request and the login refuses an id token\n * without it. A flow set here is protected by default; a host relaxes it per provider with\n * `oauth2.nonce.optional`. Absent for a plain OAuth2 provider, which has no id token.\n */\n nonce?: { web?: boolean; native?: boolean };\n\n /** step 1: generate authorization uri and redirect user to it */\n createAuthorizationUri: (params: CreateAuthorizationUriParams) => URL;\n\n /** step 2: exchange code for token */\n exchangeAuthorizationCode: (params: ExchangeCodeParams) => Promise<OAuth2Token>;\n\n /**\n * step 3: get user info. `context.clientId` is the client the token was issued to, the only\n * audience an id token may carry; `context.response` holds what the authorization response\n * carried besides the code, for the providers that send user data there (Apple).\n */\n getUserInfo: (token: OAuth2Token, context: UserInfoContext) => Promise<UserInfo>;\n\n /** others: refresh access token */\n refreshAccessToken?: (params: RefreshTokenParams) => Promise<OAuth2Token>;\n\n /** others: revoke token */\n revokeToken?: (params: RevokeTokenParams) => Promise<void>;\n\n loginOAuth2Native?: (\n params: LoginOAuth2NativeParams\n ) => Promise<{ token: OAuth2Token; userInfo: UserInfo }>;\n}\n\nexport interface UserInfoContext {\n clientId: string;\n response?: AuthorizationResponse;\n}\n\nexport interface OneTapProvider extends Provider {\n /** Verifies a One Tap / FedCM credential, which must be issued to `clientId` (the web client). */\n getTokenInfo: (\n id_token: string,\n clientId: string\n ) => Promise<{ token: OAuth2Token; userInfo: UserInfo }>;\n}\n\n/**\n * A client secret: a fixed string, or a function minting one for the client id it is called with\n * (Sign in with Apple's signed JWT, see `createAppleClientSecret`), called on every use so it can\n * cache and renew on its own.\n */\nexport type ClientSecret = string | ((clientId: string) => string | Promise<string>);\n\n/** The client a provider's server-side flows sign in as: the redirect flow and Google One Tap. */\nexport interface WebClient {\n clientId: string;\n /** Absent for a public client. */\n clientSecret?: ClientSecret;\n /** Default: `${baseUri}/login/oauth2/code/<provider key>`. */\n redirectUri?: string;\n /** Authorization scopes. Default: the provider's `defaultScope`. */\n scope?: string[];\n}\n\n/**\n * A client an app signs in as on the device (an iOS bundle id, a Google iOS / Android client) and\n * whose code or id token it posts to the native login route.\n */\nexport interface NativeClient {\n clientId: string;\n /** Absent for a public client (Google's installed-app clients have none). */\n clientSecret?: ClientSecret;\n /**\n * Redirect URI sent with the code exchange when the app sends no `redirect_uri` of its own.\n * Default: `${baseUri}/login/oauth2/code/<provider key>`.\n */\n redirectUri?: string;\n}\n\n/**\n * One identity source: every user it signs in has a `sub` unique within it, whichever client they\n * came through. The key in `OAuth2ClientConfig.providers` names it in the routes and in\n * `onAuthorized`. Two keys may share an implementation (two Google Cloud projects, one Okta per\n * tenant) when their subjects are separate namespaces.\n */\nexport interface ProviderConfig {\n /** The implementation: `apple`, `google`, `github`, ... from `@shware/security/oauth2/provider`. */\n provider: Provider;\n /** The redirect flow's and One Tap's client. Required by those flows. */\n web?: WebClient;\n /**\n * Clients the native login accepts besides `web`. An app names its client with `client_id`;\n * without one, the only native client is used, or `web` when there is none.\n */\n native?: NativeClient[];\n}\n\nexport type ProvidersConfig = { [key: string]: ProviderConfig | undefined };\n\nexport interface OAuth2ClientConfig {\n baseUri: string;\n errorUri: string;\n successUri: string;\n /** Providers by key; a function is called per request, for secrets read at request time. */\n providers: ProvidersConfig | (() => Promise<ProvidersConfig>);\n}\n"],"mappings":";;AA4KA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;AACF"}
@@ -21,6 +21,8 @@ export interface OAuth2AuthorizationRequest {
21
21
  * provider, unlike `additionalParameters`. Absent on requests stored before this field existed.
22
22
  */
23
23
  attributes?: Record<string, string>;
24
+ /** Added to the callback's redirect back to the app (`oauth2Authorization` options). */
25
+ redirectParams?: Record<string, string>;
24
26
  }
25
27
  interface CreateAuthorizationUriParams {
26
28
  state: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.cts","names":[],"sources":["../../src/oauth2/types.ts"],"mappings":"A,iBACiB;EACf;EACA;EACA;;iBAGe;EACf;;EAEA;EACA;EACA,sBAAsB;;;;;;EAMtB;;;;;;EAMA,aAAa;;UAGL;EACR;EACA;EACA;EACA;EACA,OAAO,KAAK;;EAEZ;;iBAGe;EACf;EACA;;EAEA;EACA;EACA,OAAO,KAAK;;iBAGG;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA;;iBAGe;;;;;EAKf;EACA;EACA;;;;;;EAMA,OAAO,KAAK;;EAEZ,aAAa;;iBAIE;;;;;EAKf;;;;;EAMA;;;;;;;;EASA;;;;;;EAOA;;;;;;EAOA;EAEA;GAEC;;iBAGc,kBAAkB;EACjC;;YAGU;EACV;EACA;EACA;EACA;;;;;;;;EAQA;EACA;EACA;;;;;;;;;;;;EAYA,WAAW;;;iBAII;EACf;EACA;EACA;EACA;EACA;EACA;;;;;;;iBAQe;EACf;;qBAGW;YASD,oBAAoB;iBAGf;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;EAMA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;IACE;IACA;IACA;IACA;IACA;IACA;;EAEF;;iBAGe,SAAS;EACxB,QAAQ;EACR,MAAM;;iBAGS;;EAEf;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;;;EAKA;;EAGA;;;;;EAMA;;EAGA;;;;;;;;;;EAWA;IAAU;IAAe;;;EAGzB,yBAAyB,QAAQ,iCAAiC;;EAGlE,4BAA4B,QAAQ,uBAAuB,QAAQ;;;;;;EAOnE,cAAc,OAAO,aAAa,SAAS,oBAAoB,QAAQ;;EAGvE,sBAAsB,QAAQ,uBAAuB,QAAQ;;EAG7D,eAAe,QAAQ,sBAAsB;EAE7C,qBACE,QAAQ,4BACL;IAAU,OAAO;IAAa,UAAU;;;iBAG9B;EACf;EACA,WAAW;;iBAGI,uBAAuB;;EAEtC,eACE,kBACA,qBACG;IAAU,OAAO;IAAa,UAAU;;;;;;;;YAQnC,0BAA0B,8BAA8B;;iBAGnD;EACf;;EAEA,eAAe;;EAEf;;EAEA;;;;;;iBAOe;EACf;;EAEA,eAAe;;;;;EAKf;;;;;;;;iBASe;;EAEf,UAAU;;EAEV,MAAM;;;;;EAKN,SAAS;;YAGC;GAAqB,cAAc;;iBAE9B;EACf;EACA;EACA;;EAEA,WAAW,yBAAyB,QAAQ"}
1
+ {"version":3,"file":"types.d.cts","names":[],"sources":["../../src/oauth2/types.ts"],"mappings":"A,iBACiB;EACf;EACA;EACA;;iBAGe;EACf;;EAEA;EACA;EACA,sBAAsB;;;;;;EAMtB;;;;;;EAMA,aAAa;;EAEb,iBAAiB;;UAGT;EACR;EACA;EACA;EACA;EACA,OAAO,KAAK;;EAEZ;;iBAGe;EACf;EACA;;EAEA;EACA;EACA,OAAO,KAAK;;iBAGG;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA;;iBAGe;;;;;EAKf;EACA;EACA;;;;;;EAMA,OAAO,KAAK;;EAEZ,aAAa;;iBAIE;;;;;EAKf;;;;;EAMA;;;;;;;;EASA;;;;;;EAOA;;;;;;EAOA;EAEA;GAEC;;iBAGc,kBAAkB;EACjC;;YAGU;EACV;EACA;EACA;EACA;;;;;;;;EAQA;EACA;EACA;;;;;;;;;;;;EAYA,WAAW;;;iBAII;EACf;EACA;EACA;EACA;EACA;EACA;;;;;;;iBAQe;EACf;;qBAGW;YASD,oBAAoB;iBAGf;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;EAMA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;IACE;IACA;IACA;IACA;IACA;IACA;;EAEF;;iBAGe,SAAS;EACxB,QAAQ;EACR,MAAM;;iBAGS;;EAEf;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;;;EAKA;;EAGA;;;;;EAMA;;EAGA;;;;;;;;;;EAWA;IAAU;IAAe;;;EAGzB,yBAAyB,QAAQ,iCAAiC;;EAGlE,4BAA4B,QAAQ,uBAAuB,QAAQ;;;;;;EAOnE,cAAc,OAAO,aAAa,SAAS,oBAAoB,QAAQ;;EAGvE,sBAAsB,QAAQ,uBAAuB,QAAQ;;EAG7D,eAAe,QAAQ,sBAAsB;EAE7C,qBACE,QAAQ,4BACL;IAAU,OAAO;IAAa,UAAU;;;iBAG9B;EACf;EACA,WAAW;;iBAGI,uBAAuB;;EAEtC,eACE,kBACA,qBACG;IAAU,OAAO;IAAa,UAAU;;;;;;;;YAQnC,0BAA0B,8BAA8B;;iBAGnD;EACf;;EAEA,eAAe;;EAEf;;EAEA;;;;;;iBAOe;EACf;;EAEA,eAAe;;;;;EAKf;;;;;;;;iBASe;;EAEf,UAAU;;EAEV,MAAM;;;;;EAKN,SAAS;;YAGC;GAAqB,cAAc;;iBAE9B;EACf;EACA;EACA;;EAEA,WAAW,yBAAyB,QAAQ"}
@@ -21,6 +21,8 @@ export interface OAuth2AuthorizationRequest {
21
21
  * provider, unlike `additionalParameters`. Absent on requests stored before this field existed.
22
22
  */
23
23
  attributes?: Record<string, string>;
24
+ /** Added to the callback's redirect back to the app (`oauth2Authorization` options). */
25
+ redirectParams?: Record<string, string>;
24
26
  }
25
27
  interface CreateAuthorizationUriParams {
26
28
  state: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../src/oauth2/types.ts"],"mappings":"A,iBACiB;EACf;EACA;EACA;;iBAGe;EACf;;EAEA;EACA;EACA,sBAAsB;;;;;;EAMtB;;;;;;EAMA,aAAa;;UAGL;EACR;EACA;EACA;EACA;EACA,OAAO,KAAK;;EAEZ;;iBAGe;EACf;EACA;;EAEA;EACA;EACA,OAAO,KAAK;;iBAGG;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA;;iBAGe;;;;;EAKf;EACA;EACA;;;;;;EAMA,OAAO,KAAK;;EAEZ,aAAa;;iBAIE;;;;;EAKf;;;;;EAMA;;;;;;;;EASA;;;;;;EAOA;;;;;;EAOA;EAEA;GAEC;;iBAGc,kBAAkB;EACjC;;YAGU;EACV;EACA;EACA;EACA;;;;;;;;EAQA;EACA;EACA;;;;;;;;;;;;EAYA,WAAW;;;iBAII;EACf;EACA;EACA;EACA;EACA;EACA;;;;;;;iBAQe;EACf;;qBAGW;YASD,oBAAoB;iBAGf;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;EAMA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;IACE;IACA;IACA;IACA;IACA;IACA;;EAEF;;iBAGe,SAAS;EACxB,QAAQ;EACR,MAAM;;iBAGS;;EAEf;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;;;EAKA;;EAGA;;;;;EAMA;;EAGA;;;;;;;;;;EAWA;IAAU;IAAe;;;EAGzB,yBAAyB,QAAQ,iCAAiC;;EAGlE,4BAA4B,QAAQ,uBAAuB,QAAQ;;;;;;EAOnE,cAAc,OAAO,aAAa,SAAS,oBAAoB,QAAQ;;EAGvE,sBAAsB,QAAQ,uBAAuB,QAAQ;;EAG7D,eAAe,QAAQ,sBAAsB;EAE7C,qBACE,QAAQ,4BACL;IAAU,OAAO;IAAa,UAAU;;;iBAG9B;EACf;EACA,WAAW;;iBAGI,uBAAuB;;EAEtC,eACE,kBACA,qBACG;IAAU,OAAO;IAAa,UAAU;;;;;;;;YAQnC,0BAA0B,8BAA8B;;iBAGnD;EACf;;EAEA,eAAe;;EAEf;;EAEA;;;;;;iBAOe;EACf;;EAEA,eAAe;;;;;EAKf;;;;;;;;iBASe;;EAEf,UAAU;;EAEV,MAAM;;;;;EAKN,SAAS;;YAGC;GAAqB,cAAc;;iBAE9B;EACf;EACA;EACA;;EAEA,WAAW,yBAAyB,QAAQ"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../src/oauth2/types.ts"],"mappings":"A,iBACiB;EACf;EACA;EACA;;iBAGe;EACf;;EAEA;EACA;EACA,sBAAsB;;;;;;EAMtB;;;;;;EAMA,aAAa;;EAEb,iBAAiB;;UAGT;EACR;EACA;EACA;EACA;EACA,OAAO,KAAK;;EAEZ;;iBAGe;EACf;EACA;;EAEA;EACA;EACA,OAAO,KAAK;;iBAGG;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA;;iBAGe;;;;;EAKf;EACA;EACA;;;;;;EAMA,OAAO,KAAK;;EAEZ,aAAa;;iBAIE;;;;;EAKf;;;;;EAMA;;;;;;;;EASA;;;;;;EAOA;;;;;;EAOA;EAEA;GAEC;;iBAGc,kBAAkB;EACjC;;YAGU;EACV;EACA;EACA;EACA;;;;;;;;EAQA;EACA;EACA;;;;;;;;;;;;EAYA,WAAW;;;iBAII;EACf;EACA;EACA;EACA;EACA;EACA;;;;;;;iBAQe;EACf;;qBAGW;YASD,oBAAoB;iBAGf;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;;;;EAMA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;IACE;IACA;IACA;IACA;IACA;IACA;;EAEF;;iBAGe,SAAS;EACxB,QAAQ;EACR,MAAM;;iBAGS;;EAEf;;EAGA;;EAGA;;EAGA;;EAGA;;EAGA;;;;EAKA;;EAGA;;;;;EAMA;;EAGA;;;;;;;;;;EAWA;IAAU;IAAe;;;EAGzB,yBAAyB,QAAQ,iCAAiC;;EAGlE,4BAA4B,QAAQ,uBAAuB,QAAQ;;;;;;EAOnE,cAAc,OAAO,aAAa,SAAS,oBAAoB,QAAQ;;EAGvE,sBAAsB,QAAQ,uBAAuB,QAAQ;;EAG7D,eAAe,QAAQ,sBAAsB;EAE7C,qBACE,QAAQ,4BACL;IAAU,OAAO;IAAa,UAAU;;;iBAG9B;EACf;EACA,WAAW;;iBAGI,uBAAuB;;EAEtC,eACE,kBACA,qBACG;IAAU,OAAO;IAAa,UAAU;;;;;;;;YAQnC,0BAA0B,8BAA8B;;iBAGnD;EACf;;EAEA,eAAe;;EAEf;;EAEA;;;;;;iBAOe;EACf;;EAEA,eAAe;;;;;EAKf;;;;;;;;iBASe;;EAEf,UAAU;;EAEV,MAAM;;;;;EAKN,SAAS;;YAGC;GAAqB,cAAc;;iBAE9B;EACf;EACA;EACA;;EAEA,WAAW,yBAAyB,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../../src/oauth2/types.ts"],"sourcesContent":["// reference: https://datatracker.ietf.org/doc/html/rfc7636\nexport interface PkceParameters {\n code_verifier: string;\n code_challenge: string;\n code_challenge_method: 'S256' | 'plain';\n}\n\nexport interface OAuth2AuthorizationRequest {\n state: string;\n /** Key of the provider in `OAuth2ClientConfig.providers` the flow was started for. */\n provider: string;\n authorizationRequestUri: string;\n additionalParameters: PkceParameters;\n /**\n * The `nonce` sent with the authorization request when the provider echoes one in its id token\n * (`Provider.nonce.web`); the callback refuses an id token that does not carry it back. Absent\n * for a provider without one, and on requests stored before this field existed.\n */\n nonce?: string;\n /**\n * What the host attached when the flow started (`oauth2Authorization` options), kept on the\n * server and handed back to `onAuthorized` once the callback checks out. Never sent to the\n * provider, unlike `additionalParameters`. Absent on requests stored before this field existed.\n */\n attributes?: Record<string, string>;\n}\n\ninterface CreateAuthorizationUriParams {\n state: string;\n clientId: string;\n redirectUri: string;\n scope: string[] | undefined;\n pkce?: Omit<PkceParameters, 'code_verifier'>;\n /** OpenID Connect `nonce`, sent when `Provider.nonce.web` is set. */\n nonce?: string;\n}\n\nexport interface ExchangeCodeParams {\n code: string;\n clientId: string;\n /** Absent for public clients (Google's installed-app clients, PKCE-only clients). */\n clientSecret?: string;\n redirectUri: string;\n pkce?: Omit<PkceParameters, 'code_challenge' | 'code_challenge_method'>;\n}\n\nexport interface RefreshTokenParams {\n clientId: string;\n clientSecret?: string;\n refreshToken: string;\n}\n\nexport interface RevokeTokenParams {\n token: string;\n clientId: string;\n clientSecret?: string;\n}\n\nexport interface LoginOAuth2NativeParams {\n /**\n * The configured client the app signs in as (its `client_id`, or the provider's default native\n * client). Id tokens must be issued to it: it is the only audience accepted.\n */\n clientId: string;\n clientSecret?: string;\n redirectUri: string;\n /**\n * The PKCE verifier for the code exchange, taken from `credentials.code_verifier`: the app made\n * the challenge and keeps the verifier, so the server only ever has the verifier. Providers\n * whose token endpoint takes `code_verifier` send it; the others ignore it.\n */\n pkce?: Omit<PkceParameters, 'code_challenge' | 'code_challenge_method'>;\n /** What the app posted, without `code_verifier`, which arrives as `pkce`. */\n credentials: NativeCredential;\n}\n\n// reference: https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\nexport interface OAuth2Token {\n /**\n * REQUIRED.\n * The access token issued by the authorization server.\n */\n access_token: string;\n\n /**\n * REQUIRED.\n * The type of the token issued as described in Section 7.1. Value is case insensitive.\n */\n token_type: 'Bearer' | 'bearer' | 'mac' | (string & {});\n\n /**\n * RECOMMENDED.\n * The lifetime in seconds of the access token. For example, the value \"3600\" denotes that the\n * access token will expire in one hour from the time the response was generated. If omitted, the\n * authorization server SHOULD provide the expiration time via other means or document the default\n * value.\n */\n expires_in?: number;\n\n /**\n * OPTIONAL.\n * The refresh token, which can be used to obtain new access tokens using the same authorization\n * grant as described in Section 6.\n */\n refresh_token?: string;\n\n /**\n * OPTIONAL.\n * If identical to the scope requested by the client; otherwise, REQUIRED. The scope of the\n * access token as described by Section 3.3.\n */\n scope?: string;\n\n id_token?: string;\n\n [key: string]: unknown;\n}\n\nexport interface OidcToken extends OAuth2Token {\n id_token: string;\n}\n\nexport type NativeCredential = {\n state?: string;\n code?: string;\n client_id?: string;\n redirect_uri?: string;\n /**\n * The PKCE verifier (RFC 7636) for `code`, when the app sent a `code_challenge` with the\n * authorization request: 43-128 characters from `[A-Za-z0-9-._~]`. Sent on to the provider's\n * token endpoint, which refuses the code unless the verifier matches its challenge, so a code\n * intercepted on the redirect (a custom-scheme redirect any app can claim) is useless alone.\n * RFC 8252 §8.1 requires it for native apps, whose installed-app clients have no secret.\n */\n code_verifier?: string;\n id_token?: string;\n access_token?: string;\n /**\n * The user's name as the platform hands it to the app, for providers whose id token carries none\n * (Apple sends it on the first authorization only): iOS\n * `ASAuthorizationAppleIDCredential.fullName`, expo-apple-authentication's `credential.fullName`.\n * The snake_case fields above are OAuth / OIDC protocol parameters; this one is not, it is the\n * platform's object passed through unchanged, so it keeps the platform's own name and apps send\n * `fullName: credential.fullName` as is. Unsigned client input: the provider cleans each part and\n * ignores anything that is not a string. Parts with a standard claim become `given_name`,\n * `family_name`, `middle_name` and `nickname`; `namePrefix` / `nameSuffix` have none and are kept\n * in `userInfo.data.fullName` only.\n */\n fullName?: PersonNameComponents;\n};\n\n/** iOS `PersonNameComponents` as JSON; every part may be missing or `null`. */\nexport interface PersonNameComponents {\n givenName?: string | null;\n familyName?: string | null;\n middleName?: string | null;\n namePrefix?: string | null;\n nameSuffix?: string | null;\n nickname?: string | null;\n}\n\n/**\n * Fields of the authorization response besides `code` and `state` that a provider uses to fill in\n * the user info: Apple posts the user's name as `user` (JSON) with the first authorization only,\n * since its id token never carries one. Unsigned, so treated as untrusted input.\n */\nexport interface AuthorizationResponse {\n user?: string;\n}\n\nexport const OIDC_SCOPES = [\n 'openid',\n 'profile',\n 'email',\n 'phone',\n 'address',\n 'offline_access',\n] as const;\n\nexport type OidcScope = (typeof OIDC_SCOPES)[number];\n\n// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims\nexport interface StandardClaims {\n sub: string;\n name?: string;\n given_name?: string;\n family_name?: string;\n middle_name?: string;\n nickname?: string;\n preferred_username?: string;\n profile?: string;\n picture?: string;\n website?: string;\n email?: string;\n /**\n * Set only by providers that state it (Google, Apple, GitHub from the primary address's\n * `verified` flag, X when it returns `confirmed_email`); Facebook leaves it unset. Trust the\n * address only when this is `true`.\n */\n email_verified?: boolean;\n gender?: 'female' | 'male' | (string & {});\n birthdate?: string; // ISO 8601-1 [ISO8601‑1] YYYY-MM-DD format.\n zoneinfo?: string; // IANA Time Zone Database [IANA.time‑zones]\n locale?: string; // BCP47 [RFC5646] language tag.\n phone_number?: string; // E.164\n phone_number_verified?: boolean;\n address?: {\n formatted: string;\n street_address: string;\n locality: string;\n region: string;\n postal_code: string;\n country: string;\n }; // [RFC8259] JSON object\n updated_at?: number;\n}\n\nexport interface UserInfo<T = unknown> {\n claims: StandardClaims;\n data: T;\n}\n\nexport interface Provider {\n /** Authorization URI for the provider. */\n authorizationUri: string;\n\n /** Token URI for the provider. */\n tokenUri: string;\n\n /** Token revoke URI for the provider. */\n tokenRevokeUri?: string;\n\n /** Token refresh URI for the provider. */\n tokenRefreshUri?: string;\n\n /** User info URI for the provider. */\n userInfoUri?: string;\n\n /** User info authentication method for the provider. */\n userInfoAuthenticationMethod?: string;\n\n /**\n * Name of the attribute that will be used to extract the username from the call to 'userInfoUri'.\n * */\n userNameAttribute?: string;\n\n /** JWK set URI for the provider */\n jwkSetUri?: string;\n\n /**\n * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0 Authorization\n * Server Metadata endpoint defined by RFC 8414.\n * */\n issuerUri?: string;\n\n /** Default scopes for the provider. */\n defaultScope: string[];\n\n /**\n * Whether the provider's id token carries back the `nonce` of the request it was issued for\n * (OpenID Connect), per flow. `web`: the redirect flow and One Tap, where `Auth` sends a nonce\n * with the authorization request (or the front end passes an issued one to One Tap) and refuses\n * an id token without it. `native`: the app's sign-in, where the app binds a nonce issued by\n * `GET /oauth2/nonce/:provider` into the platform's request and the login refuses an id token\n * without it. A flow set here is protected by default; a host relaxes it per provider with\n * `oauth2.nonce.optional`. Absent for a plain OAuth2 provider, which has no id token.\n */\n nonce?: { web?: boolean; native?: boolean };\n\n /** step 1: generate authorization uri and redirect user to it */\n createAuthorizationUri: (params: CreateAuthorizationUriParams) => URL;\n\n /** step 2: exchange code for token */\n exchangeAuthorizationCode: (params: ExchangeCodeParams) => Promise<OAuth2Token>;\n\n /**\n * step 3: get user info. `context.clientId` is the client the token was issued to, the only\n * audience an id token may carry; `context.response` holds what the authorization response\n * carried besides the code, for the providers that send user data there (Apple).\n */\n getUserInfo: (token: OAuth2Token, context: UserInfoContext) => Promise<UserInfo>;\n\n /** others: refresh access token */\n refreshAccessToken?: (params: RefreshTokenParams) => Promise<OAuth2Token>;\n\n /** others: revoke token */\n revokeToken?: (params: RevokeTokenParams) => Promise<void>;\n\n loginOAuth2Native?: (\n params: LoginOAuth2NativeParams\n ) => Promise<{ token: OAuth2Token; userInfo: UserInfo }>;\n}\n\nexport interface UserInfoContext {\n clientId: string;\n response?: AuthorizationResponse;\n}\n\nexport interface OneTapProvider extends Provider {\n /** Verifies a One Tap / FedCM credential, which must be issued to `clientId` (the web client). */\n getTokenInfo: (\n id_token: string,\n clientId: string\n ) => Promise<{ token: OAuth2Token; userInfo: UserInfo }>;\n}\n\n/**\n * A client secret: a fixed string, or a function minting one for the client id it is called with\n * (Sign in with Apple's signed JWT, see `createAppleClientSecret`), called on every use so it can\n * cache and renew on its own.\n */\nexport type ClientSecret = string | ((clientId: string) => string | Promise<string>);\n\n/** The client a provider's server-side flows sign in as: the redirect flow and Google One Tap. */\nexport interface WebClient {\n clientId: string;\n /** Absent for a public client. */\n clientSecret?: ClientSecret;\n /** Default: `${baseUri}/login/oauth2/code/<provider key>`. */\n redirectUri?: string;\n /** Authorization scopes. Default: the provider's `defaultScope`. */\n scope?: string[];\n}\n\n/**\n * A client an app signs in as on the device (an iOS bundle id, a Google iOS / Android client) and\n * whose code or id token it posts to the native login route.\n */\nexport interface NativeClient {\n clientId: string;\n /** Absent for a public client (Google's installed-app clients have none). */\n clientSecret?: ClientSecret;\n /**\n * Redirect URI sent with the code exchange when the app sends no `redirect_uri` of its own.\n * Default: `${baseUri}/login/oauth2/code/<provider key>`.\n */\n redirectUri?: string;\n}\n\n/**\n * One identity source: every user it signs in has a `sub` unique within it, whichever client they\n * came through. The key in `OAuth2ClientConfig.providers` names it in the routes and in\n * `onAuthorized`. Two keys may share an implementation (two Google Cloud projects, one Okta per\n * tenant) when their subjects are separate namespaces.\n */\nexport interface ProviderConfig {\n /** The implementation: `apple`, `google`, `github`, ... from `@shware/security/oauth2/provider`. */\n provider: Provider;\n /** The redirect flow's and One Tap's client. Required by those flows. */\n web?: WebClient;\n /**\n * Clients the native login accepts besides `web`. An app names its client with `client_id`;\n * without one, the only native client is used, or `web` when there is none.\n */\n native?: NativeClient[];\n}\n\nexport type ProvidersConfig = { [key: string]: ProviderConfig | undefined };\n\nexport interface OAuth2ClientConfig {\n baseUri: string;\n errorUri: string;\n successUri: string;\n /** Providers by key; a function is called per request, for secrets read at request time. */\n providers: ProvidersConfig | (() => Promise<ProvidersConfig>);\n}\n"],"mappings":";AA0KA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;AACF"}
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../../src/oauth2/types.ts"],"sourcesContent":["// reference: https://datatracker.ietf.org/doc/html/rfc7636\nexport interface PkceParameters {\n code_verifier: string;\n code_challenge: string;\n code_challenge_method: 'S256' | 'plain';\n}\n\nexport interface OAuth2AuthorizationRequest {\n state: string;\n /** Key of the provider in `OAuth2ClientConfig.providers` the flow was started for. */\n provider: string;\n authorizationRequestUri: string;\n additionalParameters: PkceParameters;\n /**\n * The `nonce` sent with the authorization request when the provider echoes one in its id token\n * (`Provider.nonce.web`); the callback refuses an id token that does not carry it back. Absent\n * for a provider without one, and on requests stored before this field existed.\n */\n nonce?: string;\n /**\n * What the host attached when the flow started (`oauth2Authorization` options), kept on the\n * server and handed back to `onAuthorized` once the callback checks out. Never sent to the\n * provider, unlike `additionalParameters`. Absent on requests stored before this field existed.\n */\n attributes?: Record<string, string>;\n /** Added to the callback's redirect back to the app (`oauth2Authorization` options). */\n redirectParams?: Record<string, string>;\n}\n\ninterface CreateAuthorizationUriParams {\n state: string;\n clientId: string;\n redirectUri: string;\n scope: string[] | undefined;\n pkce?: Omit<PkceParameters, 'code_verifier'>;\n /** OpenID Connect `nonce`, sent when `Provider.nonce.web` is set. */\n nonce?: string;\n}\n\nexport interface ExchangeCodeParams {\n code: string;\n clientId: string;\n /** Absent for public clients (Google's installed-app clients, PKCE-only clients). */\n clientSecret?: string;\n redirectUri: string;\n pkce?: Omit<PkceParameters, 'code_challenge' | 'code_challenge_method'>;\n}\n\nexport interface RefreshTokenParams {\n clientId: string;\n clientSecret?: string;\n refreshToken: string;\n}\n\nexport interface RevokeTokenParams {\n token: string;\n clientId: string;\n clientSecret?: string;\n}\n\nexport interface LoginOAuth2NativeParams {\n /**\n * The configured client the app signs in as (its `client_id`, or the provider's default native\n * client). Id tokens must be issued to it: it is the only audience accepted.\n */\n clientId: string;\n clientSecret?: string;\n redirectUri: string;\n /**\n * The PKCE verifier for the code exchange, taken from `credentials.code_verifier`: the app made\n * the challenge and keeps the verifier, so the server only ever has the verifier. Providers\n * whose token endpoint takes `code_verifier` send it; the others ignore it.\n */\n pkce?: Omit<PkceParameters, 'code_challenge' | 'code_challenge_method'>;\n /** What the app posted, without `code_verifier`, which arrives as `pkce`. */\n credentials: NativeCredential;\n}\n\n// reference: https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\nexport interface OAuth2Token {\n /**\n * REQUIRED.\n * The access token issued by the authorization server.\n */\n access_token: string;\n\n /**\n * REQUIRED.\n * The type of the token issued as described in Section 7.1. Value is case insensitive.\n */\n token_type: 'Bearer' | 'bearer' | 'mac' | (string & {});\n\n /**\n * RECOMMENDED.\n * The lifetime in seconds of the access token. For example, the value \"3600\" denotes that the\n * access token will expire in one hour from the time the response was generated. If omitted, the\n * authorization server SHOULD provide the expiration time via other means or document the default\n * value.\n */\n expires_in?: number;\n\n /**\n * OPTIONAL.\n * The refresh token, which can be used to obtain new access tokens using the same authorization\n * grant as described in Section 6.\n */\n refresh_token?: string;\n\n /**\n * OPTIONAL.\n * If identical to the scope requested by the client; otherwise, REQUIRED. The scope of the\n * access token as described by Section 3.3.\n */\n scope?: string;\n\n id_token?: string;\n\n [key: string]: unknown;\n}\n\nexport interface OidcToken extends OAuth2Token {\n id_token: string;\n}\n\nexport type NativeCredential = {\n state?: string;\n code?: string;\n client_id?: string;\n redirect_uri?: string;\n /**\n * The PKCE verifier (RFC 7636) for `code`, when the app sent a `code_challenge` with the\n * authorization request: 43-128 characters from `[A-Za-z0-9-._~]`. Sent on to the provider's\n * token endpoint, which refuses the code unless the verifier matches its challenge, so a code\n * intercepted on the redirect (a custom-scheme redirect any app can claim) is useless alone.\n * RFC 8252 §8.1 requires it for native apps, whose installed-app clients have no secret.\n */\n code_verifier?: string;\n id_token?: string;\n access_token?: string;\n /**\n * The user's name as the platform hands it to the app, for providers whose id token carries none\n * (Apple sends it on the first authorization only): iOS\n * `ASAuthorizationAppleIDCredential.fullName`, expo-apple-authentication's `credential.fullName`.\n * The snake_case fields above are OAuth / OIDC protocol parameters; this one is not, it is the\n * platform's object passed through unchanged, so it keeps the platform's own name and apps send\n * `fullName: credential.fullName` as is. Unsigned client input: the provider cleans each part and\n * ignores anything that is not a string. Parts with a standard claim become `given_name`,\n * `family_name`, `middle_name` and `nickname`; `namePrefix` / `nameSuffix` have none and are kept\n * in `userInfo.data.fullName` only.\n */\n fullName?: PersonNameComponents;\n};\n\n/** iOS `PersonNameComponents` as JSON; every part may be missing or `null`. */\nexport interface PersonNameComponents {\n givenName?: string | null;\n familyName?: string | null;\n middleName?: string | null;\n namePrefix?: string | null;\n nameSuffix?: string | null;\n nickname?: string | null;\n}\n\n/**\n * Fields of the authorization response besides `code` and `state` that a provider uses to fill in\n * the user info: Apple posts the user's name as `user` (JSON) with the first authorization only,\n * since its id token never carries one. Unsigned, so treated as untrusted input.\n */\nexport interface AuthorizationResponse {\n user?: string;\n}\n\nexport const OIDC_SCOPES = [\n 'openid',\n 'profile',\n 'email',\n 'phone',\n 'address',\n 'offline_access',\n] as const;\n\nexport type OidcScope = (typeof OIDC_SCOPES)[number];\n\n// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims\nexport interface StandardClaims {\n sub: string;\n name?: string;\n given_name?: string;\n family_name?: string;\n middle_name?: string;\n nickname?: string;\n preferred_username?: string;\n profile?: string;\n picture?: string;\n website?: string;\n email?: string;\n /**\n * Set only by providers that state it (Google, Apple, GitHub from the primary address's\n * `verified` flag, X when it returns `confirmed_email`); Facebook leaves it unset. Trust the\n * address only when this is `true`.\n */\n email_verified?: boolean;\n gender?: 'female' | 'male' | (string & {});\n birthdate?: string; // ISO 8601-1 [ISO8601‑1] YYYY-MM-DD format.\n zoneinfo?: string; // IANA Time Zone Database [IANA.time‑zones]\n locale?: string; // BCP47 [RFC5646] language tag.\n phone_number?: string; // E.164\n phone_number_verified?: boolean;\n address?: {\n formatted: string;\n street_address: string;\n locality: string;\n region: string;\n postal_code: string;\n country: string;\n }; // [RFC8259] JSON object\n updated_at?: number;\n}\n\nexport interface UserInfo<T = unknown> {\n claims: StandardClaims;\n data: T;\n}\n\nexport interface Provider {\n /** Authorization URI for the provider. */\n authorizationUri: string;\n\n /** Token URI for the provider. */\n tokenUri: string;\n\n /** Token revoke URI for the provider. */\n tokenRevokeUri?: string;\n\n /** Token refresh URI for the provider. */\n tokenRefreshUri?: string;\n\n /** User info URI for the provider. */\n userInfoUri?: string;\n\n /** User info authentication method for the provider. */\n userInfoAuthenticationMethod?: string;\n\n /**\n * Name of the attribute that will be used to extract the username from the call to 'userInfoUri'.\n * */\n userNameAttribute?: string;\n\n /** JWK set URI for the provider */\n jwkSetUri?: string;\n\n /**\n * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0 Authorization\n * Server Metadata endpoint defined by RFC 8414.\n * */\n issuerUri?: string;\n\n /** Default scopes for the provider. */\n defaultScope: string[];\n\n /**\n * Whether the provider's id token carries back the `nonce` of the request it was issued for\n * (OpenID Connect), per flow. `web`: the redirect flow and One Tap, where `Auth` sends a nonce\n * with the authorization request (or the front end passes an issued one to One Tap) and refuses\n * an id token without it. `native`: the app's sign-in, where the app binds a nonce issued by\n * `GET /oauth2/nonce/:provider` into the platform's request and the login refuses an id token\n * without it. A flow set here is protected by default; a host relaxes it per provider with\n * `oauth2.nonce.optional`. Absent for a plain OAuth2 provider, which has no id token.\n */\n nonce?: { web?: boolean; native?: boolean };\n\n /** step 1: generate authorization uri and redirect user to it */\n createAuthorizationUri: (params: CreateAuthorizationUriParams) => URL;\n\n /** step 2: exchange code for token */\n exchangeAuthorizationCode: (params: ExchangeCodeParams) => Promise<OAuth2Token>;\n\n /**\n * step 3: get user info. `context.clientId` is the client the token was issued to, the only\n * audience an id token may carry; `context.response` holds what the authorization response\n * carried besides the code, for the providers that send user data there (Apple).\n */\n getUserInfo: (token: OAuth2Token, context: UserInfoContext) => Promise<UserInfo>;\n\n /** others: refresh access token */\n refreshAccessToken?: (params: RefreshTokenParams) => Promise<OAuth2Token>;\n\n /** others: revoke token */\n revokeToken?: (params: RevokeTokenParams) => Promise<void>;\n\n loginOAuth2Native?: (\n params: LoginOAuth2NativeParams\n ) => Promise<{ token: OAuth2Token; userInfo: UserInfo }>;\n}\n\nexport interface UserInfoContext {\n clientId: string;\n response?: AuthorizationResponse;\n}\n\nexport interface OneTapProvider extends Provider {\n /** Verifies a One Tap / FedCM credential, which must be issued to `clientId` (the web client). */\n getTokenInfo: (\n id_token: string,\n clientId: string\n ) => Promise<{ token: OAuth2Token; userInfo: UserInfo }>;\n}\n\n/**\n * A client secret: a fixed string, or a function minting one for the client id it is called with\n * (Sign in with Apple's signed JWT, see `createAppleClientSecret`), called on every use so it can\n * cache and renew on its own.\n */\nexport type ClientSecret = string | ((clientId: string) => string | Promise<string>);\n\n/** The client a provider's server-side flows sign in as: the redirect flow and Google One Tap. */\nexport interface WebClient {\n clientId: string;\n /** Absent for a public client. */\n clientSecret?: ClientSecret;\n /** Default: `${baseUri}/login/oauth2/code/<provider key>`. */\n redirectUri?: string;\n /** Authorization scopes. Default: the provider's `defaultScope`. */\n scope?: string[];\n}\n\n/**\n * A client an app signs in as on the device (an iOS bundle id, a Google iOS / Android client) and\n * whose code or id token it posts to the native login route.\n */\nexport interface NativeClient {\n clientId: string;\n /** Absent for a public client (Google's installed-app clients have none). */\n clientSecret?: ClientSecret;\n /**\n * Redirect URI sent with the code exchange when the app sends no `redirect_uri` of its own.\n * Default: `${baseUri}/login/oauth2/code/<provider key>`.\n */\n redirectUri?: string;\n}\n\n/**\n * One identity source: every user it signs in has a `sub` unique within it, whichever client they\n * came through. The key in `OAuth2ClientConfig.providers` names it in the routes and in\n * `onAuthorized`. Two keys may share an implementation (two Google Cloud projects, one Okta per\n * tenant) when their subjects are separate namespaces.\n */\nexport interface ProviderConfig {\n /** The implementation: `apple`, `google`, `github`, ... from `@shware/security/oauth2/provider`. */\n provider: Provider;\n /** The redirect flow's and One Tap's client. Required by those flows. */\n web?: WebClient;\n /**\n * Clients the native login accepts besides `web`. An app names its client with `client_id`;\n * without one, the only native client is used, or `web` when there is none.\n */\n native?: NativeClient[];\n}\n\nexport type ProvidersConfig = { [key: string]: ProviderConfig | undefined };\n\nexport interface OAuth2ClientConfig {\n baseUri: string;\n errorUri: string;\n successUri: string;\n /** Providers by key; a function is called per request, for secrets read at request time. */\n providers: ProvidersConfig | (() => Promise<ProvidersConfig>);\n}\n"],"mappings":";AA4KA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/security",
3
- "version": "9.1.0",
3
+ "version": "9.2.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",