fix(auth/middleware): recognize chunked session cookies

NextAuth v5 chunks the session cookie when the JWT payload exceeds
~4KB (we hit this easily: keycloakId + tenantId + display names +
roles + accessToken JWT + idToken). When chunked, the bare
'authjs.session-token' cookie is removed in favour of
'authjs.session-token.0', '.1', etc. Looking up only the bare name
returned undefined and the middleware redirected freshly-logged-in
users back to /api/auth/signin in a loop.

Match presence-only on any cookie whose name starts with either
canonical prefix. Server-side NextAuth still validates the token on
every RSC render — this check only gates the redirect.

Repro: gscCRM/v2.3.3 with the proxy fix in place. Keycloak auth
completes, /api/auth/callback/keycloak 302s, but every subsequent
request to / 307s straight back to signin because the middleware
doesn't see the now-chunked cookie.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-05-11 11:46:01 +02:00
parent 360b611ae6
commit 387e10b2fb

View File

@@ -9,6 +9,7 @@ interface NextRequestLike {
nextUrl: URL; nextUrl: URL;
cookies: { cookies: {
get(name: string): { value: string } | undefined; get(name: string): { value: string } | undefined;
getAll(): Array<{ name: string; value: string }>;
}; };
} }
@@ -62,10 +63,7 @@ export function createAuthMiddleware(opts: AuthMiddlewareOptions = {}) {
return NextResponse.next(); return NextResponse.next();
} }
const sessionCookie = if (hasSessionCookie(req)) {
req.cookies.get("authjs.session-token") ??
req.cookies.get("__Secure-authjs.session-token");
if (sessionCookie) {
return NextResponse.next(); return NextResponse.next();
} }
@@ -75,6 +73,32 @@ export function createAuthMiddleware(opts: AuthMiddlewareOptions = {}) {
}; };
} }
/**
* NextAuth v5 chunks the session cookie when the JWT payload exceeds
* ~4 KB (claims + accessToken + roles add up fast). When chunked, the
* bare `authjs.session-token` cookie is *removed* in favour of
* `authjs.session-token.0`, `.1`, etc. Looking up only the bare name
* misses the chunked form and the middleware loops the user back to
* signin even though they have a valid session.
*
* Match presence-only on any cookie whose name starts with either
* canonical prefix. Token validity is still verified server-side by
* NextAuth on every RSC render — this check only gates the redirect.
*/
function hasSessionCookie(req: NextRequestLike): boolean {
for (const c of req.cookies.getAll()) {
if (
c.name === "authjs.session-token" ||
c.name === "__Secure-authjs.session-token" ||
c.name.startsWith("authjs.session-token.") ||
c.name.startsWith("__Secure-authjs.session-token.")
) {
return true;
}
}
return false;
}
function isAlwaysAllowed(pathname: string): boolean { function isAlwaysAllowed(pathname: string): boolean {
return ( return (
pathname.startsWith("/api/auth/") || pathname.startsWith("/api/auth/") ||