Import source: backend/src/middleware/session
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
import { randomBytes } from 'node:crypto';
|
||||||
|
import type { Request, RequestHandler } from 'express';
|
||||||
|
import { sha256 } from '../utils/crypto.js';
|
||||||
|
import { env } from '../config/env.js';
|
||||||
|
import { store } from '../services/store.js';
|
||||||
|
import type { DatabaseShape } from '../types.js';
|
||||||
|
|
||||||
|
export const SESSION_COOKIE = 'cshare_sid';
|
||||||
|
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
||||||
|
const ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000; // Rotate session once a day
|
||||||
|
|
||||||
|
// Map of sessionId -> { clientHash: string, expiresAt: number }
|
||||||
|
const sessionCache = new Map<string, { clientHash: string, expiresAt: number }>();
|
||||||
|
|
||||||
|
function getClientFingerprint(req: Request): string {
|
||||||
|
const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown';
|
||||||
|
const ua = req.headers['user-agent'] as string ?? 'unknown';
|
||||||
|
return sha256(`${ip}:${ua}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Issues a long-lived HTTP-only, SameSite=Strict secure session cookie on first visit, bound to client IP/UA, and rotates periodically. */
|
||||||
|
export const sessionMiddleware: RequestHandler = (req, res, next) => {
|
||||||
|
const currentSid = req.cookies?.[SESSION_COOKIE];
|
||||||
|
const clientHash = getClientFingerprint(req);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!currentSid) {
|
||||||
|
const sid = randomBytes(32).toString('hex');
|
||||||
|
res.cookie(SESSION_COOKIE, sid, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: env.nodeEnv === 'production',
|
||||||
|
maxAge: ONE_YEAR_MS,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
req.cookies[SESSION_COOKIE] = sid;
|
||||||
|
sessionCache.set(sid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS });
|
||||||
|
} else {
|
||||||
|
if (!sessionCache.has(currentSid)) {
|
||||||
|
sessionCache.set(currentSid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS });
|
||||||
|
}
|
||||||
|
const cached = sessionCache.get(currentSid)!;
|
||||||
|
// Bind session to the client's fingerprint to prevent cookie hijacking
|
||||||
|
if (cached.clientHash !== clientHash) {
|
||||||
|
// Fingerprint mismatch — clear cookie, force new session
|
||||||
|
res.clearCookie(SESSION_COOKIE);
|
||||||
|
const sid = randomBytes(32).toString('hex');
|
||||||
|
res.cookie(SESSION_COOKIE, sid, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: env.nodeEnv === 'production',
|
||||||
|
maxAge: ONE_YEAR_MS,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
req.cookies[SESSION_COOKIE] = sid;
|
||||||
|
sessionCache.set(sid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS });
|
||||||
|
} else if (now > cached.expiresAt) {
|
||||||
|
// Periodic rotation logic: rotate the session cookie value to limit lifetime of any stolen session id
|
||||||
|
sessionCache.delete(currentSid);
|
||||||
|
const newSid = randomBytes(32).toString('hex');
|
||||||
|
res.cookie(SESSION_COOKIE, newSid, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: env.nodeEnv === 'production',
|
||||||
|
maxAge: ONE_YEAR_MS,
|
||||||
|
path: '/',
|
||||||
|
});
|
||||||
|
req.cookies[SESSION_COOKIE] = newSid;
|
||||||
|
sessionCache.set(newSid, { clientHash, expiresAt: now + ROTATION_INTERVAL_MS });
|
||||||
|
|
||||||
|
// Migrate file ownership to the new session ID so uploads survive rotation.
|
||||||
|
store.update((db: DatabaseShape) => {
|
||||||
|
for (const file of db.files) {
|
||||||
|
if (file.sessionId === currentSid) {
|
||||||
|
file.sessionId = newSid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getSessionId(req: Request): string {
|
||||||
|
return req.cookies?.[SESSION_COOKIE] ?? '';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user