CSRF with fastify session cookies - node.js

I have a fastify session plugin that creates user sessions and manages them in postgres, but i want to make sure that i have all my sessions protected from CSRF. Im looking at the fastify-csrf plugin and im not exactly sure how to properly implement this. Do i need to generate the csrf token only when the session cookie is first generated or on all requests?
session plugin:
const cookie = require('fastify-cookie');
const session = require('fastify-session');
const csrf = require('fastify-csrf');
const pgSession = require('connect-pg-simple')(session);
const fp = require('fastify-plugin');
/**
* #param {import('fastify').FastifyInstance} fastify
*/
const plugin = async (fastify) => {
// All plugin data here is global to fastify.
fastify.register(cookie);
fastify.register(csrf, { sessionPlugin: 'fastify-session' });
fastify.register(session, {
store: new pgSession({
conString: process.env.DATABASE_URL,
tableName: 'user_session', // Defaults to 'session'
}),
secret: process.env.SESSION_SECRET,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV !== 'development',
maxAge: 86400 * 1000, // 1 day expiration time
},
});
<!-- This is from the documentation, should this only be applied to the /login route when the cookie is generated? When do i verify that the cookie has not been tampered with?
fastify.route({
method: 'GET',
path: '/',
handler: async (req, reply) => {
const token = await reply.generateCsrf();
return { token };
},
});
// Add the user object to the session for later use.
fastify.addHook('preHandler', (req, reply, next) => {
if (!req.session) req.session.user = {};
next();
});
};
module.exports = fp(plugin);

Related

SessionID is generating a unique value on every post request

I am trying to identify the user that is on my application via sessionId, not actual info on the user account itself. However, what I am noticing is that the sessionId changes everytime the user performs an action on the page. As shown below. My goal would be to have the same sessionID from the point they open the webpage until they close it.
const app = require('express')();
const https = require('https');
const fs = require('fs');
const session = require('express-session');
function getDateTimestamp(){
var today = new Date();
var date = today.getFullYear()+'_'+(today.getMonth()+1)+'_'+today.getDate();
return date;
}
app.use(session({
resave: false,
saveUninitialized: true,
secret: 'whatever',
cookie: {
maxAge: 60*60*1000,
sameSite: true
}
}))
app.get('/', (req, res) => {
res.writeHead(200, {'Content-Type': 'text/html'});
var readStream = fs.createReadStream('index.html','utf8');
readStream.pipe(res);
});
app.post('/:fieldName/:flag/:time/:dashboard/:identifier/:user', (req, res) => {
console.log('POST message received', req.params);
if (req.params && req.params.fieldName) {
fs.appendFileSync(`./changeLog_${getDateTimestamp()}.csv`, `${req.params.fieldName},${req.params.flag},${req.params.time},${req.params.dashboard},${req.params.identifier},${req.params.user},${req.sessionID}\n`);
return res.send('OK')
}
res.status(400).end()
});
Client Side
function onParameterChange (parameterChangeEvent) {
parameterChangeEvent.getParameterAsync().then(function (param) {
parameterIndicator = 'Parameter'
const details = {
method: 'POST',
credentials: 'include'
//body: JSON.stringify(data),
// headers: {
// 'Content-Type': 'application/json'
// }
};
fetch(`url/${param.name}/${parameterIndicator}/${getDateTimestamp()}/${dashboardName}/${param.name}/${worksheetData}`, details).then((res) => {console.log(res);});
});
}
Here is my output showing a different session for the same user.
Just to illustrate my comment above, I actually have ran a quick test with a simple setup, and toggling saveUninitialized actually seems to make the difference:
// app.js
const express = require('express')
const app = express()
const session = require('express-session')
// Run the file as "node app false" or "node app true" to toggle saveUninitialized.
const saveUninitialized = process.argv[2] == "true" ? true : false
app.use(session({
resave: false,
saveUninitialized,
secret: 'whatever',
cookie: {
maxAge: 60 * 60 * 1000,
sameSite: true
}
}))
app.get("/", (req, res) => {
res.status(200).send(req.sessionID)
})
app.listen(3000, () => {
console.log('server started on http://localhost:3000')
})
// Response body
node app false
// 1st request: OTnFJD-r1MdiEc_8KNwzNES84Z0z1kp2
// 2nd request: 5UVVGng_G72Vmb5qvTdglCn9o9A4N-F6
// 3rd request: 9aGsAwnHh1p1sgINa1fMBXl-oRKcaQjM
node app true
// 1st request: StUrtHOKBFLSvl5qoFai6OQCm7TY87U-
// 2nd request: StUrtHOKBFLSvl5qoFai6OQCm7TY87U-
// 3rd request: StUrtHOKBFLSvl5qoFai6OQCm7TY87U-
But maybe there is more to it than that with your setup.

Why passport authentication seems to work only in local?

I've a Node.js backend service and a React frontend. It was working till today when I had again an issue related to the CORS. It works fine in my local env but when I deploy this to App Engine the CORS issue is still there. What's is missing here?
Here my code:
Node.JS Backend Service:
const app = express();
/* MIDDLEWARE USER: set up cors to allow us to accept requests from our client */
app.use(
cors({
origin: process.env.CLIENT_URL || 'http://localhost:3001', // allow to server to accept request from different origin
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true, // allow session cookie from browser to pass through
}),
);
I'm using passport to obtain credentials from Google and pass to the server
/* MIDDLEWARE USE: use Session Middleware */
const MAX_AGE = process.env.MAX_AGE || 60 * 60 * 1000;
const SECRET = process.env.SECRET || 'Our Secret';
const DEFAULT_ENV = process.env.NODE_ENV || 'development';
app.use(session({
cookie: {
maxAge: MAX_AGE,
secure: DEFAULT_ENV === 'production',
// secure: true,
httpOnly: true,
},
secret: SECRET,
resave: false,
saveUninitialized: false,
// store: new FileStore(fileStoreOptions),
store: new FirestoreStore({
dataset: new Firestore({
kind: 'express-sessions',
}),
}),
}));
/* MIDDLEWARE USE: use Passport Middleware */
app.use(passport.initialize());
app.use(passport.session());
Then I use react & redux in my frontend and here the code to obtain credentials from my endpoint.
/* RETRIEVE INFO FROM OAUTH AS SOON USER CLICK ON LOGIN WITH GOOGLE */
export const loginWithGoogle = () => {
return (dispatch) => {
dispatch({type: FETCH_START});
axios.post('/auth/login/oauth/success').then(({data}) => {
// console.log('userSignInFromGoogle: ', data);
if (data) {
const {originalMaxAge} = data.session.cookie;
const expireDate = (new Date()).getTime() + originalMaxAge;
localStorage.setItem('token', JSON.stringify(data.result.accessToken));
localStorage.setItem('token_expires_in', JSON.stringify(expireDate));
axios.defaults.headers.common['Authorization'] = 'Bearer ' +
data.result.accessToken;
dispatch({type: FETCH_SUCCESS});
// dispatch({type: USER_DATA, payload: data.result});
dispatch({type: USER_TOKEN_SET, payload: data.result.accessToken});
} else {
dispatch({type: FETCH_ERROR, payload: data.error});
}
}).catch(function(error) {
dispatch({type: FETCH_ERROR, payload: error.message});
// console.log('Error****:', error.message);
});
};
};
/* FUNCTION TO FETCH DATA FROM THE AUTHENTICATED USER */
export const getAuthenticatedUser = () => {
return (dispatch) => {
dispatch({type: FETCH_START});
isTokenExpired();
axios.post('auth/me',
).then(({data}) => {
// console.log('userSignIn: ', data);
if (data.result) {
dispatch({type: FETCH_SUCCESS});
dispatch({type: USER_DATA, payload: data.result});
} else {
dispatch({type: FETCH_ERROR, payload: data.error});
}
}).catch(function(error) {
dispatch({type: FETCH_ERROR, payload: error.message});
// console.log('Error****:', error.message);
if (error) {
dispatch({type: SIGNOUT_USER_SUCCESS});
localStorage.removeItem('token');
localStorage.removeItem('token_expires_in');
}
});
};
};
Here where I define the endpoint for axios:
import axios from 'axios';
/* TODO: Change In production with this */
export default axios.create({
withCredentials: true,
baseURL: `backend-url`,//YOUR_API_URL HERE
headers: {
'Content-Type': 'application/json',
},
});
After some test to figure out what the problem could be here, I finally tested this code on various browser and then only Chrome showed this issue not passing the token from the backend server to the other server. In the end I modified the code snippet related to the session store, adding the "sameSite" property to the cookie. Chrome, in the latest version, requires this property to be specified, otherwise it blocks cookies from server to server.
/* MIDDLEWARE USE: use Session Middleware */
const MAX_AGE = process.env.MAX_AGE || 60 * 60 * 1000;
const SECRET = process.env.SECRET || 'Our Secret';
const DEFAULT_ENV = process.env.NODE_ENV || 'development';
app.use(session({
cookie: {
maxAge: MAX_AGE,
secure: DEFAULT_ENV === 'production',
httpOnly: true,
/*TODO: Fix for chrome*/
sameSite: 'none',
},
secret: SECRET,
resave: false,
saveUninitialized: false,
// store: new FileStore(fileStoreOptions),
store: new FirestoreStore({
dataset: new Firestore({
kind: 'express-sessions',
}),
}),
}));

How to create a session for user system in node.js

I am trying to create a session for my system of users but I do not manage in any way to implement the sessions and it returns me that they are not defined.
I am currently using express-session, body-parser and cookie-parser.
I have tried to implement the session variables from my app.js file, invoke it to my access file and I still get an error of variable without defining
my varaibles of app.js
app.use(cookieParser());
const redis = new connectRedis(session);
app.use(session({ store : new redis, saveUninitialized: true, resave: true, secret: 'it*SFVse', ttl : 3600, cookie: { maxAge: 3600000 * 24 * 7 } }));
app.use(connectFlash());
my post form
if (bcrypt.compareSync(password, result[0].password)) {
message = 'Welcome';
req.session.user = 1;
res.render('index.ejs', {
message,
title: "Login",
});
}
my index.js
getIndex: (req, res) => {
if (typeof req.session.user === 'undefined') { session = 'no session' } else { session = "session"; }
res.render('index.ejs', {
title: "Login",
message: '',
session
});
}
TypeError: Assignment to constant variable.

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'));
});

How to exclude routes from renewing express cookie expiration date

Using express js with express-session I have this main classic session middleware which defines the cookie's maxAge to one hour.
var express = require('express');
var session = require('express-session');
var RedisStore = require('connect-redis')(session);
var ExpressServer = express();
ExpressServer.use(
session(
{
secret : 'secret',
// Forces session to be saved even when unmodified
resave : false,
rolling : true,
// Forces session to be saved even when unmodified
saveUninitialized : true,
// Controls result of unsetting req.session (through delete, setting to null)
unset : 'destroy',
cookie: {
path: '/',
proxy: secureCookie,
secure: secureCookie,
httpOnly: true,
maxAge: 1000 * 60 * 60
},
store: new RedisStore(
{
client: RedisClient
}
)
}
)
);
However, I have some routes which are called periodically (every 30 seconds) from the client to the server, let's say one of them is:
ExpressServer.get(
'/periodic',
function (req, res, next) {
//doSomthing()
}
since this route is called periodically from the client, I need that it won't cause a renewal of the cookie's expiration date (in case the user leaves his browser open) and leave the current expiration date (from the last not-periodic route call)
How can I achieve it?
Simply put the ExpressServer.get('/periodic') call before the ExpressServer.use(session()) call. Or you can do something like:
var url = require('url');
var normalSessionOpts = {
secret : 'secret',
// Forces session to be saved even when unmodified
resave : false,
rolling : true,
// Forces session to be saved even when unmodified
saveUninitialized : true,
// Controls result of unsetting req.session (through delete, setting to null)
unset : 'destroy',
cookie: {
path: '/',
proxy: secureCookie,
secure: secureCookie,
httpOnly: true,
maxAge: 1000 * 60 * 60
},
store: new RedisStore(
{
client: RedisClient
}
)
};
var nonRollingSessionOpts = Object.assign({}, normalSessionOpts, { rolling: false });
var normalSession = session(normalSessionOpts);
var nonRollingSession = session(nonRollingSessionOpts);
ExpressServer.use(function (req, res, next) {
var parsedUrl = url.parse(req.url)
return parsedUrl.pathname === '/periodic'
? nonRollingSession(req, res, next)
: normalSession(req, res, next);
});

Resources