fix: prevent stateless refresh with database configured (#6700)

This commit is contained in:
Bereket Engida
2025-12-13 15:23:03 -08:00
committed by github-actions[bot]
parent 99defeaf80
commit 9fed2ff3ad
4 changed files with 73 additions and 4 deletions

View File

@@ -913,7 +913,7 @@ describe("cookie cache refreshCache", async () => {
expect(fn).toHaveBeenCalledTimes(1);
});
it("should refresh cache stateless when refreshCache threshold is exceeded", async () => {
it("should not perform stateless refresh when a database is configured", async () => {
const callsBefore = fn.mock.calls.length;
vi.useFakeTimers();
@@ -930,9 +930,10 @@ describe("cookie cache refreshCache", async () => {
});
expect(session.data).not.toBeNull();
// With stateless refresh, no DB call should be made (it just refreshes the cookie)
// With a database configured, `refreshCache` is ignored (a warning is logged),
// so no additional DB call should be made here.
const callsAfterRefresh = fn.mock.calls.length;
expect(callsAfterRefresh).toBe(callsBefore); // No DB call for stateless refresh
expect(callsAfterRefresh).toBe(callsBefore);
await client.getSession({
fetchOptions: {
@@ -1057,6 +1058,8 @@ describe("cookie cache refreshCache", async () => {
it("should work without database when refreshCache threshold is reached", async () => {
const { client, testUser, cookieSetter, auth } = await getTestInstance({
// True stateless mode: no database configured
database: undefined as any,
session: {
cookieCache: {
enabled: true,

View File

@@ -226,9 +226,12 @@ export const getSession = <Option extends BetterAuthOptions>() =>
maxAge: 0,
});
} else {
const cachedSessionExpiresAt = new Date(
session.session.expiresAt as unknown as string | number | Date,
);
const hasExpired =
sessionDataPayload.expiresAt < Date.now() ||
session.session.expiresAt < new Date();
cachedSessionExpiresAt < new Date();
if (hasExpired) {
// When the session data cookie has expired, delete it;

View File

@@ -329,6 +329,58 @@ describe("base context creation", () => {
});
});
it("should disable cookieRefreshCache and warn when database is configured with refreshCache=true", async () => {
const log = vi.fn();
const res = await initBase({
logger: {
level: "warn",
log,
} as any,
database: new Database(":memory:"),
session: {
cookieCache: {
refreshCache: true,
},
},
});
expect(res.sessionConfig.cookieRefreshCache).toBe(false);
expect(log).toHaveBeenCalledWith(
"warn",
expect.stringContaining(
"`session.cookieCache.refreshCache` is enabled while `database` or `secondaryStorage` is configured",
),
);
});
it("should disable cookieRefreshCache and warn when secondaryStorage is configured with refreshCache=true", async () => {
const log = vi.fn();
const res = await initBase({
logger: {
level: "warn",
log,
} as any,
secondaryStorage: {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
},
session: {
cookieCache: {
refreshCache: true,
},
},
});
expect(res.sessionConfig.cookieRefreshCache).toBe(false);
expect(log).toHaveBeenCalledWith(
"warn",
expect.stringContaining(
"`session.cookieCache.refreshCache` is enabled while `database` or `secondaryStorage` is configured",
),
);
});
it("should use default maxAge (300) for 20% calculation", async () => {
const res = await initBase({
session: {

View File

@@ -197,6 +197,17 @@ export async function createAuthContext(
const refreshCache = options.session?.cookieCache?.refreshCache;
const maxAge = options.session?.cookieCache?.maxAge || 60 * 5;
// `refreshCache` is intended for fully stateless / DB-less setups.
// If a server-side store is configured, prefer fetching/refreshing from that source
// and disable stateless refresh behavior to avoid confusing/unsafe configurations.
const isStateful = !!options.database || !!options.secondaryStorage;
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 === undefined) {
return false;
}