diff --git a/.changeset/sliding-auth-cookie.md b/.changeset/sliding-auth-cookie.md new file mode 100644 index 00000000..205fe62e --- /dev/null +++ b/.changeset/sliding-auth-cookie.md @@ -0,0 +1,13 @@ +--- +"aicodeman": patch +--- + +fix(auth): slide the session cookie so active users aren't logged out + +Re-issue the `codeman_session` cookie on every authenticated request so the +browser cookie lifetime tracks the server-side sliding TTL (the session store +already uses `refreshOnGet`). Previously the cookie was only set on the Basic +Auth path with a fixed 24h lifetime from login, so the browser dropped it +mid-use; the next request arrived cookie-less, fell through to Basic Auth and +popped the native username/password dialog — perceived as a random logout while +actively working. diff --git a/src/web/middleware/auth.ts b/src/web/middleware/auth.ts index 56dde1d7..5387a280 100644 --- a/src/web/middleware/auth.ts +++ b/src/web/middleware/auth.ts @@ -151,6 +151,19 @@ export function registerAuthMiddleware(app: FastifyInstance, https: boolean): Au // Use get() instead of has() so refreshOnGet extends the TTL on active sessions const sessionToken = req.cookies[AUTH_COOKIE_NAME]; if (sessionToken && authSessions.get(sessionToken) !== undefined) { + // Sliding cookie: re-issue on every authenticated request so the browser + // cookie lifetime tracks the server-side sliding TTL (refreshOnGet above). + // Without this the cookie has a fixed lifetime from login; the browser + // drops it mid-use, the next request arrives cookie-less and falls through + // to Basic Auth — popping the native username/password dialog, which reads + // as a random logout while actively working. + reply.setCookie(AUTH_COOKIE_NAME, sessionToken, { + httpOnly: true, + secure: https, + sameSite: 'lax', + maxAge: AUTH_SESSION_TTL_MS / 1000, // seconds + path: '/', + }); done(); return; }