feat: enhance session security and freshness
All checks were successful
Build and Push Backend Image / build (push) Successful in 43s
Build and Push Frontend Image / build (push) Successful in 4m15s

- Sliding expiration: reset 24h TTL on every Redis session access
- SameSite=Strict on login and logout cookies (was Lax)
- Secure flag on logout cookie in production (was missing)
- Re-fetch user from DB on every request in buildContext so role/avatar/
  admin changes take effect immediately without requiring re-login

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 11:10:01 +01:00
parent bff354094e
commit dcf2fbd3d4
3 changed files with 32 additions and 4 deletions

View File

@@ -1,9 +1,11 @@
import type { YogaInitialContext } from "graphql-yoga"; import type { YogaInitialContext } from "graphql-yoga";
import type { FastifyRequest, FastifyReply } from "fastify"; import type { FastifyRequest, FastifyReply } from "fastify";
import type { Context } from "./builder"; import type { Context } from "./builder";
import { getSession } from "../lib/auth"; import { getSession, setSession } from "../lib/auth";
import { db } from "../db/connection"; import { db } from "../db/connection";
import { redis } from "../lib/auth"; import { redis } from "../lib/auth";
import { users } from "../db/schema/index";
import { eq } from "drizzle-orm";
type ServerContext = { type ServerContext = {
req: FastifyRequest; req: FastifyRequest;
@@ -25,7 +27,30 @@ export async function buildContext(ctx: YogaInitialContext & ServerContext): Pro
); );
const token = cookies["session_token"]; const token = cookies["session_token"];
const currentUser = token ? await getSession(token) : null; let currentUser = null;
if (token) {
const session = await getSession(token); // also slides TTL
if (session) {
const dbInstance = ctx.db || db;
const [dbUser] = await dbInstance.select().from(users).where(eq(users.id, session.id)).limit(1);
if (dbUser) {
currentUser = {
id: dbUser.id,
email: dbUser.email,
role: (dbUser.role === "admin" ? "viewer" : dbUser.role) as "model" | "viewer",
is_admin: dbUser.is_admin,
first_name: dbUser.first_name,
last_name: dbUser.last_name,
artist_name: dbUser.artist_name,
slug: dbUser.slug,
avatar: dbUser.avatar,
};
// Refresh cached session with up-to-date data
await setSession(token, currentUser);
}
}
}
return { return {
db: ctx.db || db, db: ctx.db || db,

View File

@@ -45,7 +45,7 @@ builder.mutationField("login", (t) =>
// Set session cookie // Set session cookie
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
const cookieValue = `session_token=${token}; HttpOnly; Path=/; SameSite=Lax; Max-Age=86400${isProduction ? "; Secure" : ""}`; const cookieValue = `session_token=${token}; HttpOnly; Path=/; SameSite=Strict; Max-Age=86400${isProduction ? "; Secure" : ""}`;
(ctx.reply as any).header?.("Set-Cookie", cookieValue); (ctx.reply as any).header?.("Set-Cookie", cookieValue);
// For graphql-yoga response // For graphql-yoga response
@@ -74,7 +74,8 @@ builder.mutationField("logout", (t) =>
await deleteSession(token); await deleteSession(token);
} }
// Clear cookie // Clear cookie
const cookieValue = "session_token=; HttpOnly; Path=/; Max-Age=0"; const isProduction = process.env.NODE_ENV === "production";
const cookieValue = `session_token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0${isProduction ? "; Secure" : ""}`;
(ctx.reply as any).header?.("Set-Cookie", cookieValue); (ctx.reply as any).header?.("Set-Cookie", cookieValue);
return true; return true;
}, },

View File

@@ -21,6 +21,8 @@ export async function setSession(token: string, user: SessionUser): Promise<void
export async function getSession(token: string): Promise<SessionUser | null> { export async function getSession(token: string): Promise<SessionUser | null> {
const data = await redis.get(`session:${token}`); const data = await redis.get(`session:${token}`);
if (!data) return null; if (!data) return null;
// Slide the expiration window on every access
await redis.expire(`session:${token}`, 86400);
return JSON.parse(data) as SessionUser; return JSON.parse(data) as SessionUser;
} }