Beginner NodeJS developer here. I’m creating a site in NodeJS using Express and EJS. I’ve just finished a Udemy course and trying it out now myself. DUring the course, I used csrf-sync and it worked fine. However, now that I’m implementing it in my own application, I am getting the “ForbiddenError: invalid csrf token” error when I submit a form. The full error is:
ForbiddenError: invalid csrf token
at csrfSync (file:///C:/Users/kram/Dropbox/wikibeerdia/wikibeerdia-frontend/node_modules/csrf-sync/lib/esm/index.js:9:35)
at file:///C:/Users/kram/Dropbox/wikibeerdia/wikibeerdia-frontend/app.js:28:40
at ModuleJob.run (node:internal/modules/esm/module_job:262:25)
at async ModuleLoader.import (node:internal/modules/esm/loader:475:24)
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:109:5) {
code: ‘EBADCSRFTOKEN’
}
I am at my wits end, as I have searched and googled and can’t get this working.
Below is my app.js file:
import path from 'path';
import * as url from 'url';
import express from 'express';
import bodyParser from 'body-parser';
import mongoose from 'mongoose';
import session from 'express-session';
import { default as connectMongoDBSession } from 'connect-mongodb-session';
import helmet from 'helmet';
import { csrfSync } from 'csrf-sync';
import { v4 as uuidv4 } from 'uuid';
import * as errorController from './controllers/error.js';
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const MongoDBStore = connectMongoDBSession(session);
const MONGODB_URI = `mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0.vuiwnxj.mongodb.net/${process.env.MONGO_DEFAULT_DATABASE}`;
const app = express();
const store = new MongoDBStore({
uri: MONGODB_URI,
collection: 'sessions'
});
const { csrfSynchronisedProtection } = csrfSync({
getTokenFromRequest: (req) => {
if (
req.is('multipart') ||
req.is('application/x-www-form-urlencoded')
) {
return req.body['CSRFToken'];
}
return req.headers['x-csrf-token'];
}
});
app.set('view engine', 'ejs');
app.set('views', 'views');
app.use(
helmet.contentSecurityPolicy({
useDefaults: true,
directives: {
'default-src': ["'self'"],
'script-src': ["'self'", "'unsafe-inline'", "'unsafe-hashes'"],
'script-src': ["'self'", "'unsafe-inline'", 'js.stripe.com'],
'style-src': [
"'self'",
"'unsafe-inline'",
'fonts.googleapis.com',
'kit-pro.fontawesome.com'
],
'frame-src': ["'self'", 'js.stripe.com'],
'font-src': [
"'self'",
'fonts.googleapis.com',
'fonts.gstatic.com',
'kit-pro.fontawesome.com'
]
}
})
);
import mainRoutes from './routes/main.js';
import authRoutes from './routes/auth.js';
app.use(bodyParser.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/images', express.static(path.join(__dirname, 'images')));
app.use(
session({
secret: process.env.SESSION_KEY,
resave: false,
saveUninitialized: false,
store: store
})
);
app.use(csrfSynchronisedProtection);
app.use((req, res, next) => {
['log', 'warn'].forEach(function (method) {
var old = console[method];
console[method] = function () {
var stack = new Error().stack.split(/n/);
// Chrome includes a single "Error" line, FF doesn't.
if (stack[0].indexOf('Error') === 0) {
stack = stack.slice(1);
}
var args = [].slice.apply(arguments).concat([stack[1].trim()]);
return old.apply(console, args);
};
});
res.locals.isAuthenticated = req.session.isLoggedIn;
res.locals.csrfToken = req.csrfToken(true);
next();
});
app.use(mainRoutes);
app.use(authRoutes);
app.get('/500', errorController.get500);
app.use(errorController.get404);
app.use((error, req, res, next) => {
console.log(error);
res.status(500).render('500');
});
try {
await mongoose.connect(MONGODB_URI);
app.listen(process.env.PORT || 3000);
} catch (err) {
console.log(err);
}
My form that is submitting is:
<form action="/signup" method="POST" novalidate>
<div class="single_input">
<label>Name</label>
<div class="row">
<div class="col">
<input type="text" name="firstName" id="firstName" placeholder="First Name" class="<%= validationErrors.find( e => e.path === 'firstName') ? 'invalid' : '' %>" value="<%= oldInput.firstName %>">
<% if (validationErrors.find( e => e.path === 'firstName')) { %>
<p class="error_text"><i class="fas fa-exclamation-triangle"></i> Please enter a first name</p>
<% } %>
</div>
<div class="col">
<input type="text" name="lastName" id="lastName" placeholder="Last Name" class="<%= validationErrors.find( e => e.path === 'lastName') ? 'invalid' : '' %>" value="<%= oldInput.lastName %>">
<% if (validationErrors.find( e => e.path === 'lastName')) { %>
<p class="error_text"><i class="fas fa-exclamation-triangle"></i> Please enter a last name</p>
<% } %>
</div>
</div>
</div>
<div class="single_input">
<label>Email</label>
<input type="email" name="email" id="email" placeholder="Email" class="<%= validationErrors.find( e => e.path === 'email') ? 'invalid' : '' %>" value="<%= oldInput.email %>">
<% if (validationErrors.find( e => e.path === 'email')) { %>
<p class="error_text"><i class="fas fa-exclamation-triangle"></i> Please enter a valid email</p>
<% } %>
</div>
<div class="single_input">
<label>Password <i class="fal fa-info-circle" data-bs-toggle="tooltip" data-bs-html="true" data-bs-title="Password must contain:<br><ul><li>8 characters</li><li>One lowercase letter</li><li>One uppercase letter</li><li>One number</li><li>One symbol</li></ul>"></i></label>
<input type="password" name="password" id="password" placeholder="********" class="<%= validationErrors.find( e => e.path === 'password') ? 'invalid' : '' %>">
</div>
<div class="single_input">
<label>Confirm password</label>
<input type="password" name="confirmPassword" id="confirmPassword" placeholder="********" class="<%= validationErrors.find( e => e.path === 'confirmPassword') ? 'invalid' : '' %>">
</div>
<input type="hidden" name="CSRFToken" value="<%= csrfToken %>">
<button class="common_btn" type="submit">Submit</button>
</form>
I’ve looked at forums, and google lots, and just cannot solve this issue. Currently, I’m going to remove CSRF so I can at least develop, but don’t want to go live with this issue.
Thank you!
Mark Rosenberg is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.