Files
stack/apps/api/src/common/controllers/csrf.controller.ts
Jason Woltje 72c64d2eeb
All checks were successful
ci/woodpecker/push/orchestrator Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
ci/woodpecker/push/web Pipeline was successful
fix(api): add global /api prefix to resolve frontend route mismatch (#507)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-02-26 01:13:48 +00:00

58 lines
1.8 KiB
TypeScript

/**
* CSRF Controller
*
* Provides CSRF token generation endpoint for client applications.
* Tokens are cryptographically bound to the user session via HMAC.
*/
import { Controller, Get, Res, Req, UseGuards } from "@nestjs/common";
import { Response, Request } from "express";
import { SkipCsrf } from "../decorators/skip-csrf.decorator";
import { CsrfService } from "../services/csrf.service";
import { AuthGuard } from "../../auth/guards/auth.guard";
import type { AuthenticatedUser } from "../types/user.types";
interface AuthenticatedRequest extends Request {
user?: AuthenticatedUser;
}
@Controller("v1/csrf")
export class CsrfController {
constructor(private readonly csrfService: CsrfService) {}
/**
* Generate and set CSRF token bound to user session
* Requires authentication to bind token to session
* Returns token to client and sets it in httpOnly cookie
*/
@Get("token")
@UseGuards(AuthGuard)
@SkipCsrf() // This endpoint itself doesn't need CSRF protection
getCsrfToken(
@Req() request: AuthenticatedRequest,
@Res({ passthrough: true }) response: Response
): { token: string } {
// Get user ID from authenticated request
const userId = request.user?.id;
if (!userId) {
// This should not happen if AuthGuard is working correctly
throw new Error("User ID not available after authentication");
}
// Generate session-bound CSRF token
const token = this.csrfService.generateToken(userId);
// Set token in httpOnly cookie
response.cookie("csrf-token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 24 * 60 * 60 * 1000, // 24 hours
});
// Return token to client (so it can include in X-CSRF-Token header)
return { token };
}
}