security: cookie + session hardening (#41)
Three related fixes: - Cookie secure flag now tracks AUTH_URL scheme (https → Secure), not NODE_ENV — staging over HTTPS with NODE_ENV!=production used to ship Set-Cookie without Secure. Cookie name gains __Host- prefix when Secure is on. - jwt() callback no longer swallows session-registry write failures; concurrent-session cap is now fail-closed. - Session callback no longer copies token.sid onto session.user.jti. The tRPC route handler reads the JTI directly from the encrypted JWT via getToken() so it stays server-side. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -267,10 +267,9 @@ const config = {
|
||||
if (token.role) {
|
||||
(session.user as typeof session.user & { role: string }).role = token.role as string;
|
||||
}
|
||||
// Use token.sid (not token.jti) to avoid conflict with Auth.js's internal JWT ID claim
|
||||
if (token.sid) {
|
||||
(session.user as typeof session.user & { jti: string }).jti = token.sid as string;
|
||||
}
|
||||
// Do NOT expose token.sid on session.user — the JTI is an internal
|
||||
// session-revocation token and must stay inside the encrypted JWT.
|
||||
// Server-side handlers that need it decode the JWT via getToken().
|
||||
return session;
|
||||
},
|
||||
async jwt({ token, user }) {
|
||||
@@ -289,7 +288,11 @@ const config = {
|
||||
const isE2eTestMode = process.env["E2E_TEST_MODE"] === "true";
|
||||
if (isE2eTestMode) return token;
|
||||
|
||||
// Enforce concurrent session limit (kick-oldest strategy)
|
||||
// Enforce concurrent session limit (kick-oldest strategy).
|
||||
// This MUST fail-closed: if session-registry writes fail we cannot
|
||||
// honour the configured session cap, so we must refuse to mint a
|
||||
// session. Previously this path swallowed errors and logged-only,
|
||||
// which let a DB-degradation scenario bypass the session cap.
|
||||
try {
|
||||
const settings = await prisma.systemSettings.findUnique({
|
||||
where: { id: "singleton" },
|
||||
@@ -297,12 +300,10 @@ const config = {
|
||||
});
|
||||
const maxSessions = settings?.maxConcurrentSessions ?? 3;
|
||||
|
||||
// Register this new session
|
||||
await prisma.activeSession.create({
|
||||
data: { userId: user.id!, jti },
|
||||
});
|
||||
|
||||
// Count active sessions and delete the oldest if over the limit
|
||||
const activeSessions = await prisma.activeSession.findMany({
|
||||
where: { userId: user.id! },
|
||||
orderBy: { createdAt: "asc" },
|
||||
@@ -320,8 +321,11 @@ const config = {
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
// Non-blocking: don't prevent login if session tracking fails
|
||||
logger.error({ err }, "Failed to enforce concurrent session limit");
|
||||
logger.error(
|
||||
{ err, userId: user.id },
|
||||
"Failed to register active session — refusing to mint JWT",
|
||||
);
|
||||
throw new Error("Session registration failed");
|
||||
}
|
||||
}
|
||||
return token;
|
||||
|
||||
Reference in New Issue
Block a user