I am building a MERN stack app, and there is a problem with authentication and passport.js during production when using render.com to deploy my site.
Now, the authentication, passport.js, and everything was working with localhost, but it doesn't work when deployed. This likely means that there is a session/cookie issue.
My app has two methods: localhost and google authentication. Both suffer the same issue: serializeUser is not putting user data into session. I know this because of the console.log statements that I put in my code. This is an example of what happened when I did google authentication:
Apr 5 10:44:08 PM [0] Session Details: Session {
Apr 5 10:44:08 PM [0] cookie: {
Apr 5 10:44:08 PM [0] path: '/',
Apr 5 10:44:08 PM [0] _expires: 2023-04-09T02:44:08.190Z,
Apr 5 10:44:08 PM [0] originalMaxAge: 259200000,
Apr 5 10:44:08 PM [0] httpOnly: true,
Apr 5 10:44:08 PM [0] secure: true,
Apr 5 10:44:08 PM [0] sameSite: 'strict'
Apr 5 10:44:08 PM [0] }
Apr 5 10:44:08 PM [0] }
Apr 5 10:44:08 PM [0] <MESSAGE THAT CONTAINS DEVICE/BROWSER INFO THAT SENT REQUEST, I REDACTED THIS>
Apr 5 10:44:11 PM [0] Session Details: Session {
Apr 5 10:44:11 PM [0] cookie: {
Apr 5 10:44:11 PM [0] path: '/',
Apr 5 10:44:11 PM [0] _expires: 2023-04-09T02:44:11.169Z,
Apr 5 10:44:11 PM [0] originalMaxAge: 259200000,
Apr 5 10:44:11 PM [0] httpOnly: true,
Apr 5 10:44:11 PM [0] secure: true,
Apr 5 10:44:11 PM [0] sameSite: 'strict'
Apr 5 10:44:11 PM [0] }
Apr 5 10:44:11 PM [0] }
Apr 5 10:44:11 PM [0] Attempting google callback...
Apr 5 10:44:11 PM [0] Google callback succesful.
Apr 5 10:44:11 PM [0] serialize user is working
Apr 5 10:44:11 PM [0] user object is: {
Apr 5 10:44:11 PM [0] _id: new ObjectId("<long string of id, redacted>"),
Apr 5 10:44:11 PM [0] googleId: '<long string of id, redacted>',
Apr 5 10:44:11 PM [0] email: 'pleasehelpmestackoverflow@gmail.com',
Apr 5 10:44:11 PM [0] __v: 0
Apr 5 10:44:11 PM [0] }
Apr 5 10:44:11 PM [0] user_id object is: <long string of id, redacted>
serializeUser works, and it does get the right information from google authentication (as shown from the "user object is" console.log above Below is my code for passport.js:
const passport = require("passport");
const bcrypt = require("bcryptjs");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const LocalStrategy = require("passport-local").Strategy;
const User = require('./models/User');
// google authentication passport
passport.use(
"google",
new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `${process.env.SERVER_URL}/auth/google/callback`,
passReqToCallback: true
},
async function(req, accessToken, refreshToken, profile, done) {
console.log("Attempting google callback...");
try {
let user = await User.findOne({ googleId: profile.id });
if (!user) {
// Create a new user if not found
user = new User({
googleId: profile.id,
email: profile.emails[0].value,
refreshToken: refreshToken // Store refresh token in the user model
});
} else {
user.refreshToken = refreshToken; // Update refresh token
}
await user.save();
console.log("Google callback succesful.");
return done(null, user);
} catch (err) {
console.log("Google callback unsuccesful.");
console.error('Error in GoogleStrategy callback:', err); // Log the error for debugging
return done(err);
}
}
));
// local signup passport
passport.use (
"local_signup",
new LocalStrategy ({
usernameField: "username",
passwordField: "password",
passReqToCallback: true,
},
async (req, username, password, done) => {
try {
console.log("local signup function is being called...");
// Check if email is in a valid format
const emailRegex = /^[\w-]+(.[\w-]+)*@([\w-]+.)+[a-zA-Z]{2,7}$/;
if (!emailRegex.test(req.body.email)) {
return done(null, false, {
message: "Please provide an email with valid format.",
});
}
// Check if password meets the requirements
if (password.length < 6) {
return done(null, false, {
message: "Password must be at least 6 characters long.",
});
}
const existingUser = await User.findOne({
username: username
});
const existingEmail = await User.findOne({
email: req.body.email
});
if (existingUser) {
return done(null, false, {
message: "Username already exists.",
});
}
if (existingEmail) {
return done(null, false, {
message: "Email already exists.",
});
}
const hashedPassword = await bcrypt.hash(password, 10);
console.log("Got past the checkers");
const newUser = new User({
email: req.body.email,
username: username,
password: hashedPassword,
});
console.log("Made new schema for user");
await newUser.save();
console.log("Saved it!!!");
return done(null, newUser);
} catch (err) {
console.log("Apparently something went wrong somewhere...");
console.error("Something went wrong, and we don't even know why.", err);
return done(err);
}
})
);
// local login passport
passport.use(
"local_login",
new LocalStrategy({ usernameField: "identifier" }, async (identifier, password, done) => {
try {
console.log("Local LOGIN WORKING");
const user = await User.findOne(
{ username: identifier }
);
const email = await User.findOne(
{ email: identifier }
);
if (!(user || email)) {
if(!user && email == null) {
return done(null, false, { message: "There are no accounts with the given username." });
}
if(!email && user == null) {
return done(null, false, { message: "There are no accounts with the given email." });
}
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return done(null, false, { message: "Incorrect password." });
}
// succesful
return done(null, user);
} catch (err) {
return done(err);
}
})
);
passport.serializeUser((user, done) => {
console.log("serialize user is working");
console.log("user object is: " + user);
console.log("user_id object is: " + user._id);
done(null, user.id);
});
passport.deserializeUser(async (userId, done) => {
console.log("attempting deserialize");
try {
const user = await User.findById(userId);
if (user) {
console.log("deserialize working.. I think");
done(null, user);
} else {
console.log("deserialize not working");
done(new Error('User not found'));
}
} catch (err) {
done(err);
}
});
Below is my code for server.js:
const express = require("express");
// require dotenv
require('dotenv').config({ path: './.env' });
// require middlewares
const compression = require("compression");
const helmet = require("helmet");
const RateLimit = require("express-rate-limit");
const logger = require("morgan");
const cors = require("cors");
const session = require("express-session");
const MongoStore = require("connect-mongo");
var cookieParser = require('cookie-parser')
const passport = require("passport");
// require configuration stuff
const InitiateMongoServer = require("./db");
const passportStrategy = require("./passport");
// require routes
const auth = require("./routes/auth");
const user = require("./routes/user");
/* setup stuff */
InitiateMongoServer();
const app = express();
const PORT = process.env.PORT || 4000;
// the server will handle at maximum 60 requests per minut
const limiter = RateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 60,
});
app.use(cookieParser());
app.use(session({
secret: 'imnotgivingawayprivateinformationinstackoverflow',
resave: false,
saveUninitialized: true,
store: MongoStore.create({ mongoUrl: process.env.MONGODB_CONNECT_URL }),
cookie: {
secure: true, // set to true for production https
httpOnly: true,
sameSite: 'strict',
maxAge: 72 * 60 * 60 * 1000 // 3 days
}
}));
app.use(passport.initialize());
app.use(passport.session());
const printSessionDetails = (req, res, next) => {
console.log('Session Details:', req.session);
next();
};
// app.use(refreshSession);
app.use(printSessionDetails);
// middleware stuff
app.use(logger('combined'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(helmet());
app.use(compression());
// updated cors to allow cookies to be set in cross domain
app.use(cors(function (req, cb) {
let corsOptions;
corsOptions = {
origin: process.env.CLIENT_URL,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Cookie'],
credentials: true
};
cb(null, corsOptions);
}), function (req, res, next) {
res.header('Access-Control-Allow-Credentials', true);
next();
});
// Middleware for login
app.use("/auth", auth);
app.use("/user", user);
app.listen(PORT, (req, res) => {
console.log(`Server starting at PORT ${PORT}`);
});
module.exports = app;
Below is my code for controller for handling auth stuff (routing is in another file)
const passport = require("passport");
exports.success = (req, res) => {
if (req.user) {
res.redirect(process.env.CLIENT_URL + "/checklist");
} else {
res.redirect(process.env.CLIENT_URL + "/login");
}
};
exports.fail = (req, res) => {
res.redirect(process.env.CLIENT_URL + "/login");
};
exports.success2 = (req, res) => {
if (req.user) {
res.json({ success: true });
} else {
res.status(401).json({ success: false, message: "Something went wrong. Please try again."});
}
};
exports.fail2 = (req, res, errorMessage) => {
res.status(401).json({ success: false, message: errorMessage});
};
// (step 1) authenticates with google page
exports.google = passport.authenticate(
"google",
{ scope: [ 'email', 'profile' ]}
);
// (step 2) log in or create account
exports.google_callback = (req, res, next) => {
passport.authenticate("google", (err, user, info) => {
if (err) {
return exports.fail(req, res, next);
}
if (!user) {
return exports.fail(req, res, next);
}
req.logIn(user, (err) => {
if (err) {
return exports.fail(req, res, next);
}
return exports.success(req, res, next);
});
})(req, res, next);
};
exports.signup = (req, res, next) => {
console.log("controller for signup calling");
passport.authenticate("local_signup", (err, user, info) => {
console.log('Passport authenticate callback');
if (err) {
console.error('Error:', err);
return res.status(500).json({ success: false, message: "Something went wrong. Please try again." });
}
if (!user) {
console.log('No user:', info.message);
return res.status(401).json({ success: false, message: info.message });
}
req.logIn(user, (err) => {
if (err) {
console.error('Error logging in:', err);
return res.status(500).json({ success: false, message: "Something went wrong. Please try again." });
}
console.log('User logged in');
return res.json({ success: true });
});
})(req, res, next);
};
exports.login = (req, res, next) => {
passport.authenticate("local_login", (err, user, info) => {
if (err) {
return exports.fail2(req, res, info.message, next);
}
if (!user) {
return exports.fail2(req, res, info.message, next);
}
req.logIn(user, (err) => {
if (err) {
return exports.fail2(req, res, info.message, next);
}
return exports.success2(req, res, next);
});
})(req, res, next);
};
exports.logout = (req, res) => {
console.log('attempting to log out of session');
try {
req.session = null;
res.json({ loggedOut: true });
} catch (err) {
res.json({ loggedOut: false });
}
};
exports.authenticationCheck = (req, res, next) => {
console.log("req.user is: " + req.user);
console.log("req.session is: " + req.session);
console.log("Checking authentication status: ");
if (req.isAuthenticated()) {
console.log("Authentication Check is working");
res.json({ isAuthenticated: true });
} else {
console.log("Authentication Check is not working");
res.json({ isAuthenticated: false });
}
}
I have tried experimenting between cookie-session and express-session. I believe earlier in the day, using cookie-session somehow made it kind of work (passport info was being stored in session), but later I believe the same code stopped working. I honestly don't know which one to choose, but cookie-session was giving me an empty session later on, so I decided to use express-session.
I tried to check if my environmental variables, URL connections, or google console settings had an issue and had the right domain/URL. All of them were.
I changed serializeuser and deserializeUser many times using advice from chatGPT. I don't even know if I can list all of it, but my passport.js and server.js code is a mess.
The code I have works on localhost on my device. When I deploy to render (a static service for frontend and a web service for backend), serializeUser fails to store into session.
I also am not entirely sure how session or cookies differ here.