better-auth
Version:
The most comprehensive authentication framework for TypeScript.
234 lines (233 loc) • 11 kB
JavaScript
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 };