[GH-ISSUE #8770] session.expiresAt overwritten with cookieCache.maxAge during stateless refreshCache renewal #28509

Open
opened 2026-04-17 19:57:55 -05:00 by GiteaMirror · 0 comments
Owner

Originally created by @Lookwe69 on GitHub (Mar 25, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8770

Describe the bug

When cookieCache.refreshCache is enabled (stateless mode — no database configured), the get-session endpoint overwrites session.session.expiresAt with now + cookieCache.maxAge instead of preserving the real session expiry. Every getSession call during a cache refresh returns a corrupted session.expiresAt (e.g. 10 minutes from now instead of 7 days).

Root Cause

In src/api/routes/session.ts, the stateless cache refresh path creates a refreshedSession that overrides expiresAt with the cookie cache TTL:

const newExpiresAt = getDate(ctx.context.options.session?.cookieCache?.maxAge || 300, "sec");
const refreshedSession = {
  session: {
    ...session.session,
    expiresAt: newExpiresAt, // ← overwrites real session expiry (e.g. 7 days) with cache TTL (e.g. 10 min)
  },
  user: session.user,
  updatedAt: Date.now(),
};
await setCookieCache(ctx, refreshedSession, false);
return ctx.json({ session: parsedRefreshedSession, ... });
//                          ↑ expiresAt = now + 10min, not the real 7-day session expiry

setCookieCache already derives the JWE cookie TTL independently from authCookies.sessionData.attributes.maxAgenewExpiresAt serves no other purpose here and only corrupts session.session.expiresAt.

Steps to Reproduce

1- Configure better-auth without a database (stateless mode — memory adapter used automatically)
2- Enable cookieCache with refreshCache: true:

const auth = betterAuth({
  session: {
    cookieCache: {
      enabled: true,
      strategy: 'jwe',
      maxAge: 60,
      refreshCache: true,
    },
  },
});

3- Sign in - observer session.expiresAt is correct (7 days from now)
4- Wait for updateAge to pass
5- Call getSession again - the refresh path triggers
6- session.expiresAt is now now + 60s instead of now + 7 days

Expected behavior

session.expiresAt always reflects session.expiresIn (default 7 days). The cookie cache TTL is internal and must not leak into the public session object.

Actual behavior

After the first refresh cycle, session.expiresAt equals now + cookieCache.maxAge. The corrupted value is both stored in the new JWE cookie (causing the cache to prematurely expire based on maxAge rather than real session lifetime) and returned in the JSON response.

Fix

Remove expiresAt: newExpiresAt from refreshedSession. The spread ...session.session already carries the real expiresAt, and setCookieCache sets the JWE exp independently:

// Before
const newExpiresAt = getDate(ctx.context.options.session?.cookieCache?.maxAge || 300, "sec");
const refreshedSession = {
  session: { ...session.session, expiresAt: newExpiresAt }, // remove override
  ...
};

// After
const refreshedSession = {
  session: { ...session.session }, // preserves real expiresAt
  ...
};
Originally created by @Lookwe69 on GitHub (Mar 25, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8770 ## Describe the bug When `cookieCache.refreshCache` is enabled (stateless mode — no `database` configured), the `get-session` endpoint overwrites `session.session.expiresAt` with `now + cookieCache.maxAge` instead of preserving the real session expiry. Every `getSession` call during a cache refresh returns a corrupted `session.expiresAt` (e.g. 10 minutes from now instead of 7 days). ## Root Cause In `src/api/routes/session.ts`, the stateless cache refresh path creates a `refreshedSession` that overrides `expiresAt` with the cookie cache TTL: ```ts const newExpiresAt = getDate(ctx.context.options.session?.cookieCache?.maxAge || 300, "sec"); const refreshedSession = { session: { ...session.session, expiresAt: newExpiresAt, // ← overwrites real session expiry (e.g. 7 days) with cache TTL (e.g. 10 min) }, user: session.user, updatedAt: Date.now(), }; await setCookieCache(ctx, refreshedSession, false); return ctx.json({ session: parsedRefreshedSession, ... }); // ↑ expiresAt = now + 10min, not the real 7-day session expiry ``` `setCookieCache` already derives the JWE cookie TTL independently from `authCookies.sessionData.attributes.maxAge` — `newExpiresAt` serves no other purpose here and only corrupts `session.session.expiresAt`. ## Steps to Reproduce 1- Configure better-auth without a `database` (stateless mode — memory adapter used automatically) 2- Enable `cookieCache` with `refreshCache: true`: ```ts const auth = betterAuth({ session: { cookieCache: { enabled: true, strategy: 'jwe', maxAge: 60, refreshCache: true, }, }, }); ``` 3- Sign in - observer `session.expiresAt` is correct (7 days from now) 4- Wait for `updateAge` to pass 5- Call `getSession` again - the refresh path triggers 6- `session.expiresAt` is now `now + 60s` instead of `now + 7 days` ## Expected behavior `session.expiresAt` always reflects `session.expiresIn` (default 7 days). The cookie cache TTL is internal and must not leak into the public session object. ## Actual behavior After the first refresh cycle, `session.expiresAt` equals `now + cookieCache.maxAge`. The corrupted value is both stored in the new JWE cookie (causing the cache to prematurely expire based on `maxAge` rather than real session lifetime) and returned in the JSON response. ## Fix Remove `expiresAt: newExpiresAt` from `refreshedSession`. The spread `...session.session` already carries the real `expiresAt`, and `setCookieCache` sets the JWE `exp` independently: ```ts // Before const newExpiresAt = getDate(ctx.context.options.session?.cookieCache?.maxAge || 300, "sec"); const refreshedSession = { session: { ...session.session, expiresAt: newExpiresAt }, // remove override ... }; // After const refreshedSession = { session: { ...session.session }, // preserves real expiresAt ... }; ```
GiteaMirror added the corebug labels 2026-04-17 19:57:56 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28509