UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

139 lines (138 loc) 5.48 kB
import { generateRandomString } from "./crypto/random.mjs"; import { symmetricDecrypt, symmetricEncrypt } from "./crypto/index.mjs"; import { expireCookie } from "./cookies/index.mjs"; import { BetterAuthError } from "@better-auth/core/error"; import * as z from "zod"; //#region src/state.ts const stateDataSchema = z.looseObject({ callbackURL: z.string(), codeVerifier: z.string(), errorURL: z.string().optional(), newUserURL: z.string().optional(), expiresAt: z.number(), /** * CSRF nonce returned to the OAuth provider. When using cookie state storage, * this must match the callback `state` query parameter. */ oauthState: z.string().optional(), link: z.object({ email: z.string(), userId: z.coerce.string() }).optional(), requestSignUp: z.boolean().optional() }); const INTERNAL_STATE_KEYS = new Set(Object.keys(stateDataSchema.shape)); var StateError = class extends BetterAuthError { code; details; /** * The per-flow `errorCallbackURL` recovered from the parsed state, when the * failure happened after the state was successfully parsed (for example a * nonce or state-cookie mismatch). It was origin-validated at sign-in, so * the callback can safely redirect there instead of the default error page. * Absent when the state could not be parsed at all. */ errorURL; constructor(message, options) { super(message, options); this.code = options.code; this.details = options.details; this.errorURL = options.errorURL; } }; async function generateGenericState(c, stateData, settings) { const state = generateRandomString(32); if (c.context.oauthConfig.storeStateStrategy === "cookie") { const payload = { ...stateData, oauthState: state }; const encryptedData = await symmetricEncrypt({ key: c.context.secretConfig, data: JSON.stringify(payload) }); const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "oauth_state", { maxAge: 600 }); c.setCookie(stateCookie.name, encryptedData, stateCookie.attributes); return { state, codeVerifier: stateData.codeVerifier }; } const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "state", { maxAge: 300 }); await c.setSignedCookie(stateCookie.name, state, c.context.secret, stateCookie.attributes); const expiresAt = /* @__PURE__ */ new Date(); expiresAt.setMinutes(expiresAt.getMinutes() + 10); if (!await c.context.internalAdapter.createVerificationValue({ value: JSON.stringify({ ...stateData, oauthState: state }), identifier: state, expiresAt })) throw new StateError("Unable to create verification. Make sure the database adapter is properly working and there is a verification table in the database", { code: "state_generation_error" }); return { state, codeVerifier: stateData.codeVerifier }; } async function parseGenericState(c, state, settings) { if (!state) throw new StateError("State not found in OAuth callback", { code: "state_not_found" }); const storeStateStrategy = c.context.oauthConfig.storeStateStrategy; let parsedData; if (storeStateStrategy === "cookie") { const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "oauth_state"); const encryptedData = c.getCookie(stateCookie.name); if (!encryptedData) throw new StateError("State mismatch: auth state cookie not found", { code: "state_mismatch", details: { state } }); try { const decryptedData = await symmetricDecrypt({ key: c.context.secretConfig, data: encryptedData }); parsedData = stateDataSchema.parse(JSON.parse(decryptedData)); } catch (error) { throw new StateError("State invalid: Failed to decrypt or parse auth state", { code: "state_invalid", details: { state }, cause: error }); } if (!parsedData.oauthState || parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", { code: "state_security_mismatch", details: { state }, errorURL: parsedData.errorURL }); expireCookie(c, stateCookie); } else { const data = await c.context.internalAdapter.findVerificationValue(state); if (!data) throw new StateError("State mismatch: verification not found", { code: "state_mismatch", details: { state } }); parsedData = stateDataSchema.parse(JSON.parse(data.value)); if (parsedData.oauthState !== void 0 && parsedData.oauthState !== state) throw new StateError("State mismatch: OAuth state parameter does not match stored state", { code: "state_security_mismatch", details: { state }, errorURL: parsedData.errorURL }); const stateCookie = c.context.createAuthCookie(settings?.cookieName ?? "state"); const stateCookieValue = await c.getSignedCookie(stateCookie.name, c.context.secret); if (!(settings?.skipStateCookieCheck ?? c.context.oauthConfig.skipStateCookieCheck) && (!stateCookieValue || stateCookieValue !== state)) throw new StateError("State mismatch: State not persisted correctly", { code: "state_security_mismatch", details: { state }, errorURL: parsedData.errorURL }); expireCookie(c, stateCookie); await c.context.internalAdapter.deleteVerificationByIdentifier(state); } if (parsedData.expiresAt < Date.now()) throw new StateError("Invalid state: request expired", { code: "state_mismatch", details: { expiresAt: parsedData.expiresAt }, errorURL: parsedData.errorURL }); return parsedData; } //#endregion export { INTERNAL_STATE_KEYS, StateError, generateGenericState, parseGenericState };