I am building a full-stack app with a PERN stack. I can POST
a username and password from the front-end to /login
and log in successfully - isAuthenticated()
returns true. However, if I then send a GET
request to /current-session
isAuthenticated()
returns false.
I am wondering where things might be going wrong. I have looked at every related issue I can find on Stack Overflow, and haven't found an answer yet.
The server is running. The database connection is established, and queries to the database work. The database table "session" is being populated. For example:
sid | sess | expire |
---|---|---|
T6akfD8h0TWysQD2jZ5LqIBlxCBIcLSp | [object Object] | 2024-03-06 00:54:22 +0000 |
In the browswer, I can see the sid as a HTTP only cookie from the response headers when I POST
to /login
. For example:
connect.sid=s%3AT6akfD8h0TWysQD2jZ5LqIBlxCBIcLSp.H01oGHOfFy9m6Tt451i%2F0vpoKuYEDPN8insL7Wx8GaU;
Path=/;
Expires=Wed, 06 Mar 2024 00:54:21 GMT;
HttpOnly
However, when I GET
to /current-session
, I don't see the cookie in the browser response.
It seems as if app.use(passport.session());
is for some reason telling app.get("/current-session"...
that there is no current user session/current authenticated user. Wondering whether the session is not being maintained for some reason...?
From my logs, I can see that passport.deserializeUser()
is never called.
Here is the current code:
const express = require("express");
const session = require("express-session");
const passport = require("passport");
const LocalStrategy = require("passport-local");
const bcrypt = require("bcrypt");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const { Pool } = require("pg");
const pgSession = require("connect-pg-simple")(session);
require("dotenv").config();
// ------------- GENERAL SETUP --------------- //
const app = express();
const PORT = process.env.PORT || 3001;
app.use(
cors({
origin: "https://nightlifeapp.onrender.com", // "http://localhost:5173",
credentials: true,
"Access-Control-Allow-Credentials": true,
})
);
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
// ----------- DATABASE CONNECTION ----------- //
// Create a PostgreSQL connection pool
const pool = new Pool({
connectionString: process.env.ELEPHANTSQL_CONNECTION_URL,
max: 5,
});
// Test the database connection
pool.connect((err, client, done) => {
if (err) {
console.error("Error connecting to the database", err);
} else {
console.log("Connected to the database");
}
});
// ----------- PASSPORT STRATEGIES ----------- //
passport.use(
"local",
new LocalStrategy((username, password, done) => {
// Query the PostgreSQL database to find a user by username
pool.query(
"SELECT * FROM users WHERE username = $1",
[username],
(err, result) => {
if (err) {
return done(err);
}
// Check if the user exists
const user = result.rows[0];
if (!user) {
return done(null, false);
}
// Check if the password is correct
if (!bcrypt.compareSync(password, user.password_hash)) {
return done(null, false);
}
// If the username and password are correct, return the user
return done(null, user);
}
);
})
);
passport.serializeUser((user, done) => {
console.log("serializeUser invoked");
done(null, user);
});
passport.deserializeUser((user, done) => {
console.log("serializeUser invoked");
done(null, user);
});
// ------------- EXPRESS SESSION ------------- //
app.use(
session({
store: new pgSession({
pool,
tableName: "session",
}),
secret: process.env.EXPRESS_SESSION_SECRET_KEY,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 14 * 24 * 60 * 60 * 1000, // 14 days session timeout
},
})
);
app.use(passport.initialize());
app.use(passport.session());
// ----------------- ROUTING ----------------- //
app.get("/current-session", passport.authenticate("session"), (req, res) => {
if (req.isAuthenticated()) {
console.log("At GET /current-session... isAuthenticated() returned true");
} else {
console.log("At GET /current-session... isAuthenticated() returned false");
}
if (!req.user) {
res.json({ currentlyLoggedIn: false });
} else {
res.json({
currentlyLoggedIn: true,
userId: req.user.user_id,
username: req.user.username,
});
}
});
app.post(
"/login",
passport.authenticate("local"), (req, res) => {
if (req.isAuthenticated()) {
console.log("At POST /login... isAuthenticated() returned true");
} else {
console.log("At POST /login... isAuthenticated() returned false");
}
return res.json({
loginSuccessful: true,
userId: req.user.user_id,
username: req.user.username,
});
}
);
// ------------------ SERVER ----------------- //
const server = app.listen(PORT, () =>
console.log(`Server is listening on port ${PORT}`)
);
server.keepAliveTimeout = 120 * 1000;
server.headersTimeout = 120 * 1000;
On the front-end, the fetch requests I'm making to /login
and /current-session
are as follows:
fetch(`${URL}/login`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"Access-Control-Allow-Origin": "https://nightlifeapp.onrender.com",
},
body: jsonData,
})
And
fetch(`${URL}/current-session`, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"Access-Control-Allow-Origin": "https://nightlifeapp.onrender.com",
},
})```
Any suggestions would be greatly appreciated!
So, it turns out that the problem here was that cookies were being blocked because the front-end and the back-end were hosted on different Render.com sub-domains, and so the cookies were not being used to keep the session going.
I could have used different/custom domain names to avoid the cookies being blocked.
Instead, the solution I came up with was hosting the front end static files on the same server as the Express API end-points. To do that, I placed the build/dist files for the React front-end in a folder on the Express server, and then used app.use(express.static("dist"));
to serve the front-end index.html
file - by default on calls to the '/' end-point.
I used this explanation as a starting point: https://youtu.be/4pUBO31nkpk?si=M5GXQGCa__W0_yH9