1

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 }
}

0