42
votes

I've got Passport setup to authenticate users stored in mongodb. Seems to work fine: authentication succeeds/fails appropriately and session variables get set. However, getting Passport to check for a session is failing. Something seems to be quite wrong in that the console.log statements I've added to the deserializeUser callback never see the light of day. I assume my problem is related to deserializeUser never being called. Anyone able to diagnose my misstep?

// Passport configuration
passport.serializeUser(function(user, cb){ cb(null, user.id) });
passport.deserializeUser(function(uid, cb){
  console.log("Trying to deserialize user: "+uid);
  User.findById(uid, function(err, user){
    cb(err, user);
  });
});
// auth strategy function
passport.use(new LocalStrategy({usernameField: 'email'},
  function(email, pass, done){
    User.findOne({email: email}, function (err, user) {
      if (err)
        return done(err);
      if (!user)
        return done(null, false, {message: "Couldn't find user"});
      var crypted = bcrypt.hashSync(pass, user.salt);
      if(user.hashpass != crypted)
        return done(null, false, {message: "Bad password"});
      return done(null, user);
    });
  }
));

passport.CreateSession =  function (req, res, next) {
  passport.authenticate('local', function(err, user, info){
    if(err || !user)
      return res.json({status: "Failure: "+err});
    req.logIn(user, function (err){
      if(err)
        return res.json({status: "Failure: "+err});
      return res.json({status: "Authenticated"});
    });
  })(req, res, next);
};

with the following in app.js:

app.post('/session', passport.CreateSession); // restify better
app.del('/session', passport.DestroySession);
app.get('/images', passport.CheckSession, routes.images);
12

12 Answers

40
votes

For anyone else who is having this issue, take a look at this:

app.use(session({ 
    secret: 'something', 
    cookie: { 
        secure: true
    }}));

If you have cookie.secure set to true and you're NOT using SSL (i.e. https protocol) then the cookie with the session id is not returned to the browser and everything fails silently. Removing this flag resolved the problem for me - it took hours to realise this!

29
votes

If you are using the authenticate callback when you authenticate with passport you need to log the user in manually. It will not be called for you.

passport.authenticate('local', function (err, user) {
    req.logIn(user, function (err) { // <-- Log user in
       return res.redirect('/'); 
    });
})(req, res);
13
votes

Have you use()'d passport.session() middleware? Like in this example:

https://github.com/jaredhanson/passport-local/blob/v1.0.0/examples/login/app.js#L91

That's what restores the session and calls deserializeUser, so it sounds like that may be missing.

7
votes

Okay, this took me almost 3 days to solve, which seems to be a simple fix, at least in my case. I had to place:

app.use(require('cookie-parser'));

and

app.use(require('express-session')({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: false
}));

BEFORE calling:

app.use(passport.initialize());
app.use(passport.session());

Note: These snippets are not my actual code, I have taken it from GitHub, but that is the gist of it.

2
votes

In my case it was because I had this code:

app.get("/auth/google/callback", 
    passport.authenticate("google", { 
        failureRedirect: "/admin/login/?error",
    }),
    (req, res) => {
        return res.redirect("/admin/");
    },
);

Apparently in this case req.login() will not be called (which calls deserialize). I changed it to this and it worked:

app.get("/auth/google/callback", 
    passport.authenticate("google", { 
        successRedirect: "/admin/", 
        failureRedirect: "/admin/login/?error",
    })
);
1
votes

Today I faced the same issue, and in my case the cause was how the client side makes the API calls. And the following approach helped me to overcome the problem:

const doFetch(url, method = 'GET', body) {
  const headers = new Headers();
  headers.append('Content-Type', 'application/json');
  headers.append('Accept', 'application/json');
  headers.append('Cache', 'no-cache');
  const params = { 
    method, headers, 
    credentials: 'include' // The cookie must be sent!
  }
  if(body) {
    params.body = body;
  }
  return fetch(url, params);
}

After this, deserializeUser began to be called properly.

1
votes

I fought the same problem the entire day. I had a custom callback set up to do authorizations (like at the bottom of http://passportjs.org/guide/authenticate/). It looks like Passport preprocesses all request objects before they even hit the other middleware (based on some logging statements I wrote). As part of that preprocessing, it places the deserialized user information into the request.user object. Unfortunately, the default behavior of passport.authenticate ('local', callback) seems to be to ignore any session or cookie data, and instead assume that the username and PW are being sent in the request object. What I did to fix this was to first check the request.user object, and if it existed, it meant that Passport had done its preprocessing and then I could call login directly. Here is my authorization function, which I use in place of passport.authenticate('local'):

function doAuth(request, response, next)
{ 

if (request.user) {
    request.logIn(request.user, function (err) {
        if (err) {
            console.log(err);
            response.status(500).json({message:
                    "Login failed. Auth worked. Nonsense"});
            return;
        }
        console.log("doAuth: Everything worked.");
        next();
    });
    return;
}

passport.authenticate('local', function(err, user, info) {
    if (err) {
        response.status(500).json({message: "Boo Passport!"});
        return; //go no further
        //until I figure out what errors can get thrown
    }
    if (user === false) {
        console.log("Authentication failed. Here is my error:");
        console.log(err);
        console.log("Here is my info:");
        console.log(info);
        response.status(500).json({message: "Authentication failed."});
        return;
    }
    request.logIn(user, function(err) {
        if (err) {
        console.log("Login failed. This is the error message:");
        console.log(err);
        response.status(500).json({message:"Login failed."});
            return;
        }
        console.log("doAuth: Everything worked. I don't believe it.");

        next();
    });
})(request, response, next);

 }
0
votes

in my case, I've placed

app.use(cookieSession({
  maxAge : 60*60*1000,
  keys : [key.cookieSession.key1]
}));

above

app.use(passport.initialize());
app.use(passport.session());

and it worked.

0
votes

I open the google chrome with web-security or open incognite mode, it's working for me

web-security for linux /usr/bin/google-chrome-stable
--disable-web-security --user-data-dir=/home/jossnaz/.config/google-chrome/
0
votes

In Heroku, all requests come into the application as plain http, but with the X-Forwarded-Proto header to know whether the original request was http or https.

That causes express to see non-ssl traffic and so it refuses to set a secure cookie when running on Heroku. You have to tell express to trust the information in that header by setting 'trust proxy'.

app.set('trust proxy', 1);

right before

app.use(session...

might work for you if you're having trouble with this on Heroku specifically. If your cookies/session are messed up then that could be why deserialize is not being called.

-1
votes

make sure the line of code app.use(express.static(path.join(__dirname, 'public'))); stays below the app.use(require('express-session')

-4
votes
passport.serializeUser(
    function(user, done){
        done(null, user.id);
});

passport.deserializeUser(
    function(id, done){
        User.findById(id, function(err, user){
            if(err){
                done(err);
            }
            done(null, user);
        });
});

**> try this one**