Browse Source

fix(auth): prevent session rotation on expired sessions

ding113 1 week ago
parent
commit
423dcd5e4d

+ 8 - 2
src/app/api/auth/login/route.ts

@@ -233,10 +233,16 @@ export async function POST(request: NextRequest) {
         const opaqueSession = await createOpaqueSession(key, session);
         await setAuthCookie(opaqueSession.sessionId);
       } catch (error) {
-        logger.error("Failed to create opaque session, falling back to legacy cookie", {
+        logger.error("Failed to create opaque session in opaque mode", {
           error: error instanceof Error ? error.message : String(error),
         });
-        await setAuthCookie(key);
+        const serverError = t?.("serverError") ?? "Internal server error";
+        return withAuthResponseHeaders(
+          NextResponse.json(
+            { error: serverError, errorCode: "SESSION_CREATE_FAILED" },
+            { status: 503 }
+          )
+        );
       }
     }
 

+ 13 - 2
src/app/api/auth/logout/route.ts

@@ -5,7 +5,6 @@ import {
   getSessionTokenMode,
   type SessionTokenMode,
 } from "@/lib/auth";
-import { RedisSessionStore } from "@/lib/auth-session-store/redis-session-store";
 import { logger } from "@/lib/logger";
 import { withAuthResponseHeaders } from "@/lib/security/auth-response-headers";
 import { createCsrfOriginGuard } from "@/lib/security/csrf-origin-guard";
@@ -16,6 +15,18 @@ const csrfGuard = createCsrfOriginGuard({
   enforceInDevelopment: process.env.VITEST === "true",
 });
 
+let sessionStoreInstance:
+  | import("@/lib/auth-session-store/redis-session-store").RedisSessionStore
+  | null = null;
+
+async function getLogoutSessionStore() {
+  if (!sessionStoreInstance) {
+    const { RedisSessionStore } = await import("@/lib/auth-session-store/redis-session-store");
+    sessionStoreInstance = new RedisSessionStore();
+  }
+  return sessionStoreInstance;
+}
+
 function resolveSessionTokenMode(): SessionTokenMode {
   try {
     return getSessionTokenMode();
@@ -46,7 +57,7 @@ export async function POST(request: NextRequest) {
     try {
       const sessionId = await resolveAuthCookieToken();
       if (sessionId) {
-        const store = new RedisSessionStore();
+        const store = await getLogoutSessionStore();
         await store.revoke(sessionId);
       }
     } catch (error) {

+ 11 - 1
src/lib/auth-session-store/redis-session-store.ts

@@ -59,12 +59,15 @@ function parseSessionData(raw: string): SessionData | null {
   }
 }
 
-function resolveRotateTtlSeconds(expiresAt: number): number {
+function resolveRotateTtlSeconds(expiresAt: number): number | null {
   if (!Number.isFinite(expiresAt) || typeof expiresAt !== "number") {
     return DEFAULT_SESSION_TTL;
   }
 
   const remainingMs = expiresAt - Date.now();
+  if (remainingMs <= 0) {
+    return null;
+  }
   return Math.max(MIN_TTL_SECONDS, Math.ceil(remainingMs / 1000));
 }
 
@@ -181,6 +184,13 @@ export class RedisSessionStore implements SessionStore {
     }
 
     const ttlSeconds = resolveRotateTtlSeconds(oldSession.expiresAt);
+    if (ttlSeconds === null) {
+      logger.warn("[AuthSessionStore] Cannot rotate expired session", {
+        sessionId: oldSessionId,
+        expiresAt: oldSession.expiresAt,
+      });
+      return null;
+    }
     let nextSession: SessionData;
     try {
       nextSession = await this.create(

+ 7 - 0
src/lib/auth.ts

@@ -270,6 +270,13 @@ export async function validateAuthToken(
       const sessionStore = await getSessionStore();
       const sessionData = await sessionStore.read(token);
       if (sessionData) {
+        if (sessionData.expiresAt <= Date.now()) {
+          logger.warn("Opaque session expired (application-level check)", {
+            sessionId: sessionData.sessionId,
+            expiresAt: sessionData.expiresAt,
+          });
+          return null;
+        }
         return convertToAuthSession(sessionData, options);
       }
     } catch (error) {