Why does the csrf check not work on the server? - node.js

const csrfProtection = csrf({
cookie: {httpOnly: true}
})
// Middleware
app.use(express.json())
app.use(cookieParser())
app.use(csrfProtection)
app.use(cors({
origin: 'http://localhost:8081',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
exposedHeaders: 'XSRF-TOKEN'
}))
app.use(helmet.frameguard({ action: 'SAMEORIGIN' }))
app.use(helmet.ieNoOpen())
app.use(helmet.hidePoweredBy())
app.use(safetyMiddleware)
app.use('/api', router)
app.use(errorMiddleware)
I made a route that every time I visit the site, it sends a token in the request header
router.get('/', (req, res) => {
const xsrf = req.csrfToken()
res.set("XSRF-Token", xsrf).json('true')
})
Client axios:
Example:
const $host = axios.create({
withCredentials: true,
baseURL: process.env.VUE_APP_SERVER_URL,
headers: {
"xsrf_token": localStorage.getItem('csrf')
}
})
export const csrfAuth = async () => {
const {headers} = await $host.get('/')
localStorage.setItem('csrf', headers['xsrf-token'])
return headers
}
export const loginPassword = async (email, password) => {
const {data} = await $host.post('/user/login', {email, password})
return data
}
The first request comes in and saves one token in cookies, the second in local storage.
The first question is, should they be different?
Why does the server respond to a request to log in with a 500 status? The process doesn't even get to the next middleware
Thanks.

The statement
localStorage.getItem('csrf')
is executed only once when the browser reads the client-side javascript, that is, before csrfAuth() is called and the statement
localStorage.setItem('csrf', headers['xsrf-token'])
executed. Therefore the xsrf-token header in $host does not have the desired value when the POST /user/login request is made.

Related

express-sesison not saving on routes

I have a ExpressJS server and I would like to implement in Sessions however it doesn't seem to save the sessions.
The flow is to:
POST to /api/login
GET from /api/viewSession
However, the session['stuff'] returns undefined.
I suspected it might be because i'm trying to GET the session from a different URL. So I added a GET method to /api/login but it returned undefined too.
Could somebody point me in the right direction please? I'm a little lost after a few hours of Googling to no avail.
Here below is my code for index.js and my route api.js.
Also, I'm using
NPM - Version 8.3.1
Node - Version v16.14.0
npm i cors - Version 2.8.5
npm i express-session - Version 1.17.2
npm i express - Version 4.17.3
index.js
const express = require('express')
const formidable = require('express-formidable');
const cors = require('cors');
const session = require('express-session');
const api = require('./routes/api');
const app = express()
const port = 3000;
app.use(express.json());
app.use(formidable());
app.use(
cors({
origin: true,
optionsSuccessStatus: 200,
credentials: true,
})
);
app.options(
'*',
cors({
origin: true,
optionsSuccessStatus: 200,
credentials: true,
})
);
app.use(
session({
saveUninitialized: false,
secret: "anyrandomstring",
cookie: { maxAge: 36000000 }
})
);
//Routes
app.use('/api', api);
//Navigation
app.get('/', function (req, res) {
res.render('index');
res.send("Hi!");
})
//App Start
app.listen(port, () => {
console.log(`App Listening on port ${port}`);
})
api.js
"use strict";
const express = require("express");
let router = express.Router();
router
.route('/dump')
.post(async (req, res) => {
console.log(req.fields);
res.send({status: "ok"})
})
router
.route('/login')
.post(async (req, res) => {
//Saving in Session
req.session['stuff'] = "123456";
res.send("Ok");
})
router
.route('/viewSession')
.get((req, res) => {
console.log(req.session['stuff']);
res.send("ok");
})
module.exports = router;
Also, this is the way I send the POST/GET request
$.ajax({
url: "http://localhost:3000" + '/api/login',
type: "POST",
crossDomain: true,
dataType: "json",
data: {},
success: function (response) {
console.log(response);
}
})
If you're making cross-domain requests with XMLHttpRequest and you want to allow cookies to be set by the server handling the request, you need to set withCredentials : true.
Using jQuery:
$.ajax({
url: "http://localhost:3000" + '/api/login',
type: "POST",
crossDomain: true,
xhrFields: { withCredentials: true },
dataType: "json",
data: {},
success: function (response) {
console.log(response);
}
})

Cookies are not being set in production

I deployed an express server to heroku as well as a next.js app.
The cookies are being sent from the server and even being shown in the network tab:
However, the cookies are not actually stored, all my requests failed because they depends on the csrf cookie, and the storage tab is empty:
This is the code for setting the cookies in the backend:
const csrfProtection = csrf({
cookie: {
httpOnly: true,
sameSite: 'none',
secure: true,
},
});
app.set('trust proxy', 1);
app.use(cors({ credentials: true, origin: clientOrigin }));
app.get('/', csrfProtection, function (req: Request, res: Response) {
res.cookie('XSRF-TOKEN', req.csrfToken(), { sameSite: 'none', secure: true });
res.end();
});
app.use(csrfProtection);
this is my axios instance:
const baseURL = process.env.baseURL;
const axiosInstance = Axios.create({
baseURL,
withCredentials: true,
});
and the request code:
useEffect(() => {
dispatch(loadingAction.setToTrue());
const getCsrf = async () => {
await axiosInstance.get('/');
};
getCsrf()
.then(() => {
dispatch(loadingAction.setToFalse());
})
.catch((err) => {
dispatch(loadingAction.setToFalse());
setError(err);
});
}, []);
While looking at your screenshot - I observed that the browser has a different domain set hilife01 vs hilife-1
Accessing throuugh https://hlife01.herokuapp.com/auth/login - gets you the cookies but your App doesn't have the right route configured.
Most likely, The Right domain is not associated with the cookie being set so while setting the cookie, the browser silently rejects the cookie because you are not matching the domain, however it is visible in the address bar.

How to send back CSRF Token to server with Axios

I have an Express Server with an endpoint that generates the csrf token for the client,
Now, I tried sending back the token in my axios request as below but I keep getting the usual Forbidden: invalid csrf token error.
Below is my code:
static async attachCSRFTokenToHeaders(headers) {
let csrfTokenRequest = await axios.get(EndPoints.CSRF_TOKEN);
let csRefToken = csrfTokenRequest.data;
headers['X-CSRF-TOKEN'] = csRefToken.csrfToken;
}
static async getRequestHeaders() {
let headers = {};
//Possibly add more headers
await this.attachCSRFTokenToHeaders(headers); //Attach the csrf token to the headers of each request
return headers;
}
static async logInManually(email, password, cb) {
let requestBody = { email, password};
axios.post(EndPoints.SIGN_IN, requestBody, {
headers: await this.getRequestHeaders() //Attach the headers here
}).then((response) => {
cb(HttpSuccessDataHandler.getSuccessResponseData(response), null);
}).catch((e) => {
cb(null, HttpErrorHandler.spitHttpErrorMsg(e));
});
}
But the server still keeps throwing the usual:
ForbiddenError: invalid csrf token
Here is a snippet into my server setup
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const session = require('express-session');
....
initMiddleWare() {
app.use(express.static('./static'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser())
app.use(session({
secret: Constants.SESSIONS_SECRET,
resave: false,
saveUninitialized: false
}));
app.use(busboy({
highWaterMark: 2 * 1024 * 1024,
limits: {
fileSize: maxFileSize,
}
}));
app.use(csrf({ cookie: true }))
}
//Then somewhere in my routes, here is the route that provides the csrf token
.....
app.get(Routes.CSRF_TOKEN, function (req, res) {
res.send({ csrfToken: req.csrfToken() });
});
....
Because of csrf({cookie: true}), the CSRF token is bound to a cookie. The axios.post request must contain not only the CSRF token in a header, but also the cookie that was received with the response to the previous axios.get request. Your code sets only the header. Unless axios handles the cookies automatically (like a browser would do), you must include client-side code for handling them as well.

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

Invalid CSRF token error (express.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'));
});

Resources