I am using Passport JS for authentication for my MERN app. I am trying to have routes that are protected so you must be authenticated. When I test the routes and /login
in Thunder Client and then try to access a protected route it works fine. However on my React frontend when logged in successfully it doesn't set the session cookie for Passport (even thought it does show up in the response). Meaning I can't authenticate.
Here is the Auth.mjs
file.
import UsersModel from "../Models/User.mjs";
import bcrypt from "bcrypt";
import { omit, pick } from "../helpers.mjs";
// ---------------------------- //
// ----- AUTH ROUTES ----- //
// ---------------------------- //
const auth = (app, checkAuthenticated, checkNotAuthenticated, passport) => {
app.delete("/logout", checkAuthenticated, (req, res) => {
req.logout((err) => {
if (err) { return next(err); }
res.json({ msg: "Logged out"});
});
});
// BAD ROUTE - cookie SHOULD be attached
app.post("/login", checkNotAuthenticated, passport.authenticate("local"), (req, res) => {
res.json({
msg: "Authenticated",
user: req.user
})
});
app.post("/register", checkNotAuthenticated, async (req, res) => {
try {
const hashedPassword = await bcrypt.hash(req.body.password, 15)
await UsersModel.create({
name: req.body.name,
password: hashedPassword,
email: req.body.email
})
res.json({ msg: "Registered new user"})
} catch {
res.json({ msg: "Error"})
}
})
}
export default auth;
Here is my Passport.mjs
file:
import passportLocal from "passport-local";
import bcrypt from "bcrypt";
const LocalStrategy = passportLocal.Strategy;
const initialize = async (passport, getUserByEmail, getUserById) => {
const authenticateUser = async (email, password, done) => {
const user = await getUserByEmail(email);
if (user == null) {
return done(null, false, {msg: "No user with that email"})
console.log("LOL")
}
try {
console.log(`pass: ${password}, hash: ${user.password}`)
if (await bcrypt.compare(password, user.password)) {
console.log(user)
return done(null, user);
} else {
return done(null, false, {msg: "Password incorrect"})
}
} catch (e) {
return done(e)
}
}
passport.use(new LocalStrategy({ usernameField: "email"}, authenticateUser))
passport.serializeUser((user, done) => done(null, user._id));
passport.deserializeUser(async (id, done) => done(null, await getUserById(id)));
}
export default initialize;
Here is my main server file:
import dotenv from "dotenv";
if (process.env.NODE_ENV !== "production") {
dotenv.config();
}
import express from "express";
import mongoose from 'mongoose';
import cors from 'cors';
import UsersModel from './Models/User.mjs';
import passport from "passport";
import flash from "express-flash";
import session from "express-session";
import initialize from "./Routes/Passport.mjs";
import mongoStore from 'express-session-mongo';
import cookieParser from "cookie-parser";
// route imports
import items from "./Routes/Items.mjs";
import departments from "./Routes/Departments.mjs";
import houses from "./Routes/Houses.mjs";
import lists from "./Routes/Lists.mjs";
import people from "./Routes/People.mjs";
import auth from "./Routes/Auth.mjs";
const getUserByEmail = async (email) => {
return await UsersModel.findOne({email: email});
}
const getUserById = async (id) => {
return await UsersModel.findById(id);
}
// new express app
const app = express()
// ---------------------------- //
// ----- MIDDLEWARE ----- //
// ---------------------------- //
app.use(cors({credentials: true}))
app.use(express.json())
app.use(express.urlencoded({ extended: true }));
app.use(flash())
app.use(cookieParser())
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
}))
app.use(passport.initialize())
app.use(passport.session())
// connect to mongoDB
mongoose.connect(process.env.DB)
// initialize passport
initialize(passport, getUserByEmail, getUserById)
// check auth/not auth
const checkAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return next();
}
// ALWAYS HITS BELOW \/
return res.status(401).json({msg: "Must be authenticated"});
}
const checkNotAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return res.json({msg: "Can't access, authenticated"});
}
return next();
}
// ---------------------------- //
// ----- ROUTES ----- //
// ---------------------------- //
items(app, checkAuthenticated, checkNotAuthenticated);
departments(app, checkAuthenticated, checkNotAuthenticated);
houses(app, checkAuthenticated, checkNotAuthenticated);
lists(app, checkAuthenticated, checkNotAuthenticated);
people(app, checkAuthenticated, checkNotAuthenticated);
auth(app, checkAuthenticated, checkNotAuthenticated, passport);
// ---------------------------- //
// ----- START SERVER ----- //
// ---------------------------- //
app.listen(process.env.PORT, () => {
console.log("--- Server is UP and running ---")
})
// ---------------------------- //
// ---------------------------- //
This is my React frontend code:
import React, { useContext, useState } from 'react';
import "../css/Auth.css";
import { useMutation } from '@tanstack/react-query';
import { Context } from '../App';
const Authenticate = () => {
const [isLogin, setIsLogin] = useState(false)
const [loginEmail, setLoginEmail] = useState("");
const [loginPwd, setLoginPwd] = useState("");
const [registerName, setRegisterName] = useState("");
const [registerEmail, setRegisterEmail] = useState("");
const [registerPwd, setRegisterPwd] = useState("");
const [registerPwdConfirm, setRegisterPwdConfirm] = useState("");
const {user, authenticated} = useContext(Context);
const [userVal, setUserVal] = user;
const [authenticatedVal, setAuthenticatedVal] = authenticated;
const fetchLoginQuery = async () => {
const req = await fetch(`${import.meta.env.VITE_REACT_APP_API}/login`, {
method: 'post',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: loginEmail,
password: loginPwd
})
});
return req.json();
}
const fetchRegisterQuery = async () => {
const req = await fetch(`${import.meta.env.VITE_REACT_APP_API}/register`, {
method: 'post',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: loginEmail,
password: loginPwd
})
});
return req.json();
}
const loginMutation = useMutation({
mutationFn: fetchLoginQuery,
onSuccess: (data) => {
if (data.msg == "Authenticated") {
setAuthenticatedVal(true);
setUserVal(data.user)
}
// queryClient.invalidateQueries({ queryKey: [""]})
}
})
const registerMutation = useMutation({
mutationFn: fetchRegisterQuery,
onSuccess: () => {
// queryClient.invalidateQueries({ queryKey: [""]})
}
})
const handleLogin = () => {
loginMutation.mutate();
}
const handleRegister = () => {
//TODO
}
return (
<div>
<h1>Welcome to the Grocery List App</h1>
<button onClick={() => setIsLogin(!isLogin)}>{isLogin ? "Create an account?" : "Log in to existing account"}</button>
{isLogin ?
<>
<input type="text" placeholder="email" value={loginEmail} onChange={(e) => {setLoginEmail(e.target.value)}}/>
<input type="text" placeholder="password" value={loginPwd} onChange={(e) => {setLoginPwd(e.target.value)}}/>
<button onClick={handleLogin}>Login</button>
</>
:
<>
<input type="text" placeholder="name" value={registerName} onChange={(e) => {setRegisterName(e.target.value)}}/>
<input type="email" placeholder="email" value={registerEmail} onChange={(e) => {setRegisterEmail(e.target.value)}}/>
<input type="text" placeholder="password" value={registerPwd} onChange={(e) => {setRegisterPwd(e.target.value)}}/>
<input type="text" placeholder="password again" value={registerPwdConfirm} onChange={(e) => {setRegisterPwdConfirm(e.target.value)}}/>
<button onClick={handleRegister}>Register</button>
</>
}
</div>
)
}
export default Authenticate;
My backend is localhost:3001
. The below screenshot shows that the cookie is sent but it is not being set. I've tried testing res.cookie("test", "tester")
and similar in my response and that sets the cookie fine, but with Passport it doesn't want to. I've tried different browsers as well.
As you can see no cookie is set.
Adding credentials: "include"
onto my fetch call (client side) and setting the origin in the cors
middleware like so on my Node/Express backend...
app.use(cors({credentials: true, origin: "http://localhost:5173"}));
...fixed the issue. Replace "http://localhost:5173"
with your frontend URL.
Updated fetch query:
const fetchLoginQuery = async () => {
const req = await fetch(`${import.meta.env.VITE_REACT_APP_API}/login`, {
method: 'post',
headers: {
"Content-Type": "application/json",
},
credentials: "include", // <---- important! include this
body: JSON.stringify({
email: loginEmail,
password: loginPwd
})
});
return req.json();
}