better-auth
Version:
The most comprehensive authentication framework for TypeScript.
281 lines (279 loc) • 12.8 kB
JavaScript
import { getDate } from "../utils/date.mjs";
import { parseUserOutput } from "../db/schema.mjs";
import { signJWT, symmetricDecodeJWT, symmetricEncodeJWT, verifyJWT } from "../crypto/jwt.mjs";
import { createAccountStore, createSessionStore, getChunkedCookie } from "./session-store.mjs";
import { sec } from "../utils/time.mjs";
import { parseSetCookieHeader, setCookieToHeader } from "./cookie-utils.mjs";
import { env, isProduction } from "@better-auth/core/env";
import { BetterAuthError } from "@better-auth/core/error";
import { safeJSONParse } from "@better-auth/core/utils";
import { base64Url } from "@better-auth/utils/base64";
import { binary } from "@better-auth/utils/binary";
import { createHMAC } from "@better-auth/utils/hmac";
//#region src/cookies/index.ts
function createCookieGetter(options) {
const secureCookiePrefix = (options.advanced?.useSecureCookies !== void 0 ? options.advanced?.useSecureCookies : options.baseURL !== void 0 ? options.baseURL.startsWith("https://") ? true : false : isProduction) ? "__Secure-" : "";
const crossSubdomainEnabled = !!options.advanced?.crossSubDomainCookies?.enabled;
const domain = crossSubdomainEnabled ? options.advanced?.crossSubDomainCookies?.domain || (options.baseURL ? new URL(options.baseURL).hostname : void 0) : void 0;
if (crossSubdomainEnabled && !domain) throw new BetterAuthError("baseURL is required when crossSubdomainCookies are enabled");
function createCookie(cookieName, overrideAttributes = {}) {
const prefix = options.advanced?.cookiePrefix || "better-auth";
const name = options.advanced?.cookies?.[cookieName]?.name || `${prefix}.${cookieName}`;
const attributes = options.advanced?.cookies?.[cookieName]?.attributes;
return {
name: `${secureCookiePrefix}${name}`,
attributes: {
secure: !!secureCookiePrefix,
sameSite: "lax",
path: "/",
httpOnly: true,
...crossSubdomainEnabled ? { domain } : {},
...options.advanced?.defaultCookieAttributes,
...overrideAttributes,
...attributes
}
};
}
return createCookie;
}
function getCookies(options) {
const createCookie = createCookieGetter(options);
const sessionToken = createCookie("session_token", { maxAge: options.session?.expiresIn || sec("7d") });
const sessionData = createCookie("session_data", { maxAge: options.session?.cookieCache?.maxAge || 300 });
const accountData = createCookie("account_data", { maxAge: options.session?.cookieCache?.maxAge || 300 });
const dontRememberToken = createCookie("dont_remember");
return {
sessionToken: {
name: sessionToken.name,
options: sessionToken.attributes
},
sessionData: {
name: sessionData.name,
options: sessionData.attributes
},
dontRememberToken: {
name: dontRememberToken.name,
options: dontRememberToken.attributes
},
accountData: {
name: accountData.name,
options: accountData.attributes
}
};
}
async function setCookieCache(ctx, session, dontRememberMe) {
if (ctx.context.options.session?.cookieCache?.enabled) {
const filteredSession = Object.entries(session.session).reduce((acc, [key, value]) => {
const fieldConfig = ctx.context.options.session?.additionalFields?.[key];
if (!fieldConfig || fieldConfig.returned !== false) acc[key] = value;
return acc;
}, {});
const filteredUser = parseUserOutput(ctx.context.options, session.user);
const versionConfig = ctx.context.options.session?.cookieCache?.version;
let version = "1";
if (versionConfig) {
if (typeof versionConfig === "string") version = versionConfig;
else if (typeof versionConfig === "function") {
const result = versionConfig(session.session, session.user);
version = result instanceof Promise ? await result : result;
}
}
const sessionData = {
session: filteredSession,
user: filteredUser,
updatedAt: Date.now(),
version
};
const options = {
...ctx.context.authCookies.sessionData.options,
maxAge: dontRememberMe ? void 0 : ctx.context.authCookies.sessionData.options.maxAge
};
const expiresAtDate = getDate(options.maxAge || 60, "sec").getTime();
const strategy = ctx.context.options.session?.cookieCache?.strategy || "compact";
let data;
if (strategy === "jwe") data = await symmetricEncodeJWT(sessionData, ctx.context.secret, "better-auth-session", options.maxAge || 300);
else if (strategy === "jwt") data = await signJWT(sessionData, ctx.context.secret, options.maxAge || 300);
else data = base64Url.encode(JSON.stringify({
session: sessionData,
expiresAt: expiresAtDate,
signature: await createHMAC("SHA-256", "base64urlnopad").sign(ctx.context.secret, JSON.stringify({
...sessionData,
expiresAt: expiresAtDate
}))
}), { padding: false });
if (data.length > 4093) {
const sessionStore = createSessionStore(ctx.context.authCookies.sessionData.name, options, ctx);
const cookies = sessionStore.chunk(data, options);
sessionStore.setCookies(cookies);
} else {
const sessionStore = createSessionStore(ctx.context.authCookies.sessionData.name, options, ctx);
if (sessionStore.hasChunks()) {
const cleanCookies = sessionStore.clean();
sessionStore.setCookies(cleanCookies);
}
ctx.setCookie(ctx.context.authCookies.sessionData.name, data, options);
}
}
}
async function setSessionCookie(ctx, session, dontRememberMe, overrides) {
const dontRememberMeCookie = await ctx.getSignedCookie(ctx.context.authCookies.dontRememberToken.name, ctx.context.secret);
dontRememberMe = dontRememberMe !== void 0 ? dontRememberMe : !!dontRememberMeCookie;
const options = ctx.context.authCookies.sessionToken.options;
const maxAge = dontRememberMe ? void 0 : ctx.context.sessionConfig.expiresIn;
await ctx.setSignedCookie(ctx.context.authCookies.sessionToken.name, session.session.token, ctx.context.secret, {
...options,
maxAge,
...overrides
});
if (dontRememberMe) await ctx.setSignedCookie(ctx.context.authCookies.dontRememberToken.name, "true", ctx.context.secret, ctx.context.authCookies.dontRememberToken.options);
await setCookieCache(ctx, session, dontRememberMe);
ctx.context.setNewSession(session);
/**
* If secondary storage is enabled, store the session data in the secondary storage
* This is useful if the session got updated and we want to update the session data in the
* secondary storage
*/
if (ctx.context.options.secondaryStorage) await ctx.context.secondaryStorage?.set(session.session.token, JSON.stringify({
user: session.user,
session: session.session
}), Math.floor((new Date(session.session.expiresAt).getTime() - Date.now()) / 1e3));
}
function deleteSessionCookie(ctx, skipDontRememberMe) {
ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
...ctx.context.authCookies.sessionToken.options,
maxAge: 0
});
ctx.setCookie(ctx.context.authCookies.sessionData.name, "", {
...ctx.context.authCookies.sessionData.options,
maxAge: 0
});
if (ctx.context.options.account?.storeAccountCookie) {
ctx.setCookie(ctx.context.authCookies.accountData.name, "", {
...ctx.context.authCookies.accountData.options,
maxAge: 0
});
const accountStore = createAccountStore(ctx.context.authCookies.accountData.name, ctx.context.authCookies.accountData.options, ctx);
const cleanCookies$1 = accountStore.clean();
accountStore.setCookies(cleanCookies$1);
}
if (ctx.context.oauthConfig.storeStateStrategy === "cookie") {
const stateCookie = ctx.context.createAuthCookie("oauth_state");
ctx.setCookie(stateCookie.name, "", {
...stateCookie.attributes,
maxAge: 0
});
}
const sessionStore = createSessionStore(ctx.context.authCookies.sessionData.name, ctx.context.authCookies.sessionData.options, ctx);
const cleanCookies = sessionStore.clean();
sessionStore.setCookies(cleanCookies);
if (!skipDontRememberMe) ctx.setCookie(ctx.context.authCookies.dontRememberToken.name, "", {
...ctx.context.authCookies.dontRememberToken.options,
maxAge: 0
});
}
function parseCookies(cookieHeader) {
const cookies = cookieHeader.split("; ");
const cookieMap = /* @__PURE__ */ new Map();
cookies.forEach((cookie) => {
const [name, value] = cookie.split(/=(.*)/s);
cookieMap.set(name, value);
});
return cookieMap;
}
const getSessionCookie = (request, config) => {
if (config?.cookiePrefix) if (config.cookieName) config.cookiePrefix = `${config.cookiePrefix}-`;
else config.cookiePrefix = `${config.cookiePrefix}.`;
const cookies = ("headers" in request ? request.headers : request).get("cookie");
if (!cookies) return null;
const { cookieName = "session_token", cookiePrefix = "better-auth." } = config || {};
const name = `${cookiePrefix}${cookieName}`;
const secureCookieName = `__Secure-${name}`;
const parsedCookie = parseCookies(cookies);
const sessionToken = parsedCookie.get(name) || parsedCookie.get(secureCookieName);
if (sessionToken) return sessionToken;
return null;
};
const getCookieCache = async (request, config) => {
const cookies = (request instanceof Headers ? request : request.headers).get("cookie");
if (!cookies) return null;
const { cookieName = "session_data", cookiePrefix = "better-auth" } = config || {};
const name = config?.isSecure !== void 0 ? config.isSecure ? `__Secure-${cookiePrefix}.${cookieName}` : `${cookiePrefix}.${cookieName}` : isProduction ? `__Secure-${cookiePrefix}.${cookieName}` : `${cookiePrefix}.${cookieName}`;
const parsedCookie = parseCookies(cookies);
let sessionData = parsedCookie.get(name);
if (!sessionData) {
const chunks = [];
for (const [cookieName$1, value] of parsedCookie.entries()) if (cookieName$1.startsWith(name + ".")) {
const parts = cookieName$1.split(".");
const indexStr = parts[parts.length - 1];
const index = parseInt(indexStr || "0", 10);
if (!isNaN(index)) chunks.push({
index,
value
});
}
if (chunks.length > 0) {
chunks.sort((a, b) => a.index - b.index);
sessionData = chunks.map((c) => c.value).join("");
}
}
if (sessionData) {
const secret = config?.secret || env.BETTER_AUTH_SECRET;
if (!secret) throw new BetterAuthError("getCookieCache requires a secret to be provided. Either pass it as an option or set the BETTER_AUTH_SECRET environment variable");
const strategy = config?.strategy || "compact";
if (strategy === "jwe") {
const payload = await symmetricDecodeJWT(sessionData, secret, "better-auth-session");
if (payload && payload.session && payload.user) {
if (config?.version) {
const cookieVersion = payload.version || "1";
let expectedVersion = "1";
if (typeof config.version === "string") expectedVersion = config.version;
else if (typeof config.version === "function") {
const result = config.version(payload.session, payload.user);
expectedVersion = result instanceof Promise ? await result : result;
}
if (cookieVersion !== expectedVersion) return null;
}
return payload;
}
return null;
} else if (strategy === "jwt") {
const payload = await verifyJWT(sessionData, secret);
if (payload && payload.session && payload.user) {
if (config?.version) {
const cookieVersion = payload.version || "1";
let expectedVersion = "1";
if (typeof config.version === "string") expectedVersion = config.version;
else if (typeof config.version === "function") {
const result = config.version(payload.session, payload.user);
expectedVersion = result instanceof Promise ? await result : result;
}
if (cookieVersion !== expectedVersion) return null;
}
return payload;
}
return null;
} else {
const sessionDataPayload = safeJSONParse(binary.decode(base64Url.decode(sessionData)));
if (!sessionDataPayload) return null;
if (!await createHMAC("SHA-256", "base64urlnopad").verify(secret, JSON.stringify({
...sessionDataPayload.session,
expiresAt: sessionDataPayload.expiresAt
}), sessionDataPayload.signature)) return null;
if (config?.version && sessionDataPayload.session) {
const cookieVersion = sessionDataPayload.session.version || "1";
let expectedVersion = "1";
if (typeof config.version === "string") expectedVersion = config.version;
else if (typeof config.version === "function") {
const result = config.version(sessionDataPayload.session.session, sessionDataPayload.session.user);
expectedVersion = result instanceof Promise ? await result : result;
}
if (cookieVersion !== expectedVersion) return null;
}
return sessionDataPayload.session;
}
}
return null;
};
//#endregion
export { createCookieGetter, createSessionStore, deleteSessionCookie, getChunkedCookie, getCookieCache, getCookies, getSessionCookie, parseCookies, parseSetCookieHeader, setCookieCache, setCookieToHeader, setSessionCookie };
//# sourceMappingURL=index.mjs.map