Files
goodgo-platform/apps/api/src/modules/shared/infrastructure/middleware/csrf.middleware.ts
Ho Ngoc Hai 1ebdc5f0b3 fix: auth cookies cross-origin, async params, CSRF/web-vitals errors
- Set SameSite=lax for auth & CSRF cookies in development (cross-port)
- Set refresh_token cookie path to / (was /auth, preventing cross-port send)
- Await params in Next.js 15 async server components (layout, listings, agents)
- Add CSRF token to web-vitals POST requests
- Fix: 401 Unauthorized on all authenticated API calls from web app
- Fix: CSRF token missing on POST requests from different port
- Fix: params.locale sync access warning in generateMetadata

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
2026-04-13 11:24:45 +07:00

50 lines
1.6 KiB
TypeScript

import { randomBytes } from 'node:crypto';
import { ForbiddenException, Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
const CSRF_COOKIE = 'XSRF-TOKEN';
const CSRF_HEADER = 'x-csrf-token';
const TOKEN_LENGTH = 32;
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
@Injectable()
export class CsrfMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
// Safe methods: ensure a CSRF cookie exists for the client to read
if (SAFE_METHODS.has(req.method)) {
this.ensureCsrfCookie(req, res);
return next();
}
// State-changing methods: validate the double-submit token
const cookieToken = req.cookies?.[CSRF_COOKIE] as string | undefined;
const headerToken = req.headers[CSRF_HEADER] as string | undefined;
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
throw new ForbiddenException('CSRF token missing or invalid');
}
// Rotate token after successful validation
this.setCsrfCookie(res);
next();
}
private ensureCsrfCookie(req: Request, res: Response): void {
if (!req.cookies?.[CSRF_COOKIE]) {
this.setCsrfCookie(res);
}
}
private setCsrfCookie(res: Response): void {
const token = randomBytes(TOKEN_LENGTH).toString('hex');
const isProduction = process.env['NODE_ENV'] === 'production';
res.cookie(CSRF_COOKIE, token, {
httpOnly: false, // Frontend must read this cookie
secure: isProduction,
sameSite: isProduction ? 'strict' : 'lax',
path: '/',
});
}
}