Express session not storing session - express

So I'm building a web application using isomorphic-redux and React.js. I'm currently trying to get basic authentication working using passport in Node.js. However I have hit a problem in which my cookie session does not seem to be loaded in browser when I try to login.
Here is a cut down version of the code I've written so far.
Server.js
import Express from 'express';
import passport from 'passport';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import expressSession from 'express-session';
import serverConfig from './config';
const app = new Express();
// Apply body Parser and server public assets and routes
app.use(cookieParser());
app.use(bodyParser.json({ limit: '20mb' }));
app.use(bodyParser.urlencoded({ limit: '20mb', extended: true }));
app.use(expressSession({
secret: serverConfig.sessionSecret,
resave: false,
saveUninitialized: false
}));
import pp from './passport';
pp();
app.use(passport.initialize());
app.use(passport.session());
passport.js
import passport from 'passport';
import mongoose from 'mongoose';
import path from 'path';
import User from './models/user.model';
import local from './strategies/local';
const pp = () => {
// Serialize sessions
passport.serializeUser(function(user, done) {
done(null, user.id);
});
// Deserialize sessions
passport.deserializeUser(function(id, done) {
User.findOne({
_id: id
}, '-salt -password', function(err, user) {
done(err, user);
});
});
local();
}
export default pp;
./strategies/local.js
import passport from 'passport';
import passportLocal from 'passport-local';
import mongoose from 'mongoose';
const LocalStrategy = passportLocal.Strategy;
const User = mongoose.model('User');
const local = () => {
passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'
},
(username, password, done) => {
User.findOne({ username: username }, (err, user) => {
if (err) {
return done(err);
}
if (!user) {
return done(null, false, {
message: 'Unknown user or invalid password'
});
}
if (!user.authenticate(password)){
return done(null, false, {
message: 'Unknown user or invalid password'
});
}
return done(null, user);
});
}
));
}
export default local;
When I login in it is able to serialise the session, however since I'm guessing, the session does not get stored, it does not deserialise the session.

If you want the sessions to be persistent, you need to tell the express-session module where to store them. First, you need to build a store:
var MongoStore = require('connect-mongo')(expressSession);
var oneHour = 3600;
var sessionStore = new MongoStore({
url: 'mongodb://localhost:27017/my-session-store',
touchAfter: oneHour
});
This creates a mongoDB store using connect-mongo - I'm sure this can be done with mongoose as well but I don't know how so I'll leave it as an exercise to the reader :-)
Then you can tell the express-session module to use this store:
app.use(expressSession({
secret: serverConfig.sessionSecret,
store: sessionStore, // <---- added store information!
resave: false,
saveUninitialized: false
}));

Related

Using Passport.js, cookie not persisting from Heroku Rest API backend to Netlify React frontend

So I feel like I've tried everything.
Quick workflow breakdown. I have a React App deployed on Netlify that uses a Rest API backend hosted on Heroku with a Jaws MySQL instance and a Redis session store. There is an authorization workflow that uses a google0auth2.0 strategy and passport.js as well as the Redis to store each separate session which I only started using on the production build as my understanding was that express-sessions alone would cause memory leaks. That was my first step through this rabbit hole.
After some research, I added app.set('trust proxy', 1), added app.use(helmet.hsts()) to address headers issues and I attempted to secure the cookie with the code below:
app.use(
expressSession({
...
proxy: isProdEnv,
cookie: {
secure: isProdEnv
}
When checking the network tab in chrome dev tools, I see the cookie is attached to the callback however, it is not attached to the dashboard react page that the API redirects on the successful authorization.
My API index.js is below:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const expressSession = require('express-session');
const {
createClient
} = require("redis");
let RedisStore = require('connect-redis')(expressSession);
require('dotenv').config();
const PORT = process.env.PORT || 5050;
const isProdEnv = process.env.NODE_ENV === 'production' ? true : false;
// Knex instance for DB managment
const knex = require('knex')(require('./knexfile.js')[process.env.NODE_ENV || 'development']);
const app = express();
app.use(express.json());
app.use(helmet());
app.use(helmet.hsts());
app.use(
cors({
origin: true,
credentials: true,
}),
);
let redisClient = createClient({
url: process.env.REDIS_URL,
lazyConnect: true,
showFriendlyErrorStack: true,
legacyMode: true,
retry_strategy: (options) => {
const {
error,
total_retry_time,
attempt
} = options;
if (error ? .code === 'ECONNREFUSED' || error ? .code === 'NR_CLOSED') {
return 5000;
}
if (total_retry_time > 1000 * 15) {
return undefined;
}
if (attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 1000, 5000); //in ms
},
});
if (!redisClient.isOpen) {
redisClient.connect().catch(console.error);
console.info('connected to redis at', process.env.REDIS_URL);
}
redisClient.on('error', (err) => {
console.log('ⓘ on error:', err);
});
app.set('trust proxy', 1);
// Include express-session middleware (with additional config options required
// for Passport session)
app.use(
expressSession({
store: new RedisStore({
client: redisClient
}),
secret: process.env.SESSION_SECRET,
proxy: isProdEnv,
resave: false,
saveUninitialized: true,
name: 'lostnfound',
cookie: {
secure: isProdEnv
}
}),
);
// =========== Passport Config ============
// Initialize Passport middleware
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: ['profile', 'email'],
},
(_accessToken, _refreshToken, profile, done) => {
const id = String(profile.id);
const profileId = Number(id.slice(-18));
// First let's check if we already have this user in our DB
knex('users')
.select('id')
.where({
google_id: profileId
})
.then((user) => {
if (user.length) {
// If user is found, pass the user object to serialize function
done(null, user[0]);
} else {
// If user isn't found, we create a record
knex('users')
.insert({
google_id: profileId,
avatar_url: profile._json.picture,
first_name: profile.name.givenName,
last_name: profile.name.familyName,
email: profile._json.email,
})
.then((userId) => {
// Pass the user object to serialize function
done(null, {
id: userId[0]
});
})
.catch((err) => {
console.log('Error creating a user', err);
});
}
})
.catch((err) => {
console.log('Error fetching a user', err);
});
},
),
);
// `serializeUser` determines which data of the auth user object should be stored in the session
// The data comes from `done` function of the strategy
// The result of the method is attached to the session as `req.session.passport.user = 12345`
passport.serializeUser((user, done) => {
console.log('serializeUser (user object):', user);
// Store only the user id in session
done(null, user.id);
});
// `deserializeUser` receives a value sent from `serializeUser` `done` function
// We can then retrieve full user information from our database using the userId
passport.deserializeUser((userId, done) => {
console.log('deserializeUser (user id):', userId);
// Query user information from the database for currently authenticated user
knex('users')
.where({
id: userId
})
.then((user) => {
// Remember that knex will return an array of records, so we need to get a single record from it
console.log('req.user:', user[0]);
// The full user object will be attached to request object as `req.user`
done(null, user[0]);
})
.catch((err) => {
console.log('Error finding user', err);
});
});
// Additional information on serializeUser and deserializeUser:
// https://stackoverflow.com/questions/27637609/understanding-passport-serialize-deserialize
// Import all route types for server functionality
const authRoutes = require('./routes/auth');
const postRoutes = require('./routes/post');
app.use('/auth', authRoutes);
app.use('/post', postRoutes);
app.listen(PORT, () => {
console.log(`🚀 Server listening on port ${PORT}.`);
});
And this is my google endpoint and google callback:
const express = require('express');
const router = express.Router();
const passport = require('passport');
require('dotenv').config();
const AuthController = require('../controller/auth');
// Create a login endpoint which kickstarts the auth process and takes user to a consent page
router.get('/google', passport.authenticate('google'));
// This is the endpoint that Google will redirect to after user responds on consent page
router.get(
'/google/callback',
passport.authenticate('google', {
failureRedirect: `${process.env.CLIENT_URL}/auth-fail`,
}),
(_req, res) => {
// Successful authentication, redirect to client-side application
res.redirect(`${process.env.CLIENT_URL}/dashboard`);
},
);
I will add, I'm on the free Heroku account and haven't set up any SSL. Is that what is holding me back? is there any third-party free SSL that I can work into the workflow?
It's hosted here if you want to get a first-hand look. Click on the login to google to go through the workflow and checkout the network tab after you're authorized (if you dare, lol)

req.isAuthenticated() returns FALSE (express-session, passport-local)

Edit: I got the cookie to be set by transferring app.use(session({...})) from register.js to app.js before I declare use of the router in app.js. However, the problem - req.isAuthenticated() returning false, still persists.
I'm trying to build a sign up flow using passport-local, express-session and mongodb for my backend.
When I register a new user, it seems to work fine and the user gets created in my database as desired. I can also access req.user in my passport.authenticate() callback post registration. However, when I redirect to a new route using res.redirect(), req.isAuthenticated() returns false on my new route.
Upon checking the cookies in my browser, it seems that express-session never saves the session as it is supposed to. There is no cookie from my localhost. I've done hours of research, and most answers I've come across just relate to double-checking the sequence of code when initializing. I've done this countless times, as well as tried to use both passport.serializeUser(User.serializeUser()); and passport.serializeUser(function(user, done) { done(null, user.id); }); to make sure I'm doing that step correctly. I can't seem to figure out why I'm having this issue. Any and all help will be deeply appreciated! Relevant snippets from my code -
Register.js (Router)
var express = require('express');
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const passportLocalMongoose = require('passport-local-mongoose');
var router = express.Router();
const app = express();
app.use(bodyParser.urlencoded({extended: true}));
app.use(session({
secret:"Edit Later.",
resave:true,
saveUninitialized:true
}));
app.use(passport.initialize());
app.use(passport.session());
const userSchema = new mongoose.Schema({
name:String,
username:String,
contactNumber:String
});
userSchema.plugin(passportLocalMongoose);
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
router.post('/', function(req, res, next) {
const reCaptchaKey = "<MYKEY>";
var userCaptchaToken = req.body['g-recaptcha-response'];
axios.post('https://www.google.com/recaptcha/api/siteverify', null, {params: {
secret: reCaptchaKey,
response: userCaptchaToken
}}).then(response => {
if (response.data.success === true) {
User.register({username: req.body.username, name: req.body.fullName}, req.body.password,
function(err, user) {
if (err) {
console.log(err);
// res.redirect('/views/register.html');
} else {
passport.authenticate('local', {session:true})(req, res, function() {
console.log(req.user);
console.log(req.session);
res.redirect('/profile');
console.log("Done!");
});
}
});
} else if (response.data.success === false) {
console.log("Captcha failed.");
}
}).catch(error => {
console.log(error);
});
});
module.exports = router;
Profile.js (Router)
const express = require('express');
const router = express.Router();
router.get('/', function(req, res) {
if (req.isAuthenticated()) {
res.render('profile')
} else {
res.redirect('/register')
console.log('Not a user.')
}
})
module.exports = router;
App.js
var registrationRouter = require('./routes/register');
var profileRouter = require('./routes/profile');
app.use('/register', registrationRouter);
app.use('/profile', profileRouter);
module.exports = app;
So after hours of trying, I figured it out. My issue was I used the following code in Register.js
app.use(session({
secret:"Edit Later.",
resave:true,
saveUninitialized:true
}));
app.use(passport.initialize());
app.use(passport.session());
This was a problem because Register.js was being used as a router, and I directed all requests to this route via App.js. This led to the app not being configured properly before the requests were handled by Register.js. The above lines are configuration code and MUST be written before app.use('/register', registrationRouter); in App.js. Thus, the fix was simply to move these lines over to App.js like so -
\\configure App
app.use(session({
secret:"Edit Later.",
resave:true,
saveUninitialized:true
}));
app.use(passport.initialize());
app.use(passport.session());
\\handle router
app.use('/register', registrationRouter);

express-session not saving data

I have a simple MEAN app and I want to implement a simple "home-made" user authentication. My idea is to save the userId in the session when he logs in, and to check if userId exists in the session on each page request (for example, when getting the list of all users).
Backend - server.js:
const express = require("express");
const session = require("express-session");
const bodyParser = require("body-parser");
const cors = require("cors");
const app = express();
var MemoryStore = session.MemoryStore;
app.use(
session({
name: "app.sid",
secret: "my_s3cr3t",
resave: true,
store: new MemoryStore(),
saveUninitialized: true
})
);
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cors());
const dbConfig = require("./config/database.config.js");
const mongoose = require("mongoose");
mongoose.Promise = global.Promise;
mongoose
.connect(dbConfig.url)
.then(() => {
// ...
})
.catch(err => {
// ...
process.exit();
});
require("./app/routes/user.routes.js")(app);
require("./app/routes/task.routes.js")(app);
require("./app/routes/login.routes.js")(app);
app.listen(3333, () => {
console.log("Server is listening on port 3333");
});
When a user clicks the Login button, a method from the frontend controller is called:
Frontend - login.controller.js:
vm.login = function() {
userService.getUserByUsername(vm.username).then(user => {
if (user.password === vm.password) {
console.log("Login ok");
loginService.login(user).then(($window.location.href = "/#!main"));
} else {
console.log("Login not ok");
}
});
};
Backend - login.controller.js:
exports.login = (req, res) => {
req.session.userId = req.body._id;
req.session.save(function(err) {
console.log(err); // prints out "undefined", so there's no error
});
console.log(req.session);
res.status(200).send({
message: "Login ok"
});
};
The frontend LoginController prints out "Login ok" (assuming that I entered correct credentials) and redirects me to the "main" page which uses main.controller.js:
In the meantime, the backend login controller prints out the following:
Session {
cookie:
{ path: '/',
_expires: null,
originalMaxAge: null,
httpOnly: true },
userId: '5b4746cafe30b423181ad359' }
So there is definitely a userId in the session content. However, when I get redirected to the main.html and the main.controller.js gets invoked, it calls:
loginService.getSession().then(data => console.log(data));
(I just want to check if the userId is still in the session, and later I will perform some useful actions)
The getSession() method in the frontend LoginService only does the $http call:
function getSession() {
return $http.get("http://localhost:3333/session").then(
function(response) {
return response.data;
},
function(error) {
console.log(error.status);
}
);
}
This one calls the method which is defined in the backend LoginController:
exports.getSession = (req, res) => {
console.log(req.session);
if (req.session.userId) {
res
.status(200)
.send({ message: "Session existing with userId " + req.session.userId });
} else {
res.status(404).send({ message: "Session not existing" });
}
};
The frontend call prints the status code 404 in the console, while in the backend I get the following output:
Session {
cookie:
{ path: '/',
_expires: null,
originalMaxAge: null,
httpOnly: true } }
(no userId is present...)
One more thing... In a few tutorials I saw that they are using cookie-parser. However, when I try to use it, I don't get any data from my database, only the static text is displayed. So I removed it temporarily from server.js.
EDIT:
I tried adding MongoStore to my app:
const MongoStore = require("connect-mongo")(session);
...
app.use(
session({
name: "app.sid",
secret: "G4m1F1c4T10n_#ppL1c4t10N",
resave: true,
saveUninitialized: false,
cookie: { maxAge: 600000 },
store: new MongoStore({ url: "mongodb://localhost:27017/myAppDb" })
})
);
...but nothing changed.
How can I get my sessions to work?
As I found out after talking to several people, sessions are more or less deprecated and the new way of handling these things are the tokens. So I switched to JWT and it's working great.

Express: unable to access route from browser due to accept:application/javascript header missing

I'm new to express. I have a Vue application running on express. I have some API routes that I'm able to access using axios through the browser. To access those routes using postman I have to have the header:
accept: application/javascript
for it to return the result of the actual API. If I don't use this header, I get the generated index.html from webpack. I need to reuse one of these routes to return excel/pdf, based on a parameter and have it accessible via a link on the page.
Here's my server.js - based on https://github.com/southerncross/vue-express-dev-boilerplate
import express from 'express'
import path from 'path'
import favicon from 'serve-favicon'
import logger from 'morgan'
import cookieParser from 'cookie-parser'
import bodyParser from 'body-parser'
import webpack from 'webpack'
const argon2 = require('argon2');
const passport = require('passport')
const LocalStrategy = require ('passport-local')
const session = require('express-session')
import history from 'connect-history-api-fallback'
// Formal(Prod) environment, the following two modules do not need to be introduced
import webpackDevMiddleware from 'webpack-dev-middleware'
import webpackHotMiddleware from 'webpack-hot-middleware'
import config from '../../build/webpack.dev.conf'
const app = express()
app.set('trust proxy', true)
app.set("view engine", "pug")
app.set('views', path.join(__dirname, 'views'))
app.use ('/', require('./routes'))
app.use(session({
secret: process.env.SESSION_SECRET || 'secretsauce',
resave: false,
saveUninitialized: true
}))
app.use(history())
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')))
app.use(logger('dev'))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
extended: false
}))
app.use(cookieParser())
app.use(express.static(path.join(__dirname, 'public')))
const compiler = webpack(config)
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
stats: {
colors: true
}
}))
app.use(webpackHotMiddleware(compiler))
////////// PASSPORT ///////////////////////
app.use (passport.initialize ());
app.use (passport.session ());
async function authenticateUser (username, password) {
//...
}
passport.use (
new LocalStrategy (async (username, password, done) => {
const user = await authenticateUser (username, password)
if (!user) {
return done (null, false, {
message: 'Username and password combination is wrong',
});
}
delete user.password;
return done (null, user)
})
);
// Serialize user in session
passport.serializeUser ((user, done) => {
done (null, user);
});
passport.deserializeUser (function(user, done) {
if(user === undefined || !user || Object.keys(user).length === 0)
return done(null, false)
else
done (null, user);
});
//////////// passport end ///////////////
app.set("view engine", "pug")
app.use(express.static(path.join(__dirname, 'views')))
app.get('/', function (req, res) {
res.sendFile('./views/index.html')
})
app.get('/success', function (req, res) {
res.render('./views/success')
})
app.use ('/api', require('./api'))
// catch 404 and forward to error handler
app.use(function (req, res, next) {
var err = new Error('Not Found')
err.status = 404
next(err)
})
app.use(function (err, req, res) {
res.status(err.status || 500)
res.send(err.message)
})
let server = app.listen(80)
export default app
And here's a bit of api.js
const {Router} = require ('express')
const router = Router()
router.get('/whome', function(req, res){
logger.info('whome', req.user)
return res.json(req.user)
})
router.get ('/hello', auth.isAuthenticated, async (req, res) => {
res.json ({text:'hello'})
})
module.exports = router
I can call http://localhost/api/hello from postman with the accept:application/javascript header and I get:
{
"text": "hello"
}
as expected. But if I call the same URL from the browser (and it's not sending that header), I get the created bundle index.html. How can I access these routes from the browser?
You have two options.
First one, try to add this in your server:
app.options('*', cors())
before to: app.set("view engine", "pug")
If that doesnt work, try to install this addon in your Google Chrome browser to test.
Allow-Control-Allow-Origin: *
And enable it. (The icon should be green instead of red).
Why this happens?
The request that's being made is called a preflight request.
Preflight requests are made by the browser, as CORS is a browser security restriction only - This is why it works in Postman, which is, of course, not a browser.
Reference: Preflight request

How can I authenticate a GraphQL endpoint with Passport?

I have a GraphQL endpoint:
app.use('/graphql', graphqlHTTP(request => ({
graphiql: true,
schema
})));
I also have a Passport route for logging in (and handling the callback, since I'm using Google OAuth2):
this.app.get('/login', passport.authenticate('google'));
this.app.get('/auth/callback/google', ....
Passport add a user to the request, and all of the articles I can find online recommend authenticating in each of my GraphQL resolvers using that:
resolve: (root, args, { user }) => {
if (!user) throw new NotLoggedInError();
However it doesn't make sense to have to add that logic to every resolver when it applies to all of them, so I was hoping to somehow authenticate the entire endpoint.
The problem is that I'm not sure how to combine middleware. I tried the following but it just broke the endpoint:
app.use('/graphql', passport.authenticate('google'), graphqlHTTP(request => ({
graphiql: true,
schema
})));
I have the following working. Some issues I had were around making sure my google API was enabled and the proper scopes were enabled. I am also only using the passport middleware on the auth endpoints and using an isAuthenticated middleware to check if the session is authenticated and if not redirect to the auth endpoint. also putting the request object into the context so that it can be used by the resolver to potentially authorize the user. You would of course need to update the user lookup as I am just passing mock data.
import express from "express";
import graphqlHTTP from "express-graphql";
import passport from "passport";
import cookieParser from "cookie-parser";
import session from "express-session";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { buildSchema } from "graphql";
const PORT = 5000;
const data = [
{ id: "1", name: "foo1" },
{ id: "2", name: "foo2" },
{ id: "3", name: "foo3" },
];
const def = `
type Foo {
id: String!
name: String
}
type Query {
readFoo(id: String!): Foo
}
schema {
query: Query
}
`;
const schema = buildSchema(def);
const fieldMap = schema.getType("Query").getFields();
fieldMap.readFoo.resolve = (source, args) => {
return data.filter(({ id }) => id === args.id)[0] || null;
};
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((obj, done) => {
done(null, obj);
});
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `http://localhost:${PORT}/auth/google/callback`,
},
(accessToken, refreshToken, profile, cb) => {
return cb(null, {
id: "1",
username: "foo#bar.baz",
googleId: profile.id,
});
}
)
);
function isAuthenticated(req, res, next) {
return req.isAuthenticated() ? next() : res.redirect("/auth/google");
}
const app = express();
app.use(cookieParser());
app.use(
session({
secret: "sauce",
resave: false,
saveUninitialized: false,
})
);
app.use(passport.initialize());
app.use(passport.session());
app.get("/auth/fail", (req, res) => {
res.json({ loginFailed: true });
});
app.get(
"/auth/google",
passport.authenticate("google", { scope: ["profile"] })
);
app.get(
"/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/auth/fail" }),
(req, res) => {
res.redirect("/graphql");
}
);
app.use(
"/graphql",
isAuthenticated,
graphqlHTTP((req) => ({
schema,
graphiql: true,
context: req,
}))
);
app.listen(PORT, () => {
console.log("Started local graphql server on port ", PORT);
});
vbranden's answer was excellent, and it is the basis of this answer. However, his answer has a lot of other code which obfuscates the solution a bit. I didn't want to mess with it, since it offers a more complete view of things, but hopefully this answer will be helpful in its own way by being more direct. But again, all credit for this solution belongs to vbranden (please upvote his answer accordingly).
If you make an isAuthenticated function with the appropriate signature (request, response, next) you can then "chain" that function in when you setup your GraphQL endpoint:
function isAuthenticated(req, res, next) {
return req.isAuthenticated() ?
next() :
res.redirect('/auth/google');
}
app.use(
'/graphql',
isAuthenticated,
graphqlHTTP(req => ({
schema,
graphiql: true,
context: req
}))
);