I have an express app with passportJS local strategy authentication. The app is deployed to Heroku's free tier (*.herokuapp.com
). In production, with cookie.secure
set to true
, over https, deserializeUser
is never called and cookie is not sent to the client.
I know this question has been asked a lot, and I have been looking for a while now. What drives me crazy is that after two days of implementing different solutions, like in this post for example, I finally got it to work yesterday. But now it's not working again. I can see the user and sessions objects in the log, but deserializeUser
isn't called, req.isAuthenticated()
return false anywhere outside passport.authenticate()
, and there is no cookie in the client.
Additional information that might be useful is that I'm using Knex Session Store for storing sessions, and i'm using PostgreSQL with Knex.
Here's the relevant code:
servsr.js
const express = require('express');
const helmet = require('helmet');
const bodyParser = require('body-parser');
const cors = require('cors');
const passport = require('passport');
require('dotenv').config();
const dev_env = process.env.LOCAL_DEV;
const knex = require('knex')({
client: `pg`,
connection: {
host: dev_env ? process.env.DEV_DB_HOST : null,
user: dev_env ? process.env.DEV_USER : null,
password: dev_env ? process.env.DEV_PASSWORD : null,
database: dev_env ? process.env.DEV_DB_NAME : null,
connectionString: dev_env ? null : process.env.DATABASE_URL,
ssl: dev_env ? null : true
}
});
const app = express();
app.set('trust proxy', true);
app.use(cors({ origin: true, credentials: true }));
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'Origin, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-Response-Time, X-PINGOTHER, X-CSRF-Token, Authorization');
if (req.method === "OPTIONS") {
return res.status(200).end();
} else {
next();
}
});
app.use(helmet());
app.use(bodyParser.json({ limit: '5mb' }));
require('./passport')(app);
const signin = require('./controllers/signin');
const uploadData = require('./controllers/uploadData');
const getData = require('./controllers/getData');
app.get('/', (req, res) => { res.send('its working!'); })
app.post('/signin', (req, res, next) => { signin.handleSignin(req, res, next, passport) })
// app.get('/success', (req, res) => res.status(200).json(true))
// app.get('/fail', (req, res) => res.status(400).json('fail'))
app.get('/log-out', (req, res) => {
req.logOut();
res.clearCookie(process.env.COOKIE_SECRET);
res.status(200).json('logged out ');
})
app.post('/upload-data', (req, res) => {
console.log('upload-data req.headers.cookie: ', req.headers.cookie)
console.log('upload-data req.protocol: ', req.protocol)
if (req.isAuthenticated()) {
uploadData.handleUploadData(req, res, knex)
} else {
res.status(400).json('request is not authenticated');
}
})
app.get('/get-data', (req, res) => {
console.log('get-data req.headers.cookie: ', req.headers.cookie)
console.log('get-data req.protocol: ', req.protocol)
if (req.isAuthenticated()) {
getData.handleDataRequest(req, res, knex)
} else {
res.status(400).json('request is not authenticated');
}
})
app.listen(process.env.PORT, () => {
console.log(`listening on port ${process.env.PORT}`);
})
passport.js
const session = require('express-session');
const passport = require('passport');
const bcrypt = require('bcrypt');
const KnexSessionStore = require('connect-session-knex')(session);
const dev_env = process.env.LOCAL_DEV;
const knex = require('knex')({
client: `pg`,
connection: {
host: dev_env ? process.env.DEV_DB_HOST : null,
user: dev_env ? process.env.DEV_USER : null,
password: dev_env ? process.env.DEV_PASSWORD : null,
database: dev_env ? process.env.DEV_DB_NAME : null,
connectionString: dev_env ? null : process.env.DATABASE_URL,
ssl: dev_env ? null : true
}
});
const store = new KnexSessionStore({ knex });
module.exports = (app) => {
app.use(session({
secret: process.env.COOKIE_SECRET,
saveUninitialized: false,
resave: false,
rolling: true,
store,
cookie: {
httpOnly: true,
maxAge: 1000 * 60 * 60 * 2, // 2 hours
secure: true,
// domain: process.env.DOMAIN,
path: '/'
}
}));
app.use(passport.initialize());
app.use(passport.session());
const LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy((username, password, done) => {
knex.select('id', 'name', 'hash').from('login')
.where('name', '=', username)
.then(user => {
if (!user) {
return done(null, false);
}
const isValid = bcrypt.compareSync(password, user[0].hash);
if (!isValid) {
return done(null, false);
}
return done(null, user[0]);
})
.catch(err => {
console.log(err);
return done(err);
})
}));
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
console.log('deserialize!')
knex.select('id').from('login')
.where('id', '=', id)
.then(id => {
done(null, id)
})
.catch(err => {
console.log(err)
done(err, null)
});
});
}
signin.js
const handleSignin = (req, res, next, passport) => {
console.log('signin req.headers.cookie: ', req.headers.cookie);
console.log('signin req.protocol: ', req.protocol);
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json('Incorrect form submission');
}
passport.authenticate('local', (err, user) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(400).json('fail');
}
req.logIn(user, (err) => {
if (err) {
return next(err);
}
return res.status(200).json(true);
});
})(req, res, next)
}
module.exports = {
handleSignin
};
and here's my package.json
{
"name": "symbiosis-server",
"version": "1.0.0",
"description": "Server for industrial symbiosis data analysis app.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon server.js"
},
"author": "Ori Perelman",
"license": "ISC",
"devDependencies": {
"nodemon": "^1.19.1"
},
"dependencies": {
"bcrypt": "^3.0.6",
"body-parser": "^1.19.0",
"connect-session-knex": "^1.4.0",
"cors": "^2.8.5",
"dotenv": "^8.0.0",
"eslint": "^6.1.0",
"express": "^4.17.1",
"express-session": "^1.16.2",
"helmet": "^3.20.0",
"knex": "^0.19.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"pg": "^7.11.0"
}
}
As I said, this was working just yesterday and now it's not without changing a thing! So I'm really lost and would greatly appreciate any help. Thanks!
Update (25/8/19):
So after many attempts to find the problem I narrowed it down to cookie.domain
in the express-session
set-up. With cookie.domain
set, the cookie is set by the session and I can see it in the database, but for some reason it is not received by the client. Another thing I can see is that with cookie.domain
set, deserializeUser()
never gets called.
As for the authentication working at first and then breaking, the only explanation I could come up with is that I apparently forgot to delete an old cookie somewhere and it kept working until it expired...
I modified the code a little bit, and it's updated above. One change I made was adding app.set('trust proxy', true)
, because I noticed that req.protocol
returns http
instead of https
. It fixed the protocol, but not the problem.
So I guess I'll open (another) issue for this problem with express-session. But if anyone has any idea why cookie.domain
breaks authentication, or even an idea on how to fix it I'll be more than happy to hear it.
Thanks, and I hope this can help someone.