Invalid CSRF token error (express.js) - node.js

I am using node 6.5.0 and npm 3.10.3.
I'm getting this invalid csrf token error when I am trying to log in the user to the site.
{ ForbiddenError: invalid csrf token
at csrf (/Users/Documents/web-new/node_modules/csurf/index.js:113:19)
The login with storing session in redis works without the csurf module (https://github.com/expressjs/csurf). With the csurf module, the session ID is getting stored in redis but I am not able to return the proper response to the client to log in the user. I am using Angular2 with node/express. From what I understand, Angular2 by default supports CSRF/XSRF with the CookieXSRFStrategy when using HTTP service, so all I need to do is configure something on the node/express side. The Angular2 app with webpack-dev-server is running on localhost:3000 while the node/express server is running on localhost:3001. I am supporting CORS.
I am able to see cookie with name XSRF-TOKEN in devtools at localhost:3000.
Could you kindly recommend how I might fix this error?
//cors-middleware.js
var corsOptions = {
origin: 'http://localhost:3000',
credentials:true
}
app.use(cors(corsOptions));
app.use(function(req, res, next) {
res.setHeader('Content-Type','application/json');
next();
})
};
//index.js
import path from 'path';
import session from 'express-session';
import connectRedis from 'connect-redis';
import rp from 'request-promise';
import * as _ from 'lodash';
import cors from 'cors';
import csurf from 'csurf';
const redisStore = connectRedis(session);
const dbStore = new redisStore(db);
let baseUrl = app.getValue('baseUrl');
/* ~~ api authentication ~~ */
let options = {
method: 'POST',
url: `${baseUrl}/authenticate`,
rejectUnauthorized: false,
qs: {
username: 'someUsername', key: 'someKey'
},
json: true
};
rp(options)
.then(response => {
let apiToken = response.response;
app.setValue("token", apiToken);
})
.catch(err => {
console.error(err);
});
/* ~~ configure session ~~ */
app.use(session({
secret: app.getValue('env').SESSION_SECRET,
store: dbStore,
saveUninitialized: false,
resave: false,
rolling: true,
cookie: {
maxAge: 1000 * 60 * 30 // in milliseconds; 30 min
}
}));
/* ~~ login user ~~ */
let csrf = csurf();
app.post('/loginUser', csrf, (req, res, next) => {
let user = {};
let loginOptions = {
method: 'POST',
url: `${baseUrl}/client/login`,
rejectUnauthorized: false,
qs: {
token: app.getValue('token'),
email: req.body.email,
password: req.body.password
},
headers: {
'Content-Type': 'application/json'
},
json: true
};
rp(loginOptions)
.then(response => {
let userToken = response.response.token;
let clientId = response.response.clientId;
req.session.key = req.session.id;
user.userToken = userToken;
user.clientId = clientId;
let clientAttributeOptions = {
url: `${baseUrl}/client/${clientId}/namevalue`,
rejectUnauthorized: false,
qs: {
token: app.getValue('token'),
usertoken: userToken
},
json: true
};
return rp(clientAttributeOptions);
})
.then(response => {
req.session.user = user;
res.send({user:user})
})
.catch(err => {
next(err);
})
});

My issue was that I was including the csrf function as a middleware only in the app.post('/loginUser) route.
When I included it for all routes, the module worked fine.
let csrf = csurf();
app.get('/*', csrf, (req, res) => {
res.sendFile(app.get('indexHTMLPath'));
});

Related

Frontend not receiving cookie from backend (ExpreeJS, Passport, Postgresql)

I have been struggling on this problem for days. I have a NextJS frontend running on localhost:3000 and an ExpressJS backend running on localhost:3001.
I am trying to build a login flow where I send an axios login request to the backend, authenticate with passport, and send the cookie back to the frontend (that can subsequently be used).
When I run through the flow, I can successfully send data to the backend and authenticate with passport (which writes a row to my session table), and redirect on the frontend. However, I do not see the cookie in my frontend browser (Inspect Element > Application > Cookies > localhost:3000). And when I am redirected to my dashboard page, I show as unauthorized from my status endpoint (which I believe means the cookie is not being set correctly). When I hit the backend endpoint with Postman, I can see the cookie is successfully sent and the subsequent /status endpoint call returns as authorized.
Can anyone help me understand why my cookies aren't being set correctly?
Backend - Express Setup:
const app = express ()
// Enable parsing middleware for requests
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
// Enable CORS
const originURL = process.env.RAILWAY_STATIC_FRONTEND_URL ? process.env.RAILWAY_STATIC_FRONTEND_URL : process.env.LOCAL_STATIC_FRONTEND_URL || 'http://localhost:3000'
app.use(cors({
origin: [originURL],
credentials: true
}))
// Session store
const pgSession = require('connect-pg-simple')(session);
const postgreStore = new pgSession({
// check interface PGStoreOptions for more info https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/connect-pg-simple/index.d.ts
// pool: poolInstance,
createTableIfMissing: true, // this will create a `session` table if you do not have it yet
})
// 1000ms * 60seconds * 60min * 24hrs * 7days = 1 week
const maxAge = 1000 * 60 * 60 * 24 * 7
app.use(session({
secret: process.env.EXPRESS_SESSION_SECRET || 'secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: maxAge,
sameSite: "none",
secure: false,
httpOnly: false
},
store: postgreStore,
}))
// Enable Passport
app.use(passport.initialize())
app.use(passport.session())
// Prefix all backend routes with '/api'
app.use('/api', routes)
Backend - Login + Status Routes:
import passport from 'passport';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '#prisma/client';
import { Router } from "express";
const router = Router ();
const prisma = new PrismaClient();
router.post('/login', passport.authenticate('local'), (req, res) => {
return res.sendStatus(200)
})
router.get('/status', (req, res) => {
return req.user
? res.send(req.user)
: res.status(401).send({ msg: "Unauthorized" })
})
export default router
Frontend: Login API Call
import type { NextApiRequest, NextApiResponse } from 'next'
import axios from "axios"
type ResponseData = {
message: string
}
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const BACKEND_API_URL = process.env.RAILWAY_STATIC_BACKEND_URL ? process.env.RAILWAY_STATIC_BACKEND_URL : process.env.NEXT_PUBLIC_LOCAL_STATIC_BACKEND_URL
const headers = {
}
const inputs = {
username: req.body.username,
password: req.body.password
}
if (!headers) return res.redirect(302, '/')
const config = {
headers: headers,
withCredentials: true
}
try {
// const { data } = await axios.post(`${BACKEND_API_URL}/api/auth/login`, inputs, { withCredentials: true })
await axios.post(`${BACKEND_API_URL}/api/auth/login`, inputs, { withCredentials: true })
return res.redirect(307, '/dashboard')
} catch (err) {
console.log(err)
return res.redirect(302, '/login')
}
}
Frontend - Dashboard Page
import { GetServerSidePropsContext, NextPage } from 'next'
import axios from 'axios'
type PageProps = {
msg: String,
}
const DashboardPage: NextPage<PageProps> = ({ msg }) => {
console.log(msg)
return (
// <div className={styles.container}>
<div>
<div> Dashboard Page </div>
<div> { msg } </div>
</div>
)
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const BACKEND_API_URL = process.env.RAILWAY_STATIC_BACKEND_URL ? process.env.RAILWAY_STATIC_BACKEND_URL : process.env.NEXT_PUBLIC_LOCAL_STATIC_BACKEND_URL
const headers = {
}
const config = {
headers: headers,
withCredentials: true
}
let msg
try {
const user = await axios.get(`${BACKEND_API_URL}/api/auth/status`, { withCredentials: true })
console.log(user)
msg = user
} catch (err) {
// console.log(err)
console.log(err.response.data)
msg = err.response.data.msg
}
var response = {
props: {
msg
}
}
return response
}
export default DashboardPage
It is basically because the client side is not storing the cookie. It is an issue from backend. There will be different settings while running the project in local and in cloud.
Try tweaking like this in your session settings and check in local environment. These worked for my local environment.
{ httpOnly: true, sameSite: 'None', secure: true, }
you have to tweak this in trial and error method for cloud hosting in railway.

Invalid CSRF Token in React but valid in Postman

I have an Express server on which I'm generating a csrf token. My server looks like this
const csrfProtection = csrf({
cookie: {
httpOnly: true,
},
});
server.use(express.json());
server.use(express.urlencoded({ extended: true }));
server.use(
cors({
origin: "http://localhost:3000",
credentials: true,
})
);
server.use(cookieParser());
server.use(csrfProtection);
...
//Other routes
and i'm sending the token like this
export const csrf = (req, res) => {
return res.send({ csrfToken: req.csrfToken() });
};
If I take it from the response and add it to the X-CSRF-Token header in Postman, then I can access all the routes just fine. But when I do it in React I always get the invalid csrf token error
This is how I take the token in React
export const getCSRFToken = async () => {
try {
const { data } = await axios.get("/auth/csrf");
axios.defaults.headers.post["X-CSRF-Token"] = data.csrfToken;
} catch (error) {}
};
And I'm using the withCredentials: true flag on other requests. I can't figure out what I'm missing.
Apparently the problem is that you need to pass the withCredetials flag to the request getting the csrf token too. So this fixed the problem.
export const getCSRFToken = async () => {
try {
const { data } = await axios.get("/auth/csrf", { withCredentials: true });
axios.defaults.headers.common["X-CSRF-Token"] = data.csrfToken;
} catch (error) {}
};
Maybe you should change axios.defaults.headers.post["X-CSRF-Token"] = data.csrfToken to axios.defaults.headers.common["X-CSRF-Token"] = data.csrfToken

fetch: cookies not receiving cookies

I have a express server running on localhost:5000 and client running on port 3000.
After sending post request from client to login using fetch API browser is not setting cookie. I'm getting cookie in response header as 'set-cookie' but it isn't set in the browser at client side.
Here is my fetch request code:
return (
fetch(baseUrl + "users/login", {
method: "POST",
body: JSON.stringify(User),
headers: {
"Content-Type": "application/json",
},
credentials: "same-origin",
})
Server side code:
router
.route("/login")
.options(cors.corsWithOptions, (req, res) => {
res.sendStatus(200);
})
.post(cors.corsWithOptions, (req, res, next) => {
passport.authenticate("local", (err, user, info) => {
if (err) return next(err);
if (!user) {
res.statusCode = 401;
res.setHeader("Content-Type", "application/json");
res.json({ success: false, status: "Log In unsuccessful", err: info });
}
req.logIn(user, (err) => {
if (err) {
res.statusCode = 401;
res.setHeader("Content-Type", "application/json");
// res.header("Access-Control-Allow-Credentials", true);
res.json({
success: false,
status: "Login Unsuccessful!",
err: "Could not log in user!",
});
}
var token = authenticate.getToken({ _id: req.user._id });
res.cookie("jwt-token", token, {
signed: true,
path: "/",
httpOnly: true,
});
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.json({ success: true, status: "Login Successful!", token: token });
});
})(req, res, next);
});
How to handle Server-side session using Express-session
Firstly you will need the following packages
npm i express-session connect-mongodb-session or yarn add express-session connect-mongodb-session
Now that we have packages that we need to setup our mongoStore and express-session middleware:
//Code in server.js/index.js (Depending on your server entry point)
import expressSession from "express-session";
import MongoDBStore from "connect-mongodb-session";
import cors from "cors";
const mongoStore = MongoDBStore(expressSession);
const store = new mongoStore({
collection: "userSessions",
uri: process.env.mongoURI,
expires: 1000,
});
app.use(
expressSession({
name: "SESS_NAME",
secret: "SESS_SECRET",
store: store,
saveUninitialized: false,
resave: false,
cookie: {
sameSite: false,
secure: process.env.NODE_ENV === "production",
maxAge: 1000,
httpOnly: true,
},
})
);
Now the session middleware is ready but now you have to setup cors to accept your ReactApp so to pass down the cookie and have it set in there by server
//Still you index.js/server.js (Server entry point)
app.use(
cors({
origin: "http://localhost:3000",
methods: ["POST", "PUT", "GET", "OPTIONS", "HEAD"],
credentials: true,
})
);
Now our middlewares are all setup now lets look at your login route
router.post('/api/login', (req, res)=>{
//Do all your logic and now below is how you would send down the cooki
//Note that "user" is the retrieved user when you were validating in logic
// So now you want to add user info to cookie so to validate in future
const sessionUser = {
id: user._id,
username: user.username,
email: user.email,
};
//Saving the info req session and this will automatically save in your mongoDB as configured up in sever.js(Server entry point)
request.session.user = sessionUser;
//Now we send down the session cookie to client
response.send(request.session.sessionID);
})
Now our server is ready but now we have to fix how we make request in client so that this flow can work 100%:
Code below: React App/ whatever fron-tend that your using where you handling logging in
//So you will have all your form logic and validation and below
//You will have a function that will send request to server
const login = () => {
const data = new FormData();
data.append("username", username);
data.append("password", password);
axios.post("http://localhost:5000/api/user-login", data, {
withCredentials: true, // Now this is was the missing piece in the client side
});
};
Now with all this you have now server sessions cookies as httpOnly

Nodejs Express + Nuxt - Session Cookies

I am using an Express App for the backend and VueJs with Nuxt (Server Side Rendering). My problem is that the cookies are not getting saved when the session is getting refreshed.
Server:
const express = require('express')
const cookieParser = require('cookie-parser')
const { loadNuxt } = require('nuxt')
const app = express()
app.use(cookieParser())
// Middleware
app.use(async (req, res, next) => {
// ...
if (sessionExpired && refreshTokenIsValid) {
// Generate new session
// ...
res.cookie('sessionToken', token, { maxAge: 86400000, path: '/' })
res.cookie('sessionId', id, { maxAge: 86400000, path: '/' })
res.cookie('refreshToken', refreshToken, { maxAge: 86400000, path: '/' })
return next()
}
})
...
Login route
router.get('/login', async (req, res, next) => {
// ...
res.cookie('sessionToken', token, { maxAge: 86400000, path: '/' })
res.cookie('sessionId', id, { maxAge: 86400000, path: '/' })
res.cookie('refreshToken', refreshToken, { maxAge: 86400000, path: '/' })
res.status(200).redirect('/')
})
Client:
async asyncData({ $axios }) {
const data = await $axios.get('/something')
},
methods: {
async someMethod() {
let data = await this.$axios.$get('/something')
}
}
The cookies are not getting saved when sending a request from asyncData().
I solved the problem by using an axios helper.
Solution source: proxy cookies
// plugins/ssr-cookie-proxy.js
import { parse as parseCookie } from 'cookie';
function parseSetCookies(cookies) {
return cookies
.map(cookie => cookie.split(';')[0])
.reduce((obj, cookie) => ({
...obj,
...parseCookie(cookie),
}), {});
}
function serializeCookies(cookies) {
return Object
.entries(cookies)
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
.join('; ');
}
function mergeSetCookies(oldCookies, newCookies) {
const cookies = new Map();
function add(setCookie) {
const cookie = setCookie.split(';')[0];
const name = Object.keys(parseCookie(cookie))[0];
cookies.set(name, cookie);
}
oldCookies.forEach(add);
newCookies.forEach(add);
return [...cookies.values()];
}
export default function ({ $axios, res }) {
$axios.onResponse((response) => {
const setCookies = response.headers['set-cookie'];
if (setCookies) {
// Combine the cookies set on axios with the new cookies and serialize them
const cookie = serializeCookies({
...parseCookie($axios.defaults.headers.common.cookie),
...parseSetCookies(setCookies),
});
$axios.defaults.headers.common.cookie = cookie; // eslint-disable-line no-param-reassign
// If the res already has a Set-Cookie header it should be merged
if (res.getHeader('Set-Cookie')) {
const newCookies = mergeSetCookies(
res.getHeader('Set-Cookie'),
setCookies,
);
res.setHeader('Set-Cookie', newCookies);
} else {
res.setHeader('Set-Cookie', setCookies);
}
}
});
}

Cookie is not set using express and passport

I spent a long time trying figure it out why it's not working.
I'm implementing a login page using react.
This page send the user and pass to backend (nodejs + express) using axios:
const login = useCallback(e => {
e.preventDefault()
fetch(process.env.REACT_APP_HOST + '/login', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: e.target.elements.username.value,
password: e.target.elements.password.value
})
})
.then(response => {
if (response.ok) {
return response.json()
} else if (response.status === 401) {
throw new Error('Invalid user or pass.')
} else {
throw new Error('An error ocurred')
}
...
})
}, [])
In backend, I have a route that receive those data and check on ldap system. So I generate a token using JWT and save it on token
const express = require('express'),
passport = require('passport'),
bodyParser = require('body-parser'),
jwt = require('jsonwebtoken'),
cors = require('cors')
cookieParser = require('cookie-parser')
LdapStrategy = require('passport-ldapauth');
let app = express();
app.use(cookieParser())
app.use(cors());
....
app.post('/login', function (req, res, next) {
passport.authenticate('ldapauth', { session: false }, function (err, user, info) {
const token = jwt.sign(user, env.authSecret)
res.cookie('cookie_token', token) //it doesn't set the cookie
if (err) {
return next(err)
}
if (!user) {
res.sendStatus(401)
} else {
return res.status(200).send({ firstName: user.givenName});
}
})(req, res, next);
});
The problem is that the token is empty, it's not being set.
Couple of things. In your react fetch post method you need to add
withCredentials: true,
beside the httpheader.
fetch(process.env.REACT_APP_HOST + '/login', {
method: 'POST',
withCredentials: true,
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: e.target.elements.username.value,
password: e.target.elements.password.value
})
})
After that in your nodejs part you are using cors but validating all origin. Thats not going to work with credentials. You need to use cors like this to validate specific origin and also turn credentials to true-
app.use(cors({credentials: true, origin: 'http://localhost:4200'}));
after that you can send your cookie by
res.cookie('cookie_token', token, { maxAge: 900000 })
This way the cookie will arrive and once the cookie is arrived in client side you can retrieve the cookie with document.cookie or with any other package like "js-cookie"

Resources