/** * CSRF Guard * * Implements CSRF protection using double-submit cookie pattern with session binding. * Validates that: * 1. CSRF token in cookie matches token in header * 2. Token HMAC is valid for the current user session * * Usage: * - Apply to controllers handling state-changing operations * - Use @SkipCsrf() decorator to exempt specific endpoints * - Safe methods (GET, HEAD, OPTIONS) are automatically exempted */ import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Logger, } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { Request } from "express"; import { CsrfService } from "../services/csrf.service"; import type { AuthenticatedUser } from "../types/user.types"; export const SKIP_CSRF_KEY = "skipCsrf"; interface RequestWithUser extends Request { user?: AuthenticatedUser; } @Injectable() export class CsrfGuard implements CanActivate { private readonly logger = new Logger(CsrfGuard.name); constructor( private reflector: Reflector, private csrfService: CsrfService ) {} canActivate(context: ExecutionContext): boolean { // Check if endpoint is marked to skip CSRF const skipCsrf = this.reflector.getAllAndOverride(SKIP_CSRF_KEY, [ context.getHandler(), context.getClass(), ]); if (skipCsrf) { return true; } const request = context.switchToHttp().getRequest(); // Exempt safe HTTP methods (GET, HEAD, OPTIONS) if (["GET", "HEAD", "OPTIONS"].includes(request.method)) { return true; } // Get CSRF token from cookie and header const cookies = request.cookies as Record | undefined; const cookieToken = cookies?.["csrf-token"]; const headerToken = request.headers["x-csrf-token"] as string | undefined; // Validate tokens exist and match if (!cookieToken || !headerToken) { this.logger.warn({ event: "CSRF_TOKEN_MISSING", method: request.method, path: request.path, hasCookie: !!cookieToken, hasHeader: !!headerToken, securityEvent: true, timestamp: new Date().toISOString(), }); throw new ForbiddenException("CSRF token missing"); } if (cookieToken !== headerToken) { this.logger.warn({ event: "CSRF_TOKEN_MISMATCH", method: request.method, path: request.path, securityEvent: true, timestamp: new Date().toISOString(), }); throw new ForbiddenException("CSRF token mismatch"); } // Validate session binding via HMAC const userId = request.user?.id; if (!userId) { this.logger.warn({ event: "CSRF_NO_USER_CONTEXT", method: request.method, path: request.path, securityEvent: true, timestamp: new Date().toISOString(), }); throw new ForbiddenException("CSRF validation requires authentication"); } if (!this.csrfService.validateToken(cookieToken, userId)) { this.logger.warn({ event: "CSRF_SESSION_BINDING_INVALID", method: request.method, path: request.path, securityEvent: true, timestamp: new Date().toISOString(), }); throw new ForbiddenException("CSRF token not bound to session"); } return true; } }