`Cannot set headers after they are sent to the client ` when I try to login with `passport-google-oauth20`

349 Views Asked by At

When I tried to implement Google OAuth into my node app using passport-google-oauth20, I got a problem.

Whenever I attempt the first login to the secrets page with the following code, I fail to authenticate and got redirected to the /login page, also got the error saying Cannot set headers after they are sent to the client at the line serializing the user, even though newUser has been saved in the mongoDB.

However, I can successfully authenticate and login to the secrets page the second login attempt.

What's happening behind the scenes where the error occurs? How can I successfully authenticate the user when the first login attempt?

I referred to this Q&A as well.

passport.use(new GoogleStrategy({
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: "http://localhost:3000/auth/google/secrets"
    },
    (accessToken, refreshToken, profile, done) => {
      User.findOne({ googleId: profile.id }, (err, foundUser) => {
        if (err) return done(err);
        if (!foundUser) {
          const newUser = new User({
            googleId: profile.id
          });

          newUser.save((err, savedUser) => {
            if (err) throw err;
            return done(null, savedUser);
          });
        }
        return done(null, foundUser);
      });
    }
));
passport.serializeUser((user, done) => {
  done(null, user.id);     ///// The error occurs at this line /////
});

passport.deserializeUser((id, done) => {
  User.findById(id, (err, user) => {
    done(err, user);
  });
});
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile'] }));

app.get(
  "/auth/google/secrets",
  passport.authenticate("google", {
    successRedirect: "/secrets",
    failureRedirect: "/login"
  })
);

app.get("/secrets", (req, res) => {
  if (req.isAuthenticated()) return res.render("secrets");
  res.redirect("/login");
});
1

There are 1 best solutions below

0
On

The issue I see is within the verify callback. Calling return done(null, savedUser) will occur asynchronously. This means that the program will first call return done(null, foundUser) then after the saving call return done(null, savedUser).

To resolve the issue I would recommend refactoring the verify callback to use async/await. This makes it easier to reason about and reduces the chances of race conditions from conflicting callbacks.

Example Refactor:

async (accessToken, refreshToken, profile, done) => {
  try {
    let foundUser = await User.findOne({ googleId: profile.id });
    
    if (!foundUser) {
      const newUser = new User({
        googleId: profile.id
      });

      await newUser.save();
      return done(null, newUser);
    }

    return done(null, foundUser);
  } catch (err) {
    return done(err);
  }
}));