UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

200 lines (198 loc) • 6.61 kB
import { symmetricDecodeJWT, symmetricEncodeJWT } from "../crypto/jwt.mjs"; import "../crypto/index.mjs"; import { safeJSONParse } from "@better-auth/core/utils"; import * as z from "zod"; //#region src/cookies/session-store.ts const ALLOWED_COOKIE_SIZE = 4096; const ESTIMATED_EMPTY_COOKIE_SIZE = 200; const CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE; /** * Parse cookies from the request headers */ function parseCookiesFromContext(ctx) { const cookieHeader = ctx.headers?.get("cookie"); if (!cookieHeader) return {}; const cookies = {}; const pairs = cookieHeader.split("; "); for (const pair of pairs) { const [name, ...valueParts] = pair.split("="); if (name && valueParts.length > 0) cookies[name] = valueParts.join("="); } return cookies; } /** * Extract the chunk index from a cookie name */ function getChunkIndex(cookieName) { const parts = cookieName.split("."); const lastPart = parts[parts.length - 1]; const index = parseInt(lastPart || "0", 10); return isNaN(index) ? 0 : index; } /** * Read all existing chunks from cookies */ function readExistingChunks(cookieName, ctx) { const chunks = {}; const cookies = parseCookiesFromContext(ctx); for (const [name, value] of Object.entries(cookies)) if (name.startsWith(cookieName)) chunks[name] = value; return chunks; } /** * Get the full session data by joining all chunks */ function joinChunks(chunks) { return Object.keys(chunks).sort((a, b) => { return getChunkIndex(a) - getChunkIndex(b); }).map((key) => chunks[key]).join(""); } /** * Split a cookie value into chunks if needed */ function chunkCookie(storeName, cookie, chunks, logger) { const chunkCount = Math.ceil(cookie.value.length / CHUNK_SIZE); if (chunkCount === 1) { chunks[cookie.name] = cookie.value; return [cookie]; } const cookies = []; for (let i = 0; i < chunkCount; i++) { const name = `${cookie.name}.${i}`; const start = i * CHUNK_SIZE; const value = cookie.value.substring(start, start + CHUNK_SIZE); cookies.push({ ...cookie, name, value }); chunks[name] = value; } logger.debug(`CHUNKING_${storeName.toUpperCase()}_COOKIE`, { message: `${storeName} cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`, emptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE, valueSize: cookie.value.length, chunkCount, chunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE) }); return cookies; } /** * Get all cookies that should be cleaned (removed) */ function getCleanCookies(chunks, cookieOptions) { const cleanedChunks = {}; for (const name in chunks) cleanedChunks[name] = { name, value: "", options: { ...cookieOptions, maxAge: 0 } }; return cleanedChunks; } /** * Create a session store for handling cookie chunking. * When session data exceeds 4KB, it automatically splits it into multiple cookies. * * Based on next-auth's SessionStore implementation. * @see https://github.com/nextauthjs/next-auth/blob/27b2519b84b8eb9cf053775dea29d577d2aa0098/packages/next-auth/src/core/lib/cookie.ts */ const storeFactory = (storeName) => (cookieName, cookieOptions, ctx) => { const chunks = readExistingChunks(cookieName, ctx); const logger = ctx.context.logger; return { getValue() { return joinChunks(chunks); }, hasChunks() { return Object.keys(chunks).length > 0; }, chunk(value, options) { const cleanedChunks = getCleanCookies(chunks, cookieOptions); for (const name in chunks) delete chunks[name]; const cookies = cleanedChunks; const chunked = chunkCookie(storeName, { name: cookieName, value, options: { ...cookieOptions, ...options } }, chunks, logger); for (const chunk of chunked) cookies[chunk.name] = chunk; return Object.values(cookies); }, clean() { const cleanedChunks = getCleanCookies(chunks, cookieOptions); for (const name in chunks) delete chunks[name]; return Object.values(cleanedChunks); }, setCookies(cookies) { for (const cookie of cookies) ctx.setCookie(cookie.name, cookie.value, cookie.options); } }; }; const createSessionStore = storeFactory("Session"); const createAccountStore = storeFactory("Account"); function getChunkedCookie(ctx, cookieName) { const value = ctx.getCookie(cookieName); if (value) return value; const chunks = []; const cookieHeader = ctx.headers?.get("cookie"); if (!cookieHeader) return null; const cookies = {}; const pairs = cookieHeader.split("; "); for (const pair of pairs) { const [name, ...valueParts] = pair.split("="); if (name && valueParts.length > 0) cookies[name] = valueParts.join("="); } for (const [name, val] of Object.entries(cookies)) if (name.startsWith(cookieName + ".")) { const indexStr = name.split(".").at(-1); const index = parseInt(indexStr || "0", 10); if (!isNaN(index)) chunks.push({ index, value: val }); } if (chunks.length > 0) { chunks.sort((a, b) => a.index - b.index); return chunks.map((c) => c.value).join(""); } return null; } async function setAccountCookie(c, accountData) { const accountDataCookie = c.context.authCookies.accountData; const options = { maxAge: 300, ...accountDataCookie.options }; const data = await symmetricEncodeJWT(accountData, c.context.secret, "better-auth-account", options.maxAge); if (data.length > ALLOWED_COOKIE_SIZE) { const accountStore = createAccountStore(accountDataCookie.name, options, c); const cookies = accountStore.chunk(data, options); accountStore.setCookies(cookies); } else { const accountStore = createAccountStore(accountDataCookie.name, options, c); if (accountStore.hasChunks()) { const cleanCookies = accountStore.clean(); accountStore.setCookies(cleanCookies); } c.setCookie(accountDataCookie.name, data, options); } } async function getAccountCookie(c) { const accountCookie = getChunkedCookie(c, c.context.authCookies.accountData.name); if (accountCookie) { const accountData = safeJSONParse(await symmetricDecodeJWT(accountCookie, c.context.secret, "better-auth-account")); if (accountData) return accountData; } return null; } const getSessionQuerySchema = z.optional(z.object({ disableCookieCache: z.coerce.boolean().meta({ description: "Disable cookie cache and fetch session from database" }).optional(), disableRefresh: z.coerce.boolean().meta({ description: "Disable session refresh. Useful for checking session status, without updating the session" }).optional() })); //#endregion export { createAccountStore, createSessionStore, getAccountCookie, getChunkedCookie, getSessionQuerySchema, setAccountCookie }; //# sourceMappingURL=session-store.mjs.map