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:
@@ -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/") ||
|
||||||
|
|||||||
Reference in New Issue
Block a user