I have an unexpected behavior when using isomorphic-fetch vs. request-promise related to Express sessions (and Express session ID in particular):
As part of the troubleshooting process, I implemented two methods in client.js for calling endpoints in server.js: 1) isomorphic-fetch, and 2) request-promise.
Client.js
// Method 1: isomorphic-fetch
require('es6-promise').polyfill();
require('isomorphic-fetch');
fetch('http://localhost:3000', {
credentials: 'same-origin',
})
.then(function(response) {
console.log(response.status);
});
fetch('http://localhost:3000', {
credentials: 'same-origin',
})
.then(function(response) {
console.log(response.status);
});
// Method 2: request-promise
var rp = require('request-promise').defaults({
jar: true
});
function requestPage() {
return rp('http://localhost:3000/');
}
requestPage()
.then(console.dir)
.then(requestPage)
.then(console.dir)
.catch(console.error);
Server.js
var app = require('express')();
app.use(require('morgan')('dev'));
var session = require('express-session');
var FileStore = require('session-file-store')(session);
app.use(session({
name: 'server-session-cookie-id',
secret: 'my express secret',
saveUninitialized: true,
resave: true,
store: new FileStore(),
cookie: {
secure: false
}
}));
app.get('/', function initViewsCount(req, res, next) {
console.log('req.session.id = ' + req.session.id);
if (typeof req.session.views === 'undefined') {
req.session.views = 1;
return res.end('Welcome to the file session demo. Refresh page!');
}
return next();
});
app.get('/', function incrementViewsCount(req, res, next) {
console.assert(typeof req.session.views === 'number',
'missing views count in the session', req.session);
req.session.views++;
return next();
})
app.use(function printSession(req, res, next) {
console.log('req.session', req.session);
return next();
});
app.get('/', function sendPageWithCounter(req, res) {
res.setHeader('Content-Type', 'text/html');
res.write('<p>views: ' + req.session.views + '</p>\n');
res.end();
});
var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
When I execute node client.js, here is the output of the server:
req.session.id = deWKCvqcyGiAvVSUvHv2Db7sjvE7xN1E
req.session.id = MxLHWjbMMvV4GRfPSf6sQ12XvauiJJot
req.session.id = A3KTLMdBopQ7pAfcTsJhnzzdokdA7hGI
GET / 200 1.407 ms - -
GET / 200 7.625 ms - -
GET / 200 0.728 ms - -
req.session.id = A3KTLMdBopQ7pAfcTsJhnzzdokdA7hGI
req.session Session {
cookie:
{ path: '/',
_expires: null,
originalMaxAge: null,
httpOnly: true,
secure: false },
views: 2,
__lastAccess: 1517449125197 }
GET / 200 6.902 ms - -
I confirmed that method 2 (request-promise) successfully persists the session on the server. In other words, session A3KTLMdBopQ7pAfcTsJhnzzdokdA7hGI is associated with this method.
However, as observed from the output, method 1 (isomorphic-fetch) generates two separate sessions on the server.
Question: Why does isomorphic-fetch create two separate sessions on the server?
Troubleshooting performed:
I replaced localhost with 127.0.0.1, but this did not change the behavior.
I replaced same-origin with include, but this did not change the behavior.
Environment:
node v6.10.3
isomorphic-fetch 2.2.1
request-promise 4.2.2
Related
I have been developing a simple login system and trying to deploy it on a production server. The express session works perfectly fine in localhost but when it comes to the production server or cross-origin it sends a new session in every request.
Server Codes:
const express = require('express');
const app = express();
const path = require('path');
const mysql = require('mysql');
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
const bcrypt = require('bcryptjs');
require('dotenv').config();
const cors=require("cors");
const bodyparser = require("body-parser")
app.use(express.json())
app.use(bodyparser.urlencoded({extended: true}))
app.use(express.static(path.join(__dirname, 'public')));
app.use(function (req, res, next) {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
res.setHeader('Access-Control-Allow-Credentials', true);
next();
});
var db = mysql.createConnection({
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASS,
database: process.env.DATABASE_NAME
});
db.connect((err) => {
if(err) {
console.log('error when connecting to db: ', err);
throw err;
}
});
const sessionStore = new MySQLStore({
expiration : (365 * 60 * 60 * 24 * 1000),
endConnectionOnClose: false,
}, db);
app.use(session({
key: 'fsasfsfafawfrhykuytjdafapsovapjv32fq',
secret: 'abc2idnoin2^*(doaiwu',
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: (365 * 86400 * 1000),
httpOnly: false,
secure: false,
sameSite: 'none'
}
}));
app.post('/isLoggedIn', (req, res)=>{
// req.session.destroy();
if(req.session.userID || req.session.userID == 0) {
let cols = [req.session.userID];
db.query('SELECT * FROM user WHERE user_id = ? LIMIT 1',cols, (err, data, fields) => {
if(data && data.length === 1) {
res.json({
success: true,
first_name: data[0].first_name,
email: data[0].email,
type:data[0].type,
});
return true;
}else {
res.json({
success: false,
});
}
});
}else {
res.json({
success: false
})
}
});
API request:
let res = await fetch("https://hotel-network-manager1.herokuapp.com/isLoggedIn", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
credentials: 'include',
mode:'cors'
});
I have read many posts and documentation but couldn't solve the issue. You can even check the server running in Heroku in the above URL of the fetch API. When using this same server running on the localhost it works perfectly fine which means this has something to do with CORS. Does anyone has any idea what I'm missing here.
My index.js Server
// USE STRICT;
const express = require('express');
const app = express();
const session = require('express-session');
const http = require('http').Server(app);
const socket = require('socket.io');
const schedule = require('node-schedule');
const cors = require('cors');
const io = socket(http, {
cors: {
origin: 'http://localhost:8080',
methods: ['GET', 'POST'],
allowedHeaders: ['my-custom-header'],
credentials: true
}
});
const port = 8080;
app.use(express.static(__dirname + '/public'));
app.use(express.static(__dirname + '/uploads'));
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const mustacheExpress = require('mustache-express');
app.engine('html', mustacheExpress());
app.set('view engine', 'html');
app.set('views', __dirname + '/views');
const secret = 'somesecretkeyhere';
const passport = require('passport');
const helmet = require('helmet');
const { sendMail } = require('./controllers/sellerAdsController');
// Gives us access to variables set in the .env file via `process.env.VARIABLE_NAME` syntax
// require('dotenv').config();
// Must first load the models before passport
require('./models/user');
// Pass the global passport object into the configuration function
require('./config/passport')(passport);
// This will initialize the passport object on every request
app.use(passport.initialize());
// Allows our remote applications to make HTTP requests to Express application
app.use(cors());
app.use(helmet());
app.use(express.urlencoded({ extended: false }));
// app.use(express.json()); //WARNING: Do not turn on. stops formidable for api calls
app.use(cookieParser(secret));
app.use(session({
secret: secret,
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true,
secure: true
}
}));
app.use(csrf());
// Stop page caching
app.use(function (req, res, next) {
res.set('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
next();
});
// Imports all of the routes from ./routes/index.js
app.use(require('./routes/api/v1'));
// Socket Operations
// io.on('connection', io => {
// let sessionId = io.id;
// io.on('clientHandshake', (data) => {
// console.log(data);
// io.emit('serverHandshake', { sessionId: sessionId });
// });
// });
// io.use((socket, next) => {
// const username = socket.handshake.auth.username;
// if (!username) {
// return next(new Error('invalid username'));
// }
// console.log(username);
// socket.username = username;
// next();
// });
io.on('connection', (socket) => {
console.log('👾 New socket connected! >>', socket.id);
// notify existing users
socket.broadcast.emit('user connected', {
userID: socket.id,
username: socket.username,
});
socket.on('private message', ({ content, to }) => {
socket.to(to).emit('private message', {
content,
from: socket.id,
});
console.log(content, to);
});
});
// EROOR HANDLING ROUTES MUST BE BENEATH ALL APP.USE AND ROUTES
// Check if request is from web or app (HTML/JSON)
// Handle 404
app.use(function (req, res) {
res.status(404);
res.render('404.html', { title: '404: File Not Found' });
});
// Handle 500
app.use(function (error, req, res) {
return res.send(error);
// res.status(500);
// res.render('500.html', { title: '500: Internal Server Error', error: error });
});
// SCHEDULED JOBS
const now = new Date();
let date = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 0, 0);
schedule.scheduleJob(date, sendMail);
http.listen(port, () => {
console.log(`listening on *:${port}`);
});
And this is how I am getting from VUE
window.axios.get('/databank/getCSRF').then((response) => {
window.axios.defaults.headers.common['XSRF-TOKEN'] = response.data;
}, (err) => {
console.log(err)
})
And this is my login request header
XSRF-TOKEN from my login request header sent by axios
So Ive set my server up like that, and my vue SPA, but getCSRF() seems to be getting the request but I can't do a POST request back to the server throws an error
ForbiddenError: invalid csrf token
at csrf
Maybe because you wrote XSRF-TOKEN instead of CSRF-Token as it suggests in the Express Documentation.
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.
I currently have a React App (via create-react-app) and an Express server for my API calls. They are both running separately and have been working fine until I ran into this problem:
I'm also using express-session and passport for user authentication. I'm able to authenticate users but the session doesn't persist between API calls. If I successfully login, the next time I make an API call, req.sessionID will be different, and req.isAuthenticated() will return false.
Here is my server code:
'use strict'
var express = require('express');
var path = require('path');
var bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var mongoose = require('mongoose');
var passport = require('passport');
var session = require('express-session');
var cors = require('cors');
var flash = require('connect-flash');
var store = require('./store');
var database = require('./database');
var app = express();
var port = process.env.port || 3001;
var router = express.Router();
var promise = mongoose.connect('mongodb://localhost:27017/lifeless-db', {useMongoClient: true});
//app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cookieParser('lifeless-secret-01890'));
app.use(session({
secret: 'lifeless-secret-01890',
saveUninitialized: true,
cookie: {
secure: false,
}
}));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());
// Initialize Passport
var authinit = require('./auth/init');
authinit(passport);
// For debugging:
app.use(function(req, res, next) {
console.log("SessionID: " + req.sessionID);
console.log(req.isAuthenticated() ? "This user is logged in" : "This user is NOT logged in");
next();
});
// GET
app.get('/items', function(req, res) {
store.getAllItems((items) => {
if(!items) return res.send({error: true, message: 'Error loading store :/', items: null});
else return res.send({error: false, message: 'Success', items: items});
});
});
app.get('/login', function(req, res) {
console.log(req.flash());
console.log("Login fail");
res.send({error: true, message: 'Unknown error'});
});
// POST
app.post('/message', function(req, res) {
database.submitMessage(req.body.email, req.body.message, (success) => {
if (success) return res.send({error: false, message: 'Success'});
else return res.send({error: true, message: 'Error sending message'});
});
});
app.post('/login', passport.authenticate('local', {failureRedirect: '/login', failureFlash: true}), function(req, res) {
console.log(req.flash());
console.log("Login success");
return res.send({error: false, message: 'Success'});
});
// SERVER
app.listen(port, function(){
console.log('Server started.');
console.log('Listening on port ' + port);
});
And here is an example of an API call from the React App (using axios):
login(e) {
e.preventDefault();
axios({
method: 'post',
url: 'http://localhost:3001/login',
data: {
username: this.state.username,
password: this.state.password,
}
})
.then((response) => {
console.log(response.data);
if (response.data.error) {
this.setState({error: true, errmessage: response.data.message});
} else {
this.setState({redirect: true});
}
})
.catch((error) => {
this.setState({error: true, errmessage: 'Error logging in'});
});
}
I figure there must be some way to have React store the session somehow (?) but I'm fairly new to React and don't really know how to use it with an express backend.
Your axios request from your React client needs to be sent withCredentials. To fix it, either do axios.defaults.withCredentials = true; or do axios.get('url', {withCredentials: true})...Also in your expressjs backend, set cors options credentials: true
Here is an example of setting up express-session using connect-redis. First, setup both express-session and the Redis store.
var session = require('express-session);
var RedisStore = require('connect-redis')(session);
Then, where you're declaring middleware for your app;
app.use(session({
store: new RedisStore({
url: process.env.REDIS_URL
}),
secret: process.env.REDISSECRET,
resave: false,
saveUninitialized: false
}));
now when you use req.session.{etc} this writes to your Redis DB.
I'm running webpack-dev-server on localhost:8000 for my app and express+socket.io on port 3000 for my api. I've proxied requests to socket.io in webpack.config.js as such:
devServer: {
proxy: {
'/socket.io': {
target: 'http://localhost:3000',
ws: true
}
}
}
However, not only do the session ids in express and socket.io don't match, the session id in express changes every request:
Server:
let app = require('express')();
let session = require('express-session')({
secret: 'panopticon',
resave: true,
saveUninitialized: true
});
let server = require('http').createServer(app);
let io = require('socket.io')(server);
//session middleware
app.use(session);
io.use(require('express-socket.io-session')(session, {
autoSave: true
}));
let i=0;
app.get('/socket.io', (req, res) => {
console.log(i++, req.session.id);
//0 'ShgnU91kCZzC7xHP9B57ZtsCbwi3XjdB'
//1 'qLsYYpRZXpyoUrcKzF6K7uoAIKtE9oCh'
res.send();
});
io.on('connection', socket => {
console.log(socket.handshake.session.id);
//MRUYZMVstMh6ssNrq9LP-Z4vTaT5SZcs
});
Client:
//connect to socket
let socket = io();
//make two requests to /socket.io
fetch('socket.io').then(() => fetch('socket.io'));
The only way I got this to work was to do an AJAX request first to localhost:3000:
fetch('http://127.0.0.1:3000', {
credentials: 'include'
});
With the following handler on the response:
app.use('/', (req, res) => {
res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:8080');
res.header('Access-Control-Allow-Credentials', 'true');
res.sendStatus(200);
});
GitHub Gist