gadget/docs/archive/services/csrf-token.ts

128 lines
3.2 KiB
TypeScript

// 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<void> {
this.log.info("service started");
}
async stop(): Promise<void> {
this.log.info("service stopped");
}
middleware(options: CsrfTokenOptions): RequestHandler {
return async (
req: Request,
_res: Response,
next: NextFunction,
): Promise<void> => {
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<ICsrfToken> {
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();