'Passport.js isAuthenticated() always returns false when on heroku, but works properly when server is run locally

I am using passport.js for authentication as well as express-sessions to store cookies on the client. Additionally, my client is a react app. I still am not sure if this is a server-side issue or client-side issue. Anyways, when I run my node server locally, then log in from my react app, the cookie is stored locally and I am authenticated, so if I access a route that requires authentication, I get access. However, when I run the same exact server code on Heroku, and the same react app, and I log in from the React app, passport. authenticate works and send a success message back to the client however for some reason now if I access a protected route my server cannot authenticate the client. I know that the cookie is stored because as soon as I switch to my local server, I become authenticated again, which means the cookie is with the client. I'm not sure what the issue is.

Node.js server code BACKEND


    const app = express();
    
    mongoose.connect("mongodb+srv://admin:" + process.env.DBPASS + "@cluster0.xpbd4.mongodb.net/" + process.env.DBNAME + "?retryWrites=true&w=majority", {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
    mongoose.set("useCreateIndex", true);
    
    app.use(cors({origin: "http://localhost:3000", credentials: true}));
    app.use(bodyParser.urlencoded({
      extended: true
    }));
    app.enable('trust proxy'); // add this line
    app.use(session({
      secret: process.env.SECRET,
      resave: false,
      saveUninitialized: false,
      store: new MongoStore({ mongooseConnection: mongoose.connection })
    }));
    app.use(passport.initialize());
    app.use(passport.session());
    
    passport.use(User.createStrategy());
    passport.serializeUser(function(user, done) {
      console.log("serializing user")
      done(null, user.id);
    });
    passport.deserializeUser(function(id, done) {
      console.log("deserializeUser called: " + id)
      User.findById(id, function(err, user) {
        done(err, user, function(){
          console.log("deserializeUserError: " + err)
        });
      });
    });

Login route


    app.post("/login", function(req, res) {
      const user = new User({
        email: req.body.email,
        password: req.body.password
      });
      req.login(user, function(err) {
        console.log("req.login called")
        if (err) {
          console.log(err)
          res.send("Nuts! An unknown error occured")
        } else {
          passport.authenticate("local")(req, res, function() {
            res.send("success")
          });
        }
      });
    });

the route that tells the user if they're authenticated or not. Returns false when the server is on Heroku, but true when run locally.


    app.get("/user/auth", function(req, res){
      console.log("auth called");
      if (req.isAuthenticated()) {
        res.send("true")
      } else {
        res.send("false")
      }
    })

FRONT END React App where I log in the user


    function submit(event){
        event.preventDefault();
        axios({
          method: 'post',
          data: Querystring.stringify(user),
          withCredentials: true,
          url: props.serverURL + '/login',
          headers: {
            'Content-type': 'application/x-www-form-urlencoded'
          }
        }).then(res => {
          console.log("resdata: " + res.data)
          if (res.data === "success"){
            props.reAuth();
            props.history.push('/');
          }else{
            setResult((prevValue) => {
              return (
                res.data.toString()
              )
            });
          }
        }).catch(function(err){
          setResult((prevValue) => {
            return (
              "Invalid email or password"
            )
          });
        })
      }

The part of the recent app that checks for authentication


    function reAuth(){
        axios.get(serverURL + "/user/auth", {withCredentials: true}).then(res => {
          console.log(res.data)
          setAuth(res.data)
        })
      }



Solution 1:[1]

I had a problem where my passport session did not survive res.redirect and I found out that I was missing a return statement in the serialize and deserialize functions even though I was running locally it might do the trick for you

passport.serializeUser(function(user, done) {
  console.log("serializing user")
  return done(null, user.id);
});
passport.deserializeUser(function(id, done) {
  console.log("deserializeUser called: " + id)
  User.findById(id, function(err, user) {
    return done(err, user, function(){
      console.log("deserializeUserError: " + err)
    });
  });
});

Solution 2:[2]

I encountered a similar issue when deploying a MERN app to Heroku and found this answer on SO helpful: https://stackoverflow.com/a/66395281/16921734.

In essence, you need to add the following to your session configuration:

  • proxy : true
  • sameSite : 'none'

Setting sameSite to none allows the browser to set cookies with cross domains, which is disabled by default.

With this, my session configuration looks like:

const sessionConfig={
  store:MongoDBStore.create({ mongoUrl:dbUrl}),
  secret:'thisisnotagoodsecret', 
  resave: false, 
  saveUninitialized: true,
  cookie: {
    httpOnly:true,
    secure:true, 
    maxAge: 1000*60*60*24*7,  
    sameSite:'none',
  },
  proxy: true,
};

app.use(session(sessionConfig));

Hope this helps!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ron Vaknin
Solution 2