// app/services/csrf-token.ts // Copyright (C) 2026 DTP Technologies, LLC // All Rights Reserved import { Request, Response, NextFunction, RequestHandler } from "express"; import { v4 as uuidv4 } from "uuid"; import dayjs from "dayjs"; import CsrfToken, { ICsrfToken } from "../models/csrf-token.js"; import { DtpService } from "../lib/service.js"; export interface CsrfTokenOptions { name: string; expiresMinutes: number; allowReuse: boolean; } export class CsrfTokenService extends DtpService { get name(): string { return "CsrfTokenService"; } get slug(): string { return "csrfToken"; } constructor() { super(); } async start(): Promise { this.log.info("service started"); } async stop(): Promise { this.log.info("service stopped"); } middleware(options: CsrfTokenOptions): RequestHandler { return async ( req: Request, _res: Response, next: NextFunction, ): Promise => { const requestToken = req.body[`csrf-token-${options.name}`]; if (!requestToken) { this.log.error("missing CSRF token", { name: options.name }); const error = new Error("Must include valid CSRF token"); error.statusCode = 401; return next(error); } const token = await CsrfToken.findOne({ token: requestToken }); if (!token) { const error = new Error("CSRF request token is invalid"); error.statusCode = 401; return next(error); } if (token.ip !== req.ip) { const error = new Error("CSRF request token client mismatch"); error.statusCode = 401; return next(error); } if (token.claimedAt && !options.allowReuse) { const error = new Error( "Your request can't be accepted. Please refresh the page and try again.", ); error.statusCode = 401; return next(error); } if (token.user) { if (!req.user) { const error = new Error("Must be logged in"); error.statusCode = 401; return next(error); } if (!token.user._id.equals(req.user._id)) { const error = new Error("CSRF request token user mismatch"); error.statusCode = 401; return next(error); } } await CsrfToken.updateOne( { _id: token._id }, { $set: { claimed: new Date() } }, ); return next(); }; } async create(req: Request, options: CsrfTokenOptions): Promise { const NOW = new Date(); options = Object.assign( { expiresMinutes: 30, }, options, ); if (options.expiresMinutes > 120) { const error = new Error("CSRF tokens have a max lifespan of 120 minutes"); error.statusCode = 400; throw error; } const token = new CsrfToken(); token.name = `csrf-token-${options.name}`; token.createdAt = NOW; token.expiresAt = dayjs(NOW).add(options.expiresMinutes, "minute").toDate(); if (req.user) { token.user = req.user._id; } if (req.ip) { token.ip = req.ip; } token.token = uuidv4(); await token.save(); return token.toObject(); } } export default new CsrfTokenService();