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.
Related
I'm trying to create an API with express that I will access using a React front end. I'm having a hard time figuring out how to handle authentication using express-session.
I used the middleware like follows:
var corsOptions = {
credentials: true,
origin: 'http://localhost:3000'
}
app.use(cors(corsOptions));
app.use(session({
secret: 'dfgdfbgbdfgd',
saveUninitialized: false,
resave: false,
cookie: {
secure: false
}
}));
Here is the Log in route (the local_auth middleware is just checking if the credentials are correct):
AccountRouter.post('/login', local_auth, (req, res) => {
req.session.user_id = req.user._id;
return res.status(200).send('Connected');
});
After loging in, I try to access the following route from React to check if the session is operational:
AccountRouter.get('/authcheck', (req, res) => {
let sess = req.session.user_id;
console.log(sess);
if (sess) return res.status(200);
else return res.status(404).send('pffff');
});
req.session is undefined.
The front end code is just a simple axios request to the above url. I have no idea if I have to save the cookie to localStorage and send each for each request.
Here is the request to the authcheck route:
axios.get('http://localhost:5000/api/accounts/authcheck', {withCredentials: true})
.then((response) => {
console.log(response);
})
.catch(() => {
console.log('waaah va te connecter');
});
And the login request:
const data = {
'email': e.target.email.value,
'password': e.target.password.value
};
axios.post('http://localhost:5000/api/accounts/login', data)
.then((response) => {
const sessionID = response.data;
console.log(sessionID);
});
req.session is undefined.
If req.session is undefined, then you are apparently trying to use the session in a route that is defined BEFORE this middleware code:
app.use(session({
secret: 'dfgdfbgbdfgd',
saveUninitialized: false,
resave: false,
cookie: {
secure: false
}
}));
has had a chance to run. When req.session is undefined, that means the session code has not yet run. Even if you had a cookie issue that causes a prior session to get lost, you would still have a new empty req.session if the session middleware had already run.
So, since you don't show the overall order of all your routes, we can't offer a specific fix, but it appears that this:
AccountRouter.get('/authcheck, ...)
is running before your session middleware which is a problem for any route that depends upon the session.
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.
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
I am trying to set a single cookie key-value pair with multiple values in nodeJS. The reason for the single cookie is that I'm sending Token and Secret variables that are linked as part of authentication, separating these out over two cookies may cause issues. I followed a tutorial that suggests stingify method.
When tested in Postman, the cookie seems to be encoded with token and secret. i.e. '%7B%22' etc. Is this as expected, if yes how do I parse values when the cookie is sent to server.
// userToken and mySecret test values.
CustomerRoute.post('/login', (req, res) => {
...
...
var mycookie = JSON.stringify({userToken:1234,mySecret:5678});
res.cookie('ID', mycookie, {HttpOnly:true, maxAge:20*60*1000, sameSite: 'strict'});
res.apiSuccess(resInfo);
Cookie in postman:
ID=%7B%22userToken%22%3A1234%2C%22mySecret%22%3A5678%7D; Path=/; Domain=localhost; Expires=Tue, 02 Mar 2021 17:37:24 GMT;
UPDATE -
I also managed to send the two tokens without stingify by simply concatenating the two strings.
var mycookie = 'Token='+'1234'+'Secret='+'5678';
UPDATE2
I'm using cookie-parser. When I call route:
.post('/data1', (req, res) => {
//var rc = req.headers.cookie;
const { cookies } = req;
console.log(cookies);
res.apiSuccess();
In console I get:
{ ID: '{"userToken":1234,"mySecret":5678}' }
Whats the best method to split values to variables?
UPDATE3 - As a recap. I want to write a single cookie with userToken and mySecret, then in the /data route verify (this will eventually form a function in middleware)
const http = require('http');
const https = require('https');
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
var cookieParser = require('cookie-parser')
const config = require('./config');
var corsOptions = {
origin: 'http://example.com',
optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
}
/* init server */
const server = express();
/* middleware */
server.use(express.json());
server.use(cookieParser());
server.use(express.static('public'))
server.use(bodyParser.json({
limit: "10000kb"
}));
server.use(bodyParser.urlencoded({
extended: true,
limit: "10000kb"
}));
server.use(function (req, res, next) {
res.apiError = function (message) {
res.json({
status: false,
message: message
})
};
res.apiSuccess = function (data) {
res.json({
status: true,
data: data
})
};
next();
})
Create cookie - login function
var mycookie = JSON.stringify({userToken:1234,mySecret:5678});
res.cookie('session_id', mycookie, {HttpOnly:true, maxAge:20*60*1000, sameSite: 'strict'});
Read cookie:
CustomerRoute.post('/data1', (req, res) => {
// var rc = req.headers.cookie;
const { cookies } = req;
console.log(cookies);
if ('session_id' in cookies) {
console.log('Session Id exists');
var points = JSON.parse(cookies);
//console.log(cookies['id']);
console.log(points);
}
res.apiSuccess();
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'));
});