// csrf-token.js // Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved 'use strict'; import dayjs from 'dayjs'; import mongoose from 'mongoose'; import { v4 as uuidv4 } from 'uuid'; const CsrfToken = mongoose.model('CsrfToken'); import { SiteService, SiteError } from '../../lib/site-lib.js'; export default class CsrfTokenService extends SiteService { static get name ( ) { return 'CsrfTokenService'; } static get slug ( ) { return 'csrfToken'; } constructor (dtp) { super(dtp, CsrfTokenService); } middleware (options) { options = Object.assign({ allowReuse: false }, options); return async (req, res, next) => { const requestToken = req.body[`csrf-token-${options.name}`]; if (!requestToken) { this.log.error('missing CSRF token', { options }); return next(new Error('Must include valid CSRF token')); } const token = await CsrfToken.findOne({ token: requestToken }); if (!token) { return next(new Error('CSRF request token is invalid')); } if (token.ip !== req.ip) { return next(new Error('CSRF request token client mismatch')); } if (!options.allowReuse && token.claimed) { return next(new SiteError(403, 'This request cannot be accepted. Please re-load the form and try again.')); } if (token.user) { if (!req.user) { return next(new Error('Must be logged in')); } if (!token.user.equals(req.user._id)) { return next(new Error('CSRF request token user mismatch')); } } await CsrfToken.updateOne( { _id: token._id }, { $set: { claimed: new Date() } }, ); return next(); }; } async create (req, options) { options = Object.assign({ expiresMinutes: 30, }, options); const now = new Date(); let csrfToken = await CsrfToken.create({ created: now, expires: dayjs(now).add(options.expiresMinutes, 'minute').toDate(), user: req.user ? req.user._id : null, ip: req.ip, token: uuidv4(), }); csrfToken = csrfToken.toObject(); csrfToken.name = `csrf-token-${options.name}`; return csrfToken; } }