feat(security): add CSRF protection for all state-changing API routes
Build & Push Docker Image / build (push) Successful in 2m16s
Build & Push Docker Image / build (push) Successful in 2m16s
- Synchronizer token stored in session; GET /auth/csrf to obtain it - csrfProtection middleware requires X-CSRF-Token on non-safe methods - customFetch injects the header via setCsrfTokenGetter - toolrate boot loads token; reload after local login (session regenerate) - OpenAPI GET /auth/csrf + CsrfToken schema, orval regenerated
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import crypto from "node:crypto";
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
|
||||
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
||||
|
||||
export function getCsrfToken(req: Request): string {
|
||||
if (!req.session.csrfToken) {
|
||||
req.session.csrfToken = crypto.randomBytes(24).toString("hex");
|
||||
}
|
||||
return req.session.csrfToken;
|
||||
}
|
||||
|
||||
export function csrfProtection(req: Request, res: Response, next: NextFunction): void {
|
||||
if (SAFE_METHODS.has(req.method.toUpperCase())) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const provided = req.headers["x-csrf-token"];
|
||||
const token = getCsrfToken(req);
|
||||
if (typeof provided === "string" && provided && provided === token) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
res.status(403).json({ error: "CSRF token missing or invalid" });
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { logger } from "../lib/logger";
|
||||
import { writeAuditLog } from "../lib/audit";
|
||||
import { loginRateLimit, passwordRateLimit } from "../lib/rate-limit";
|
||||
import { getEntitlements, requireFeature } from "../middleware/feature";
|
||||
import { getCsrfToken } from "../middleware/csrf";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
@@ -109,6 +110,10 @@ router.get("/auth/mode", (_req, res): void => {
|
||||
res.json({ mode: isOidcConfigured() ? "oidc" : "local" });
|
||||
});
|
||||
|
||||
router.get("/auth/csrf", (req, res): void => {
|
||||
res.json({ token: getCsrfToken(req) });
|
||||
});
|
||||
|
||||
router.post("/auth/login", loginRateLimit, async (req, res): Promise<void> => {
|
||||
if (isOidcConfigured()) {
|
||||
res.status(400).json({ error: "Use OIDC login when Keycloak is configured." });
|
||||
|
||||
@@ -8,9 +8,12 @@ import usersRouter from "./users";
|
||||
import auditRouter from "./audit";
|
||||
import costsRouter from "./costs";
|
||||
import adminRouter from "./admin";
|
||||
import { csrfProtection } from "../middleware/csrf";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.use(csrfProtection);
|
||||
|
||||
router.use(authRouter);
|
||||
router.use(healthRouter);
|
||||
router.use(toolsRouter);
|
||||
|
||||
+1
@@ -14,5 +14,6 @@ declare module "express-session" {
|
||||
codeVerifier?: string;
|
||||
returnTo?: string;
|
||||
oidcState?: string;
|
||||
csrfToken?: string;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user