passport.js deserialization ok, but authentication not working - express

I have a working development version and even my testing version worked until recently. The authentication works sometimes, but mostly the authentication just fails with req.isAuthenticated().
server.js:
var express = require('express');
var app = express();
var port = PORTS[ENVIRONMENT];
var passport = require('passport');
var morgan = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var session = require('express-session');
var passportConfigs = require('./config/passport');
var routes = require('./routes.js');
// App setup
app.use("/", express.static(__dirname + '/public/'));
// configuration ===============================================================
/* open mongo connection */
require('./database/' + ENVIRONMENT + '.js');
/* === passport configs === */
passportConfigs(passport, ENVIRONMENT);
// set up our express application
app.use(morgan(morganEnv)); // log every request to the console
app.use(cookieParser()); // read cookies (needed for auth)
//app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
})); // get information from html forms
app.set('view engine', 'ejs'); // set up ejs for templating
// required for passport
app.use(session({
secret: '********' ,
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
passport serialize:
passport.deserializeUser(function(id, done) {
userQueries.findID(id)
.then(function(user) {
var firebaseRef = firebaseRefMod(user, environment).ref;
if(!firebaseRef) {
throw new Error('problem with generating firebase reference, dunno why!');
}
console.log("UserFirst:", { userData: filterUserData( user ), FBRef: firebaseRef })
done(null, { userData: filterUserData( user ), FBRef: firebaseRef } );
}).then(null, function(err) {
console.log(err);
done(err, null);
});
});
routes:
app.post('/auth', isLoggedIn, function(req, res) {
console.log("req", req.user)
res.status(200).send({ "authKey": authGenerator.createToken(authGenerator.types.NORMAL, req.user.userData) , "user": req.user.userData } );
});
function isLoggedIn(req, res, next) {
console.log("userIn", req.isAuthenticated())
// if user is authenticated in the session, carry on
if (req.isAuthenticated())
return next();
// if they aren't redirect them to the home page
errorInRestRequest(res, errorCodes.userNotAuthenticated, req.user);
return false;
}
So basically if the authentication succeeds as it should, the isLoggedIn-function has access to user data from req.user. Even if the authentication does not success, the user is deserialized without problem every time verified by the "console.log("UserFirst:"... entry), but the req.user does not hold the data when it reaches in isLoggedIn-function.
Unfortunately my knowledge of passport and express middlewares are limited, so I'm puzzled as to where the data vanishes in between.

Seems like the problem was caused by wrong node module versions. Do not have the specifics yet, but reverting several modules to older versions fixed the issue.

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)

Passport user object in request not available in getInitialProps when server is on a different port

I have my express server on a different port than my client-side nextjs project.
I know when you have a server on the same port you can use getRequestHandler with next that passes the user object to be accessible with getInitialProps in the client-side.
const express = require("express");
const next = require("next");
const app = next({ dev: true });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
// adds passport session
require("./middlewares").init(server);
const apolloServer = require("./graphql").createApolloServer();
apolloServer.applyMiddleware({ app: server });
server.all("*", (req, res) => {
return handle(req, res);
});
server.listen(port, (err) => {
if (err) throw err;
});
});
My passport implementation is as follows
const config = require("../config");
const session = require("express-session");
const passport = require("passport");
exports.init = (server, db) => {
require("./passport").init(passport);
const sess = {
name: "pid",
secret: config.SESSION_SECRET,
cookie: { maxAge: 2 * 60 * 60 * 1000 },
resave: false,
saveUninitialized: false,
store: db.initSessionStore(),
};
if (process.env.NODE_ENV === "production") {
server.set("trust proxy", 1);
sess.cookie.secure = true;
sess.cookie.httpOnly = true;
sess.cookie.sameSite = 'none';
sess.cookie.domain = process.env.DOMAIN;
}
server.use(session(sess));
server.use(passport.initialize());
server.use(passport.session());
};
And running the following on the express server, I can see req.user returning the user object.
app.use((req, res, next) => {
console.log(req.user);
next();
});
In a page in my nextjs app, in getInitialProps req.user is undefined
Home.getInitialProps = async (ctx) => {
const { req } = ctx;
const { user } = req;
console.log(user);
..........
};
Is there a way to either access the passport user object via SSR in nextjs or a different method to authorize and user on a page?
I do have a Github Repo with instructions on how to run the app in the README.md
Passport auth doesn't seems work across port. The solution is put a ngnix in front.
Local passport authorization on different ports

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

Google OAuth Broken After Heroku Deployment

My Google OAuth Strategy works perfectly on the client side. However after deploying the project to heroku the OAuth breaks once google attempts to redirect the user back to the specified redirect route. I am getting a Request Timed out error on the '/auth/google/callback/ route which is the route google is sending the user back to after authentication. It is defined in my authRoutes (Screenshot below). I am using PassportJS and Express for my backend. My development keys have been setup correctly as have my Google OAuth routes, the error only occurs upon the redirect from Google. Any help would be much appreciated
Here is my Passport Strategy:
const GoogleStrategy = require('passport-google-oauth20').Strategy
const keys = require('../config/keys')
const passport = require('passport')
const mongoose = require('mongoose')
const User = mongoose.model('users')
// This will store a cookie containing the user ID
// in our session after login is complete
passport.serializeUser((user, done) => {
done(null, user.id);
});
// This is called during every request
// It obtains a user object from the user id we serialized earlier
// the user object is stored in req.user
passport.deserializeUser((id, done) => {
User.findById(id).then(user => {
done(null, user);
});
});
passport.use (
new GoogleStrategy({
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: '/auth/google/callback',
proxy: true
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({googleID: profile.id});
if(!existingUser) {
const user = await User.create({googleID: profile.id}).save()
return done(null, user)
}
done(null, existingUser)
})
)
Here are my Routes:
const passport = require('passport')
module.exports = app => {
// This handles getting authentication details
// (gmail profile) from google
app.get("/auth/google", passport.authenticate('google', {
scope: ['profile', 'email']
}))
app.get("/auth/google/callback", passport.authenticate('google'), (req, res) => {
res.redirect('/surveys')
})
app.get("/api/logout", (req, res) => {
// Provided by passport
req.logout()
res.redirect('/')
})
app.get("/api/current_user", (req, res) => {
res.send(req.user)
})
}
And my Index.js:
const express = require('express');
const mongoose = require('mongoose');
const cookieSession = require('cookie-session');
const passport = require('passport');
const keys = require('./config/keys');
const bodyParser = require('body-parser')
require('./models/User');
require('./models/Survey');
require('./services/passport');
mongoose.connect(keys.mongoURI);
const app = express();
app.use(
cookieSession({
maxAge: 30 * 24 * 60 * 60 * 1000,
keys: [keys.cookieKey]
})
);
app.use(bodyParser.json())
app.use(passport.initialize());
// This middleware injects a cookie in every request
// to allow us to identify the user
app.use(passport.session());
require('./routes/authRoutes')(app);
require('./routes/billingRoutes')(app);
if(process.env.NODE_ENV == "production"){
// Express will serve up production assets
// like our main.js file or main.css file!
app.use(express.static('client/build'))
// Express will serve up the index.html file
// if it doesn't recognize the route
const path = require('path')
app.get('*', (req, res ) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'))
})
}
const PORT = process.env.PORT || 5000;
app.listen(PORT);
Heroku Error:
at=error code=H12 desc="Request timeout" method=GET path="/auth/google/callback?code=4/tgDTZZu8osZZvvMGjX4qaazb46SqNukZU6kNARY7R2enmH21cX6IfkVYSVnVHIoQ_qHaUbLttS_VGiS81KYE3D0&scope=email+profile+https://www.googleapis.com/auth/userinfo.email+https://www.googleapis.com/auth/userinfo.profile" host=cryptic-citadel-17268.herokuapp.com request_id=e8eb956d-64e4-4217-b846-91c53953e0a7 fwd="193.203.134.47" dyno=web.1 connect=0ms service=30001ms status=503 bytes=0 protocol=https
It's how you connect to your DB. You've mentioned that everything is working on your localhost correctly, but your production is giving your errors, please check this bit on prod - your connection error is likely due to production DB key configuration.
passport.use (
new GoogleStrategy({
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: '/auth/google/callback',
proxy: true
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({googleID: profile.id});
if(!existingUser) {
const user = await User.create({googleID: profile.id}).save()
return done(null, user)
}
done(null, existingUser)
})

Can't set a cookie w/ Nuxt.js, Express-Session

I'm new to NUXT and SSR and I've been researching this for a few hours now and I can't seem to figure it out. I'm using JWT to authenticate users in my Nuxt app with a Bearer Token, which is working great until I hit refresh and lose my session.
Now I'm looking to persist sessions using express-session and connect-mongo. I can't get the cookie to set on the client to be included on future requests.
When a user is authenticated:
router.post('/login', function(req, res) {
User.findOne({
username: req.body.username
}, function(err, user) {
if (err) throw err;
if (!user) {
res.status(401).send({success: false, msg: 'Authentication failed. User not found.'});
} else {
// check if password matches
user.comparePassword(req.body.password, function (err, isMatch) {
if (isMatch && !err) {
// if user is found and password is right create a token
var token = jwt.sign(user.toJSON(), config.secret, { expiresIn: 604800 });
req.session.authUser = { 'user': 'Test User' }
return res.json({success: true, token: token, user: user});
} else {
res.status(401).send({success: false, msg: 'Authentication failed. Wrong password.'});
}
});
}
The console.log above shows the authUser in the session.
Session {
cookie:
{ path: '/',
_expires: 2018-04-03T18:13:53.209Z,
originalMaxAge: 60000,
httpOnly: true },
authUser: { user: 'Test User' } }
When I look at my chrome devtools application cookies a connect.ssid hasn't been set and when I console.log(req.session) on future requests the authUser is missing.
My server code is:
// Passport
var passport = require('passport');
var passportJWT = require("passport-jwt");
var ExtractJwt = passportJWT.ExtractJwt;
var JwtStrategy = passportJWT.Strategy;
// Config File
let config = require('./config/settings.js')
// Initialize Express
var app = express();
// CORS-ENABLE
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "http://127.0.0.1:1337");
res.header("Access-Control-Allow-Credentials", 'true');
next();
});
app.use(cors())
const dbPath = 'mongodb://blogUser:blogUserPassword#localhost:27017/blog'
// Express Session
app.use(session({
secret: 'super-secret-key',
resave: false,
saveUninitialized: false,
store: new MongoStore({ url: dbPath }),
cookie: { maxAge: 60000 }
}))
// File Upload
app.use(fileUpload());
// view engine setup
// app.set('views', path.join(__dirname, 'views'));
// app.set('view engine', 'jade');
// uncomment after placing your favicon in /public
//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')));
// Routes
var index = require('./routes/index');
var users = require('./routes/users');
app.use('/api', index);
app.use('/users', users);
// Passport Config
app.use(passport.initialize());
app.use(passport.session())
// mongoose
const options = {
autoIndex: true, // Don't build indexes
reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
reconnectInterval: 500, // Reconnect every 500ms
poolSize: 10, // Maintain up to 10 socket connections
// If not connected, return errors immediately rather than waiting for reconnect
bufferMaxEntries: 0
};
console.log(options);
// Localhost Connect
mongoose.connect(dbPath, options).then(
() => { console.log("connected !!!"); },
err => { console.log(err); }
);
Any and all help is appreciated.
If you want to use the server you create the problem with the session is the express router, because change res and req vars so like recommend in nuxt use this.
const express = require('express');
// Create express router
const router = express.Router()
// Transform req & res to have the same API as express
const app = express()
router.use((req, res, next) => {
Object.setPrototypeOf(req, app.request)
Object.setPrototypeOf(res, app.response)
req.res = res
res.req = req
next()
})
You are missing this step
// Create express router
const router = express.Router()
// Transform req & res to have the same API as express
// So we can use res.status() & res.json()
router.use((req, res, next) => {
Object.setPrototypeOf(req, app.request)
Object.setPrototypeOf(res, app.response)
req.res = res
res.req = req
next()
})
The req, res parameters need to be interchanged on the client side
Once you do router.post('/login') and logout
app.use('/api', router)
And that will work perfectly