node.jsexpresscookiespassport.jssveltekit

passport.deserializeUser function never getting called


Passport's deserialization just does not work no matter what I do. This is my passport.js:

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const User = require('../models/User');

module.exports = function(passport) {
  passport.use(new GoogleStrategy({
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: 'https://messenger-tu85.onrender.com/auth/google/callback',
    },
    (accessToken, refreshToken, profile, done) => {
      User.findOne({ googleId: profile.id })
        .then(existingUser => {
          if (existingUser) {
            console.log('Existing user found:', existingUser);
            return done(null, existingUser);
          } else {
            const tempUser = {
              googleId: profile.id,
              email: profile.emails[0].value
            };
            console.log('Creating new temp user:', tempUser);
            return done(null, tempUser);
          }
        })
        .catch(err => {
          console.error('Error finding user:', err);
          return done(err);
        });
    }
  ));

  passport.serializeUser((user, done) => {
    console.log('Serialize user:', user);
    if (user._id) {
      done(null, user._id);
    } else {
      done(null, user.googleId);
    }
  });

  passport.deserializeUser(async (id, done) => {
    try {
      // If id is an object and contains googleId, it’s a temporary user
      if (typeof id === 'object' && id.googleId) {
        done(null, id);
      } else {
        // If id is an _id, fetch the user from the database
        const user = await User.findById(id);
        done(null, user);
      }
    } catch (err) {
      done(err);
    }
  });
  
};

As you can see, when a user signs in with their Google account, if a user matching their google id does not exist in the database, then a temporary session is created for the user. This part works smoothly.

The user is serialized successfully and the console statement reads:

Creating new temp user: { googleId: 'my google id', email: 'my gmail' }
Serialize user: { googleId: 'my google id', email: 'my gmail' }

the point of creating the temporary session is so that they can continue their registration in another page.

This is my authRoutes.js:

const express = require('express');
const passport = require('passport');
const router = express.Router();

router.get('/auth/google', passport.authenticate('google', { scope: ['profile', 'email'] }));

router.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: 'https://svelte-of1p.onrender.com/signin' }),
  (req, res) => {
    if (req.user) {
      // Manually set the cookie
      res.cookie('connect.sid', req.sessionID, {
        maxAge: 24 * 60 * 60 * 1000, // 1 day
        secure: process.env.NODE_ENV === 'production', // True in production
        httpOnly: false, // Allow client-side access
        sameSite: 'None' // Allow cross-site cookies
      });
      if (req.user.username) {
        res.redirect('https://svelte-of1p.onrender.com');
      } else {
        res.redirect('https://svelte-of1p.onrender.com/select');
      }
    } else {
      res.redirect('https://svelte-of1p.onrender.com/signin');
    }
  }
);

module.exports = router;

As can be seen, if the user does not have a username (they can't have a username yet since they have not finished their registration) they are redirected to the /select page where they are to choose a unique username.

Notice that I am sending a cookie to the client. I didn't do this initially but i had to do it because I realized that no cookie was ever sent to the client, so by manually setting the cookie myself, the problem was fixed and cookie is now getting sent to the client after the user signs in with their Google account. Everything is working fine so far.

Now, the user without a username is redirected to the select page which is handled by SvelteKit. On the select/+page.svelte I have this:

const checkUserStatus = async () => {
    try {
        console.log('checkUserStatus function called');
        
        const response = await fetch('https://messenger-tu85.onrender.com/api/check-new-user', {
            credentials: 'include'
        });

        console.log('Response status:', response.status);

        if (response.status === 401) {
            console.log('Unauthorized: Redirecting to sign-in page');
            window.location.href = '/signin';
            return;
        }

        const data = await response.json();
        console.log('Response data:', data);
        
        userExists = data.exists;

        if (userExists) {
            console.log('User exists: Redirecting to messages');
            window.location.href = '/messages';
        } else {
            console.log('User does not exist: Enabling form');
            errorMessage = '';
            successMessage = '';
            formDisabled = false;
        }
    } catch (error) {
        console.error('Error checking user status:', error);
    }
};

    onMount(checkUserStatus); 

So I am sending a fetch request to the server to confirm if the user making the request should be allowed to be on the page. The following is the endpoint on the server that the client calls:

const User = require('../models/User');

const checkNewUser = async (req, res, next) => {
  try {
    console.log('checkNewUser middleware called');
    console.log('Request received:', {
      headers: req.headers,
      body: req.body,
      user: req.user
    });

    // Correctly check for req.user.googleId directly
    if (!req.user || !req.user.googleId) {
      console.log('Unauthorized: No user or user ID found');
      return res.status(401).json({ error: 'Unauthorized' });
    }

    const googleId = req.user.googleId;
    console.log('Checking existing user with Google ID:', googleId);
    const existingUser = await User.findOne({ googleId });

    if (existingUser) {
      console.log('Existing user found:', existingUser);
      if (existingUser.username) {
        console.log('User has a username');
        return res.json({ exists: true });
      } else {
        console.log('User does not have a username');
        return res.json({ exists: false });
      }
    } else {
      console.log('No existing user found');
      return res.json({ exists: false });
    }
  } catch (error) {
    console.error('Error checking user:', error);
    res.status(500).json({ error: 'Server error' });
  }
};

module.exports = checkNewUser;

And this is where the problem is. The request headers being logged out show that:

user: undefined

It now occurs to me that after the user signed in with their Google account they were correctly serialized, but they were never deserialized. The function was never called and I am assuming that is why req.user is undefined.

Based on my understanding, deserializeUser is what would populate req.user but this does not happen for reasons I cannot figure out.

I have attempted to use cookie-session, cookie-parser, removing secure in my cookie setup, forcing req.login(), and a plethora of other "answers" I have found while browsing this forum, but they have not helped. I have even ensured that my middlewares are ordered correctly, but it does not help either.

I built a custom deserializer where I extracted the cookie from the request header (the cookie is being sent back to the server as i explained earlier) and then extracted the session id and then tried to know which user has the session id, it worked, but i ran into problems later trying to modify this my custom deserializer middleware to handle temporary users and fully registered users.

I would rather not use my custom deserializer middleware and would like to use the one that comes with passport, but why is it not getting called?

This is my server.js to show that my ordering of my middlewares are correct, I have removed codes not relevant to the problem:

const express = require('express');
const WebSocket = require('ws');
const app = express();
const port = 3000;
const { createServer } = require('http');
const dotenv = require('dotenv');
const passport = require('passport');
const selectUser = require('./controllers/selectUser');
const checkNewUser = require('./middleware/checkNewUser'); 
const cors = require('cors');
const session = require('express-session');
const MongoStore = require('connect-mongo');

dotenv.config();

const connectDB = require('./config/mongoose');
connectDB();

// Use CORS middleware before any other middleware
app.use(cors({
  origin: 'https://svelte-of1p.onrender.com',
  credentials: true
}));

// Use session middleware before Passport initialization
app.use(session({
  secret: process.env.SECRET,
  resave: false,
  saveUninitialized: false,
  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URI,
    collectionName: 'sessions'
  }),
  cookie: {
    maxAge: 24 * 60 * 60 * 1000, // 1 day
    httpOnly: false,
    sameSite: 'none'
  }
}));

// Initialize Passport after session middleware
const initializePassport = require('./config/passport');
initializePassport(passport);

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(passport.initialize());
app.use(passport.session());

app.use('/', require('./controllers/authControllers/authRoutes'));
app.use('/', require('./controllers/authControllers/usernameRoutes'));
app.use('/api/users', require('./controllers/users'));
app.use('/api/messages', require('./controllers/messageController'));
app.use('/api/select-user', selectUser.selectUser);
app.use('/api/selected-users', require('./controllers/selectedUsers'));
app.use('/api/check-new-user', checkNewUser);

Solution

  • I couldn't solve it so I just switched to jwt instead.