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:
@@ -2,6 +2,7 @@ import { createTRPCContext, loadRoleDefaults } from "@capakraken/api";
|
||||
import { appRouter } from "@capakraken/api/router";
|
||||
import { prisma } from "@capakraken/db";
|
||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { auth } from "~/server/auth.js";
|
||||
|
||||
@@ -42,9 +43,19 @@ const handler = async (req: NextRequest) => {
|
||||
// Sessions kicked by concurrent-session limits or manual logout are rejected immediately.
|
||||
// Fail-open: if the table doesn't exist yet (pending migration) the check is skipped.
|
||||
// In E2E test mode the jwt callback skips registration, so skip validation too.
|
||||
//
|
||||
// We decode the JWT directly (not session.user.jti) because the session
|
||||
// token is client-visible and therefore must not carry internal
|
||||
// session-revocation identifiers — see security ticket #41.
|
||||
const isE2eTestMode = process.env["E2E_TEST_MODE"] === "true";
|
||||
if (session?.user && !isE2eTestMode) {
|
||||
const jti = (session.user as typeof session.user & { jti?: string }).jti;
|
||||
const secret = process.env["AUTH_SECRET"] ?? process.env["NEXTAUTH_SECRET"] ?? "";
|
||||
const cookieName =
|
||||
(process.env["AUTH_URL"] ?? "").startsWith("https://") || process.env["VERCEL"] === "1"
|
||||
? "__Host-authjs.session-token"
|
||||
: "authjs.session-token";
|
||||
const jwt = secret ? await getToken({ req, secret, salt: cookieName }) : null;
|
||||
const jti = (jwt?.["sid"] as string | undefined) ?? undefined;
|
||||
if (jti) {
|
||||
try {
|
||||
const activeSession = await prisma.activeSession.findUnique({ where: { jti } });
|
||||
|
||||
Reference in New Issue
Block a user