fix: exempt public unauthenticated endpoints from CSRF protection

The lusca CSRF implementation was breaking login and registration because
the frontend doesn't fetch or send CSRF tokens. This is a structural issue
that requires frontend implementation.

As a pragmatic fix, this commit exempts public unauthenticated endpoints
from CSRF protection:
- /api/login, /api/register, /api/verify-email
- /api/version, /api/registration-status, /api/health
- /api/oidc/* (all OIDC authentication endpoints)
- /api/feature-flags

Authenticated endpoints still require CSRF tokens via lusca.

Also updates csrf.js to use lusca's token generation mechanism, making
it compatible with the global lusca CSRF middleware.

TODO: Implement proper CSRF token handling in the frontend for enhanced
security on public endpoints.
This commit is contained in:
Chris Veleris 2026-04-13 13:05:33 +03:00
parent caffea977c
commit a89f2b72d9
2 changed files with 44 additions and 10 deletions

View file

@ -97,10 +97,28 @@ const lusca = require('lusca');
// Pre-check middleware to exempt test/Bearer requests before lusca runs
app.use((req, res, next) => {
// Public unauthenticated endpoints that should bypass CSRF
// These are routes that don't require authentication and are used during login/registration
const publicPaths = [
'/api/login',
'/api/register',
'/api/verify-email',
'/api/version',
'/api/registration-status',
'/api/health',
];
const isPublicPath = publicPaths.some((path) => req.path === path);
const isOidcPath = req.path.startsWith('/api/oidc/');
const isFeatureFlagsPath = req.path.startsWith('/api/feature-flags');
// Mark exempt requests so lusca wrapper can skip them
if (
process.env.NODE_ENV === 'test' ||
req.headers.authorization?.startsWith('Bearer ')
req.headers.authorization?.startsWith('Bearer ') ||
isPublicPath ||
isOidcPath ||
isFeatureFlagsPath
) {
req._csrfExempt = true;
}

View file

@ -1,11 +1,16 @@
const csurf = require('@dr.pogodin/csurf');
const lusca = require('lusca');
const csrfMiddleware = csurf({
cookie: false,
value: (req) => {
return req.headers['x-csrf-token'] || req.body?._csrf;
},
});
const csrfMiddleware = (req, res, next) => {
if (!req.session) {
return res.status(500).json({ error: 'Session not initialized' });
}
if (!req.session._csrf) {
req.session._csrf = require('crypto').randomBytes(16).toString('hex');
}
next();
};
const csrfProtection = (req, res, next) => {
if (
@ -16,11 +21,22 @@ const csrfProtection = (req, res, next) => {
return next();
}
return csrfMiddleware(req, res, next);
return lusca.csrf({
header: 'x-csrf-token',
cookie: false,
})(req, res, next);
};
const generateToken = (req) => {
return req.csrfToken ? req.csrfToken() : '';
if (!req.session) {
return '';
}
if (!req.session._csrf) {
req.session._csrf = require('crypto').randomBytes(16).toString('hex');
}
return req.session._csrf;
};
module.exports = {