feat: enhance session security and freshness
- 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:
@@ -1,9 +1,11 @@
|
||||
import type { YogaInitialContext } from "graphql-yoga";
|
||||
import type { FastifyRequest, FastifyReply } from "fastify";
|
||||
import type { Context } from "./builder";
|
||||
import { getSession } from "../lib/auth";
|
||||
import { getSession, setSession } from "../lib/auth";
|
||||
import { db } from "../db/connection";
|
||||
import { redis } from "../lib/auth";
|
||||
import { users } from "../db/schema/index";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
type ServerContext = {
|
||||
req: FastifyRequest;
|
||||
@@ -25,7 +27,30 @@ export async function buildContext(ctx: YogaInitialContext & ServerContext): Pro
|
||||
);
|
||||
|
||||
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 {
|
||||
db: ctx.db || db,
|
||||
|
||||
@@ -45,7 +45,7 @@ builder.mutationField("login", (t) =>
|
||||
|
||||
// Set session cookie
|
||||
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);
|
||||
|
||||
// For graphql-yoga response
|
||||
@@ -74,7 +74,8 @@ builder.mutationField("logout", (t) =>
|
||||
await deleteSession(token);
|
||||
}
|
||||
// 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);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -21,6 +21,8 @@ export async function setSession(token: string, user: SessionUser): Promise<void
|
||||
export async function getSession(token: string): Promise<SessionUser | null> {
|
||||
const data = await redis.get(`session:${token}`);
|
||||
if (!data) return null;
|
||||
// Slide the expiration window on every access
|
||||
await redis.expire(`session:${token}`, 86400);
|
||||
return JSON.parse(data) as SessionUser;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user