Problems
I'm using Passport.js for authentication on a Node.js server (localhost:8000) and a Next.js client (localhost:3000). Despite trying various solutions, I can't get cross-domain authentication to work correctly. Here’s my setup:
Code
// server.ts
import express, { Application } from 'express';
import passport from 'passport';
import session from 'express-session';
import { authRoutes, configurePassport, sessionConfig } from '@/auth/index';
import { productsRoutes } from '@/products/index';
import { usersRoutes } from '@/users/index';
const app: Application = express();
const port: number = 8000;
app.set('trust proxy', 1);
// Allow client access to the server
const allowedOrigins = ['http://localhost:3000', 'http://localhost:8000'];
app.use((req, res, next) => {
const origin = req.headers.origin;
console.log('origin: ', origin);
if (!origin || allowedOrigins.includes(origin)) {
console.log('Setting headers');
// res.setHeader('Access-Control-Allow-Origin', origin || '*');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Set-Cookie, Cookie',
);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
} else {
res.status(403).send('Origin not allowed');
}
});
// Middlewares
app.use(session(sessionConfig)); // Create a session for the API
app.use(passport.initialize()); // Passport initialize
app.use(passport.session()); // Passport session
configurePassport(); // Configure Passport
// sessionConfig.ts
import connectPgSimple from 'connect-pg-simple';
import session, { SessionOptions } from 'express-session';
import { pool } from '@/db/index';
import dotenv from 'dotenv';
dotenv.config();
// Create a session store
const PgSession = connectPgSimple(session);
// Session configuration for the API
export const sessionConfig: SessionOptions = {
store: new PgSession({
pool: pool, // Use the existing pool
tableName: 'sessions',
errorLog: (err) => console.error(err),
}),
// domain: 'localhost',
secret: process.env.SESSION_SECRET as string,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: false,
maxAge: 365 * 24 * 60 * 60 * 1000, // One year
secure: false, // Change to true in production
// sameSite: 'none',
// path: '/',
// domain: 'localhost',
},
};
// configurePassport.ts
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import dotenv from 'dotenv';
import { pool } from '@/db/index';
import { createUser, getUserById } from '@/users/index';
dotenv.config();
// Configure Passport
export const configurePassport = () => {
// Use the user id to serialize the user
passport.serializeUser((user: any, done) => {
try {
console.log('Serizalize user is executed: ', user?.id);
done(null, user.id);
} catch (error) {
done(error);
}
});
// Deserialize the user with the id
passport.deserializeUser(async (id, cb) => {
console.log('Deserializing user', id);
// cb(null, user);
try {
const user = await getUserById({ id: Number(id) });
if (user) {
const minimalUserInfo = { id: user.id };
cb(null, minimalUserInfo);
// cb(null, user);
} else {
cb(null, false);
}
} catch (err) {
cb(err);
}
});
// Use Google strategy
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
callbackURL: `${process.env.SERVER_URL}/auth/google/callback`,
passReqToCallback: true,
},
async (_req, _accessToken, _refreshToken, profile, done) => {
try {
const { rows } = await pool.query('SELECT * FROM users WHERE google_id = $1', [profile.id]);
let user = rows[0];
if (user) {
console.log('User logged in: ' + user.id);
return done(null, user);
} else {
// Create a new user if possible
if (
profile?.emails?.[0]?.value &&
profile?.name?.givenName &&
profile?.name?.familyName &&
profile?.photos?.[0]?.value &&
profile.id
) {
user = await createUser({
email: profile.emails[0].value,
username: undefined,
first_name: profile.name.givenName,
last_name: profile.name.familyName,
profile_photo: profile.photos[0].value,
account_type: 'Google',
password: undefined,
google_id: profile.id,
});
}
if (user) {
console.log('User registered: ' + user.id);
return done(null, user);
} else {
return done(new Error('Failed to create user with Google'));
}
}
} catch (err) {
console.error(err);
return done(err);
}
},
),
);
};
// getCurrentUser.tsx from the client
export async function getCurrentUser() {
// 'use server'
try {
// FIXME: Change no-store when the function is ready
const res = await fetch('http://localhost:8000/users/current', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Origin': 'http://localhost:3000',
},
});
const data = await res.json();
return data;
} catch (error) {
console.error('An error occurred while fetching the current user:', error);
throw error;
}
}
Issue
I can create a user account with Google and save it to my PostgreSQL database (along with sessions). I can directly access http://localhost:8000/users/current from the browser and see the user data. However, when I try to fetch the same path from the client, my req.session only has the cookie but not the passport session.
Results from localhost:3000 fetch request: json Copy code cookie: { path: '/', _expires: 2025-06-05T15:27:10.903Z, originalMaxAge: 31536000000, httpOnly: false, secure: false }
req.headers: { host: 'localhost:8000', connection: 'keep-alive', 'content-type': 'application/json', origin: 'http://localhost:3000', accept: '/', 'accept-language': '*', 'sec-fetch-mode': 'cors', 'user-agent': 'node', 'accept-encoding': 'gzip, deflate' }
Question
Why is req.session only containing the cookie and not the passport session when fetching from the client? How can I correctly maintain the session between my Next.js client and Node.js server?
Additional context
I've read numerous blogs, posts, and documentation about cross-domain authentication using Passport.js and Next.js, but I haven't found a solution that works.
Goal: I want to stay authenticated when making fetch requests from the client (localhost:3000). Specifically, I need req.user and req.isAuthenticated() to function correctly.
Expected result
When fetching from the client, I expect req.session to include both the cookie and the passport user session:
cookie: {
path: '/',
_expires: 2025-06-05T15:26:23.501Z,
originalMaxAge: 31536000000,
httpOnly: false,
secure: false
},
passport: { user: 7 }
}