UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

234 lines (233 loc) 11 kB
import { getBaseURL, isDynamicBaseURLConfig } from "../utils/url.mjs"; import { matchesOriginPattern } from "../auth/trusted-origins.mjs"; import { hasServerSessionStore } from "./store-capabilities.mjs"; import { isPromise } from "../utils/is-promise.mjs"; import { hashPassword, verifyPassword } from "../crypto/password.mjs"; import { createCookieGetter, getCookies } from "../cookies/index.mjs"; import { createInternalAdapter } from "../db/internal-adapter.mjs"; import { getInternalPlugins, getTrustedOrigins, getTrustedProviders, runPluginInit } from "./helpers.mjs"; import { checkPassword } from "../utils/password.mjs"; import { checkEndpointConflicts } from "../api/index.mjs"; import { DEFAULT_SECRET } from "../utils/constants.mjs"; import { buildSecretConfig, parseSecretsEnv, validateSecretsArray } from "./secret-utils.mjs"; import { getBetterAuthVersion } from "@better-auth/core/context"; import { getAuthTables } from "@better-auth/core/db"; import { createLogger, env, isProduction, isTest } from "@better-auth/core/env"; import { BetterAuthError } from "@better-auth/core/error"; import { generateId } from "@better-auth/core/utils/id"; import { socialProviders } from "@better-auth/core/social-providers"; import { findInvalidTrustedProxies } from "@better-auth/core/utils/ip"; import { createTelemetry } from "@better-auth/telemetry"; import defu$1 from "defu"; //#region src/context/create-context.ts /** * Estimates the entropy of a string in bits. * This is a simple approximation that helps detect low-entropy secrets. */ function estimateEntropy(str) { const unique = new Set(str).size; if (unique === 0) return 0; return Math.log2(Math.pow(unique, str.length)); } /** * Validates that the secret meets minimum security requirements. * Throws BetterAuthError if the secret is invalid. * Skips validation for DEFAULT_SECRET in test environments only. * Only throws for DEFAULT_SECRET in production environment. */ function validateSecret(secret, logger) { const isDefaultSecret = secret === DEFAULT_SECRET; if (isTest()) return; if (isDefaultSecret && isProduction) throw new BetterAuthError("You are using the default secret. Please set `BETTER_AUTH_SECRET` in your environment variables or pass `secret` in your auth config."); if (!secret) throw new BetterAuthError("BETTER_AUTH_SECRET is missing. Set it in your environment or pass `secret` to betterAuth({ secret })."); if (secret.length < 32) logger.warn(`[better-auth] Warning: your BETTER_AUTH_SECRET should be at least 32 characters long for adequate security. Generate one with \`npx auth secret\` or \`openssl rand -base64 32\`.`); if (estimateEntropy(secret) < 120) logger.warn("[better-auth] Warning: your BETTER_AUTH_SECRET appears low-entropy. Use a randomly generated secret for production."); } async function createAuthContext(adapter, options, getDatabaseType) { const isStateful = hasServerSessionStore(options); if (!isStateful) options = defu$1(options, { session: { cookieCache: { enabled: true, strategy: "jwe", refreshCache: true, maxAge: options.session?.expiresIn || 3600 * 24 * 7 } } }); if (!options.database) options = defu$1(options, { account: { storeAccountCookie: true } }); const plugins = options.plugins || []; const internalPlugins = getInternalPlugins(options); const logger = createLogger(options.logger); const isDynamicConfig = isDynamicBaseURLConfig(options.baseURL); if (isDynamicBaseURLConfig(options.baseURL)) { const { allowedHosts } = options.baseURL; if (!allowedHosts || allowedHosts.length === 0) throw new BetterAuthError("baseURL.allowedHosts cannot be empty. Provide at least one allowed host pattern (e.g., [\"myapp.com\", \"*.vercel.app\"])."); } const baseURL = isDynamicConfig ? void 0 : getBaseURL(typeof options.baseURL === "string" ? options.baseURL : void 0, options.basePath); if (!baseURL && !isDynamicConfig) logger.warn(`[better-auth] Base URL is not set. Set the baseURL option or BETTER_AUTH_URL env, or use a dynamic baseURL with allowedHosts for multi-host setups. Without it the origin is derived from the incoming request, and callbacks and redirects may not work correctly.`); if (adapter.id === "memory" && options.advanced?.database?.generateId === false) logger.error(`[better-auth] Misconfiguration detected. You are using the memory DB with generateId: false. This will cause no id to be generated for any model. Most of the features of Better Auth will not work correctly.`); const secretsArray = options.secrets ?? parseSecretsEnv(env.BETTER_AUTH_SECRETS); const legacySecret = options.secret || env.BETTER_AUTH_SECRET || env.AUTH_SECRET || ""; let secret; let secretConfig; if (secretsArray) { validateSecretsArray(secretsArray, logger); secret = secretsArray[0].value; secretConfig = buildSecretConfig(secretsArray, legacySecret); } else { secret = legacySecret || "better-auth-secret-12345678901234567890"; validateSecret(secret, logger); secretConfig = secret; } options = { ...options, secret, baseURL: isDynamicConfig ? options.baseURL : baseURL ? new URL(baseURL).origin : "", basePath: options.basePath || "/api/auth", plugins: plugins.concat(internalPlugins) }; checkEndpointConflicts(options, logger); const trustedProxies = options.advanced?.ipAddress?.trustedProxies; if (trustedProxies && trustedProxies.length > 0) { const invalid = findInvalidTrustedProxies(trustedProxies); if (invalid.length > 0) logger.warn(`Ignoring invalid \`advanced.ipAddress.trustedProxies\` entries: ${invalid.join(", ")}. Each entry must be an IP address or CIDR range.`); } const cookies = getCookies(options); const tables = getAuthTables(options); const providers = (await Promise.all(Object.entries(options.socialProviders || {}).map(async ([key, originalConfig]) => { const config = typeof originalConfig === "function" ? await originalConfig() : originalConfig; if (config == null) return null; if (config.enabled === false) return null; if (!config.clientId) logger.warn(`Social provider ${key} is missing clientId or clientSecret`); const provider = socialProviders[key](config); provider.disableImplicitSignUp = config.disableImplicitSignUp; return provider; }))).filter((x) => x !== null); const generateIdFunc = ({ model, size }) => { if (typeof options.advanced?.generateId === "function") return options.advanced.generateId({ model, size }); const dbGenerateId = options?.advanced?.database?.generateId; if (typeof dbGenerateId === "function") return dbGenerateId({ model, size }); if (dbGenerateId === "uuid") return crypto.randomUUID(); if (dbGenerateId === "serial" || dbGenerateId === false) return false; return generateId(size); }; const { publish } = await createTelemetry(options, { adapter: adapter.id, database: typeof options.database === "function" ? "adapter" : getDatabaseType(options.database) }); const pluginIds = new Set(options.plugins.map((p) => p.id)); const getPluginFn = (id) => options.plugins.find((p) => p.id === id) ?? null; const hasPluginFn = (id) => pluginIds.has(id); const trustedOrigins = await getTrustedOrigins(options); const trustedProviders = await getTrustedProviders(options); const ctx = { appName: options.appName || "Better Auth", baseURL: baseURL || "", version: getBetterAuthVersion(), socialProviders: providers, options, oauthConfig: { storeStateStrategy: options.account?.storeStateStrategy || (isStateful ? "database" : "cookie"), skipStateCookieCheck: !!options.account?.skipStateCookieCheck }, tables, trustedOrigins, trustedProviders, isTrustedOrigin(url, settings) { return this.trustedOrigins.some((origin) => matchesOriginPattern(url, origin, settings)); }, sessionConfig: { updateAge: options.session?.updateAge !== void 0 ? options.session.updateAge : 1440 * 60, expiresIn: options.session?.expiresIn || 3600 * 24 * 7, freshAge: options.session?.freshAge === void 0 ? 3600 * 24 : options.session.freshAge, cookieRefreshCache: (() => { const refreshCache = options.session?.cookieCache?.refreshCache; const maxAge = options.session?.cookieCache?.maxAge || 300; if (isStateful && refreshCache) { logger.warn("[better-auth] `session.cookieCache.refreshCache` is enabled while `database` or `secondaryStorage` is configured. `refreshCache` is meant for stateless (DB-less) setups. Disabling `refreshCache` — remove it from your config to silence this warning."); return false; } if (refreshCache === false || refreshCache === void 0) return false; if (refreshCache === true) return { enabled: true, updateAge: Math.floor(maxAge * .2) }; return { enabled: true, updateAge: refreshCache.updateAge !== void 0 ? refreshCache.updateAge : Math.floor(maxAge * .2) }; })() }, secret, secretConfig, rateLimit: { ...options.rateLimit, enabled: options.rateLimit?.enabled ?? isProduction, window: options.rateLimit?.window || 10, max: options.rateLimit?.max || 100, storage: options.rateLimit?.storage || (options.secondaryStorage ? "secondary-storage" : "memory") }, authCookies: cookies, logger, generateId: generateIdFunc, session: null, secondaryStorage: options.secondaryStorage, password: { hash: options.emailAndPassword?.password?.hash || hashPassword, verify: options.emailAndPassword?.password?.verify || verifyPassword, config: { minPasswordLength: options.emailAndPassword?.minPasswordLength || 8, maxPasswordLength: options.emailAndPassword?.maxPasswordLength || 128 }, checkPassword }, setNewSession(session) { this.newSession = session; }, newSession: null, adapter, internalAdapter: createInternalAdapter(adapter, { options, logger, hooks: options.databaseHooks ? [{ source: "user", hooks: options.databaseHooks }] : [], generateId: generateIdFunc }), createAuthCookie: createCookieGetter(options), async runMigrations() { throw new BetterAuthError("runMigrations will be set by the specific init implementation"); }, publishTelemetry: publish, skipCSRFCheck: !!options.advanced?.disableCSRFCheck, skipOriginCheck: options.advanced?.disableOriginCheck !== void 0 ? options.advanced.disableOriginCheck : isTest() ? true : false, runInBackground: options.advanced?.backgroundTasks?.handler ?? ((p) => { p.catch(() => {}); }), async runInBackgroundOrAwait(promise) { try { if (options.advanced?.backgroundTasks?.handler) { if (promise instanceof Promise) options.advanced.backgroundTasks.handler(promise.catch((e) => { logger.error("Failed to run background task:", e); })); } else await promise; } catch (e) { logger.error("Failed to run background task:", e); } }, getPlugin: getPluginFn, hasPlugin: hasPluginFn }; const initOrPromise = runPluginInit(ctx); if (isPromise(initOrPromise)) await initOrPromise; return ctx; } //#endregion export { createAuthContext };