How does passportJS deal with session cookies? - express

Initial question:
I'm using passportJS in my backend, and MongoStore as the session store, with a react frontend.
What I'm trying to do is allow the user to have a persistent login session. There's already a cookie that's getting automatically set by express-session called connect.sid. However the session doesn't seem to be persistent because req.user is undefined on subsequent requests using the same cookie that was set on login. req.user keeps returning undefined which means that passport is not verifying the session cookie somehow.
According to passport's docs that shouldn't happen.
Each subsequent request will not contain credentials, but rather the
unique cookie that identifies the session. In order to support login
sessions, Passport will serialize and deserialize user instances to
and from the session.
So what I'm trying to understand is how exactly does passportJS deal with cookies sent by the client?
Can someone help me by explaining the steps that passport takes when it comes to that?
I'm also using passport-local-mongoose plugin in my mongoose model which includes built in authenticate/serialize/deserialize methods used in auth.js below.
Relevant parts of my code for reference:
app.js: (didn't include the full file so it can be clear since it's 100+ lines, if someone suggests it could be an issue of middleware order I'll include the full code)
//session and passport initialization
const sessionStore = new MongoStore({
mongooseConnection: mongoose.connection,
collection: "sessions",
});
app.use(
session({
secret: process.env.SERVER_SECRET_KEY,
resave: false,
saveUninitialized: false,
store: sessionStore,
cookie: {
path:"/",
httpOnly: true,
expires: 9999999999999999
}
})
);
app.use(passport.initialize());
app.use(passport.session());
auth.js
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
const config = require("./config");
const User = require("./models/userModel");
passport.use(
new LocalStrategy({ usernameField: "userName", passwordField: "password" }, User.authenticate())
);
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
exports.userLogin = passport.authenticate("local", {
session: true,
failureRedirect: '/users/loginfailed'
});
users.js (login request which returns the new cookie if successful)
usersRouter
.route("/login")
.post(auth.userLogin, (req, res, next) => {
console.log(req.user); // returns a valid user on login
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.json({ success: true, message: "Login successful!" });
});
movies.js (separate request after login, previous cookie included in headers)
moviesRouter
.route("/top")
.get((req, res, next) => {
console.log(req.user); // undefined
//includes a mongodb query here and a corresponding server response
})
UPDATE (thanks to this answer):
I found the issue, it was as simple as removing the cookie option from the object that was passed to session().
changed to this:
app.use(
session({
secret: process.env.SERVER_SECRET_KEY,
resave: false,
saveUninitialized: false,
store: sessionStore,
})
);
But I still don't understand why Passport likes to ignore my cookie options. What if I want to use secure or disable httpOnly or anything else I may want to try with my cookies?

Related

HttpOnly cookie appears in response header but is not being saved to the browser

I recently built a simple real-time chat application with Nextjs on the frontend and Express on the backend. The frontend is deployed on vercel while the backend is deployed on heroku. When a user logs into the app, the backend generates a jwt token which is then sent via an HttpOnly cookie back to the frontend. Here is the code for said response:
const authenticate = async (req, res, next) => {
userService
.authenticate(req)
.then((user) => {
const { token, ...userInfo } = user;
res
.setHeader(
"Set-Cookie",
cookie.serialize("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV !== "development",
maxAge: 60 * 60 * 24,
sameSite: "none",
path: "/",
})
)
.status(200)
.json(userInfo);
})
.catch(next);
};
After authentication, each subsequent request to the backend is supposed to send the token to ensure the user is logged in. For example, this is the request sent to the server to get a chat between the logged in user and another user.
const getChat = async (id) => {
const identification = id;
const response = await axios.get(
`<SERVER_URL>/chats/chat/${identification}`,
{ withCredentials: true }
);
return response;
};
In development when on localhost:3000 for the frontend and localhost:4000 for the backend, everything works fine. However, when I deployed the frontend to vercel and the backend to heroku, the browser simply refuses to save the cookie! The jwt token appears in the response header after sending the authentication request, but it isn't saved to the browser. I have tried absolutely everything I can think of, including changing the cookie parameters, but I can't get it to work. I am pretty sure I have cors properly configured on the backend as well, along with the cookie-parser module:
const cors = require("cors");
const cookieParser = require("cookie-parser");
app.use(
cors({
origin: "<CLIENT_URL>",
credentials: true,
})
app.use(cookieParser());
Thanks for taking the time to read this, any help would be greatly appreciated! And my apologies if I have not elaborated enough, this is my first post here and I'm still trying to learn the proper etiquette of the site!
HttpOnly can not read or write on client-side but when the first HttpOnly send through a request other request on the same origin can access the coockies in backend but you should request in Next.js like this.
Next.js using fetch :
const req = await fetch("http://localhost:7000/api/auth/login", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Credentials": true,
},
body: JSON.stringify({
email: formData.get("email"),
password: formData.get("password"),
}),
});
const data = await req.json();
then in backend you can read the coockie through coockie-parser
server.js:
const cookieParser = require("cookie-parser");
app.use(coockieParser());
route.post('/login',(req,res) => {
if(user){
res
.cookie("access_token", newToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production" ? true : false,
})
.status(200)
.json({ ok: true, payload: data });
}
})
Now you can read this cookie in other routes but sure about the expiration time.

Cookie is sent, but not stored when deployed

I'm having issues with cookies in a MERN app hosted to Vercel (Front-end) and Heroku (Back-end).
Everything is working fine in localhost, but when deployed i'm having issues storing cookies. Set-Cookie is sent with the sign in request, and the request itself looks fine. I get no errors.
After being signed in, authenticated routes return what I expect for each user, so the cookie seems to be accessible to the back-end even tho the cookie is not stored on the front-end under storage, which I assume is a security issue. This is not the case in Safari, in Safari the cookies are gone after login, so the user is logged out again.
The only route that does not work is signing out. On sign out I clear the token cookie, but it is trying to delete something that technically isn't there, still no errors. UPDATE: Clearing cookie works! I wasn't sending any data back with the sign out, so the front-end thought it was unresponsive, adding a .send({ message: 'Sign Out Successful' }) solved that issue. However, still no cookie visible in the front-end storage.
For CORS settings I have the origin set to the front-end url, and credentials set to true. While on the front-end I have withCredentials: true set on every request.
When deployed, the cookie is using sameSite: none, and secure: true.
Below are the sign in and sign out routes, but you can find the full back-end code here, and the front-end code here on GitHub.
Sign in route on back-end
import type Route from '../../types/Route'
import jwt from 'jsonwebtoken'
import env from '../../env/env'
import bcrypt from 'bcryptjs'
import User from '../../models/User'
import validateSignIn from '../../utils/validation/signIn'
const route: Route = {
method: 'post',
execute: async (req, res) => {
const { username, password } = req.body.user
try {
const { errors, valid } = validateSignIn(username, password)
if (!valid) return res.status(401).send({ errors })
const user = await User.findOne({ username })
if (!user) return res.status(404).send({ errors: { username: 'User not found' }})
const correctPassword = await bcrypt.compare(password, user.password)
if (!correctPassword) return res.status(401).send({ errors: { password: 'Wrong password' } })
const token = jwt.sign({ userId: user._id }, env.SECRET, { expiresIn: "1hr" })
return res.status(200).cookie('token', token, {
expires: new Date(Date.now() + 604800000),
secure: env.ENVIRONMENT === 'LIVE',
sameSite: env.ENVIRONMENT === 'LIVE' ? 'none' : 'lax',
httpOnly: true
}).send(user)
} catch (error) {
console.log('#sign/in', error)
return res.sendStatus(500)
}
}
}
export default route
Sign out route on back-end:
import type Route from '../../types/Route'
import authorization from '../../middlewares/http'
import env from '../../env/env'
// TODO: Fix Sign Out
const route: Route = {
method: 'get',
authorization,
execute: async (req, res) => {
try {
return res.clearCookie('token').sendStatus(200)
} catch (error) {
console.log('#sign/out', error)
return res.status(500)
}
}
}
export default route
The .clearCookie() doc says
Web browsers and other compliant clients will only clear the cookie if the given options is identical to those given to res.cookie(), excluding expires and maxAge.
So, try making your clear operation match your set operation, something like this.
res
.clearCookie('token', token, {
secure: env.ENVIRONMENT === 'LIVE',
sameSite: env.ENVIRONMENT === 'LIVE' ? 'none' : 'lax',
httpOnly: true })
.sendStatus(200)

req.isAuthenticated() is always false

My authentication function using passportjs will always return false even though the user exists already and it will always redirect to the login page and this is overwriting all my authentication routes, so when I log in with a valid user credential or create a new user, the default behavior is to redirect to the 'secret page' but that is only redirecting to the login page every time.
I don't know what I am doing wrong here guys, I need ur help, please...
I have seen other related questions, but most of the threads aren't really answering the questions, or the answers that looks like a solution are not working even though I applied it, as they should I am still confused about what to do to make this work.
I have written a simple app to authenticate user login signup and logout using routes and passportjs.
My last piece of code is setup to only allow user access to the contents of the main site which is called a secret template in this case only if the user is a valid user (that is they are logged in or have successfully signed up).
The function I have created to do that looks like this:
// Authenticate user Login
function isLoggedIn(req, res, next) {
if(req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}
and this basically was supposed to check if a user was already logged in.
and then I called the function as a middleware in one of my routes:
app.get('/secret', isLoggedIn , (req, res)=>{
res.render('secret');
});
This is supposed to make sure that the user is logged in or have signed up before they get access to the secret page, otherwise, it should return the login page and require that the user is logged in or has signed up to gain access to the secret page.
This is my full code just in case, you have a spotty eyes keener than mine.
var express = require('express'),
app = express(),
mongoose = require('mongoose'),
bodyParser = require ('body-parser'),
User = require('./models/user'),
passport = require('passport'),
localStrategy = require('passport-local'),
passportLocalMongoose = require('passport-local-mongoose');
mongoose.connect('mongodb://localhost/auth_demo_app', {
useNewUrlParser: true
});
app.set('view engine', 'ejs');
app.use(express.static(__dirname + '/public'));
app.use(bodyParser.urlencoded({extended: true}));
app.use(passport.initialize());
app.use(passport.session());
app.use(require("express-session")({
secret: "Rusty is the worst and ugliest dog in the wolrd",
resave: true,
saveUninitialized: true
}));
passport.use(new localStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
// ==================================================
// ROUTES
// ==================================================
app.get('/', (req, res)=>{
res.render('home');
});
app.get('/secret',isLoggedIn, (req, res)=>{
res.render('secret');
});
// AUTH ROUTES
// Register - Show Registration form
app.get('/register', (req, res)=>{
res.render('register');
});
// Handle user Signup
app.post('/register', (req, res)=>{
req.body.username
req.body.password
User.register(new User({username: req.body.username}), req.body.password, (err, user)=>{
if(err){
console.log(err);
return res.render('register');
}
passport.authenticate('local')(req, res, ()=>{
res.redirect('/secret');
})
})
});
// Login - Show Login form
app.get('/login', (req, res)=>{
res.render('login');
});
// Handle user Signup
app.post('/login', passport.authenticate('local', {
successRedirect: '/secret',
failureRedirect: '/login',
}),(req, res)=>{
// Other stuff goes here
});
// LOGOUT ROUTE
// Logs user out - ends user session
app.get('/logout', (req, res)=>{
req.logOut();
res.redirect('/');
});
// Authenticate user Login
function isLoggedIn(req, res, next) {
if(req.isAuthenticated()) {
console.log('User logged in successfully');
return next();
}
res.redirect('/login');
}
app.listen(3000, ()=>{
console.log('Server Started...');
});
console.log(req.isAuthenticated()) // Is always returning false.
Try changing the order of
app.use(passport.initialize());
app.use(passport.session());
app.use(require("express-session")({
secret: "Rusty is the worst and ugliest dog in the wolrd",
resave: true,
saveUninitialized: true
}));
to
app.use(require("express-session")({
secret: "Rusty is the worst and ugliest dog in the wolrd",
resave: true,
saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());
If you are using cookies make sure you add cookie-parser middleware
var express = require('express')
var cookieParser = require('cookie-parser')
var app = express()
app.use(cookieParser())
If this is not the case check you calling end, if you are using axios include withCredentials
axios.get('some api url', {withCredentials: true});
if you are uisg fetch make sure to add credentials: 'include'
fetch('/...', {
method: ..,
headers: ...,
credentials: 'include',
body: ...
...})
Starting with version 0.2.1 passport-local-mongoose adds a helper method createStrategy as static method to your schema. The createStrategy is responsible to setup passport-local LocalStrategy with the correct options.
const User = require('./models/user');
// CHANGE: USE "createStrategy" INSTEAD OF "authenticate"
passport.use(User.createStrategy());
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
If you are not using a session middleware e.g., express-session you could use the following to sign the user and turn isAuthenticated() to its actual value:
req.logIn(user, { session: false });

Session Management in Express

I have a express app with express-session, session-file-store and passport. When a user first logs in, server creates a sessionID and creates the corresponding session file. However, I have the following problems:
When the User-Agent or, curl sends a cookie with the connect.sid, I can't lookup the session store for the saved ID. I looked into session-file-store docs and could not find any methods or ways of doing so. Is there a way?
Is there a way to destroy or remove the session at 'logout` request?
Edit
As #jfriend00 suggested, I thought it best to provide more context. As mentioned earlier, I have around 3 middleware for session. This is how they are mounted:
authserver.js
// session options
const sessOpts = {
genid: (req) => uuid(),
store: new sessionFS(),
secret: _secret,
resave: false,
saveUninitialized: true
};
// mws
authservice.use(initLogger);
authservice.use(exprSession(sessOpts));
authservice.use(bodyParser.json());
authservice.use(bodyParser.urlencoded({ extended: false }));
authservice.use(expSanitizer()) // must follow preceeding line
authservice.use(corsHandler);
authservice.disable('x-powered-by');
authservice.disable('etag');
// session mws
authservice.use(passportInst.initialize());
authservice.use(passportInst.session());
This how the passport LocalStrategy is set:
passport_strategies.js
passport.use(new passportLocal(
(username, password, done) => {
//console.log('In local');
mongoHelpers.connect((cerr, db) => {
if(!!cerr) return done(cerr);
mongoHelpers.findUser({
username: username,
password: password
}, db, (err, usrs) => {
if(!!err) return done(err);
if(!!!usrs) return done(null, false);
//if(!validateUser(usrs[0], userSchema)) return done(null, false);
//console.log('local: ' + usrs.toString());
//console.log(usrs);
return done(null, usrs[0]);
});
});
}
));
passport.serializeUser((user, done) => {
console.log('In serializer');
if(!!!user) {
done(new Error("User cant be undefined in serializeUser"), null);
//console.log("user is null");
}
done(null, user._id); // mongo auto id
});
passport.deserializeUser((id, done) => {
console.log('In Dcserializer');
console.log(id);
});
Now there are two sessions exprSession which is an instance of express-session and passport.session(). Are both these instances referring to the same object?
I am looking up the user collection on each authentication strategy for matching pair or username and password. This lookup is also done in another middleware down the chain. Is the second middleware redundant?|
In the serialize user method, I am passing the mongo id. In deserialize, I get a session ID back as id. If this is the session ID, is there a way to use this to destroy session?
When a user logs out, is it a good practice (or even done generally) to remove/unset the previously set cookie even with maxAge or expires set?
I apologize if the question seems somewhat convoluted. I am fairly new to these middleware.

Is there a Passport Strategy to handle an offsite API which isn't OAuth?

I've got a relatively unique use-case here and I can't find the perfect solution.
I have a website that has user log-in via facebook and linkedin, which works flawlessly via passportjs.
However I also need to be able to login via an offsite API which is not OAuth but rather offers a standard JSON response.
I currently use angular to hit the API and return the user object but then I have to store that in sessionStorage to persist the log in. This isn't ideal as it means there is a server session and a local session. To log out I have to manually clear the sessionStorage and this hit a log out path for the server.
What I want to do is create a local log in path on the ExpressJS website and the route then hits the API to log in and then the user is stored in the server session.
In the end I hacked the local strategy.
The key was adding passReqToCallback so I could manipulate the request.
This adds the response to req.user and starts a session.
Strategy:
var passport = require('passport'),
LocalStrategy = require('passport-local').Strategy,
request = require('request')
module.exports = function() {
passport.use(new LocalStrategy({
passReqToCallback: true
},
function(req, username, password, done) {
request.post({
url: 'http://api/',
form: {
username: username,
password: password
},
json: true
}, function(err, httpResponse, body) {
if (err || !body.success) done();
else done(null, body);
});
}
));
};
Route:
app.post('/auth/local',
passport.authenticate('local', {
failureRedirect: '/fail'
}),
function(req, res) {
res.redirect('/success');
});