Why is PassportJS in Node not removing session on logout

70,509

Solution 1

Brice’s answer is great, but I still noticed an important distinction to make; the Passport guide suggests using .logout() (also aliased as .logOut()) as such:

app.get('/logout', function(req, res){
  req.logout();
  res.redirect('/'); //Can fire before session is destroyed?
});

But as mentioned above, this is unreliable. I found it behaved as expected when implementing Brice’s suggestion like this:

app.get('/logout', function (req, res){
  req.session.destroy(function (err) {
    res.redirect('/'); //Inside a callback… bulletproof!
  });
});

Hope this helps!

Solution 2

Ran into the same issue. Using req.session.destroy(); instead of req.logout(); works, but I don't know if this is the best practice.

Solution 3

session.destroy may be insufficient, to make sure the user is fully logged out you have to clear session cookie as well.

The issue here is that if your application is also used as an API for a single page app (not recommended but quite common) then there can be some request(s) being processed by express that started before logout and end after logout. If this were the case then this longer running request will restore the session in redis after it was deleted. And because the browser still has the same cookie the next time you open the page you will be successfully logged in.

req.session.destroy(function() {
    res.clearCookie('connect.sid');
    res.redirect('/');
});

That's the what maybe happening otherwise:

  1. Req 1 (any request) is received
  2. Req 1 loads session from redis to memory
  3. Logout req received
  4. Logout req loads session
  5. Logout req destroys session
  6. Logout req sends redirect to the browser (cookie is not removed)
  7. Req 1 completes processing
  8. Req 1 saves the session from memory to redis
  9. User opens the page without login dialog because both the cookie and the session are in place

Ideally you need to use token authentication for api calls and only use sessions in web app that only loads pages, but even if your web app is only used to obtain api tokens this race condition is still possible.

Solution 4

I was having the same issue, and it turned out to not be a problem with Passport functions at all, but rather in the way I was calling my /logout route. I used fetch to call the route:

(Bad)

fetch('/auth/logout')
  .then([other stuff]);

Turns out doing that doesn't send cookies so the session isn't continued and I guess the res.logout() gets applied to a different session? At any rate, doing the following fixes it right up:

(Good)

fetch('/auth/logout', { credentials: 'same-origin' })
  .then([other stuff]);

Solution 5

I was having the same issues, capital O fixed it;

app.get('/logout', function (req, res){
  req.logOut()  // <-- not req.logout();
  res.redirect('/')
});

Edit: this is no longer an issue.

Share:
70,509
Jeffrey Chen
Author by

Jeffrey Chen

Updated on September 07, 2021

Comments

  • Jeffrey Chen
    Jeffrey Chen over 2 years

    I am having trouble getting my system to log out with PassportJS. It seems the logout route is being called, but its not removing the session. I want it to return 401, if the user is not logged in in specific route. I call authenticateUser to check if user is logged in.

    Thanks a lot!

    /******* This in index.js *********/
    // setup passport for username & passport authentication
    adminToolsSetup.setup(passport);
    
    // admin tool login/logout logic
    app.post("/adminTool/login",
        passport.authenticate('local', {
            successRedirect: '/adminTool/index.html',
            failureRedirect: '/',
            failureFlash: false })
    );
    app.get('/adminTool/logout', adminToolsSetup.authenticateUser, function(req, res){
        console.log("logging out");
        console.log(res.user);
        req.logout();
        res.redirect('/');
    });
    
    
    // ******* This is in adminToolSetup ********
    // Setting up user authentication to be using user name and passport as authentication method,
    // this function will fetch the user information from the user name, and compare the password     for authentication
    exports.setup = function(passport) {
        setupLocalStrategy(passport);
        setupSerialization(passport);
    }
    
    function setupLocalStrategy(passport) {
        passport.use(new LocalStrategy(
            function(username, password, done) {
                console.log('validating user login');
                dao.retrieveAdminbyName(username, function(err, user) {
                    if (err) { return done(err); }
                    if (!user) {
                        return done(null, false, { message: 'Incorrect username.' });
                    }
                    // has password then compare password
                    var hashedPassword = crypto.createHash('md5').update(password).digest("hex");
                    if (user.adminPassword != hashedPassword) {
                        console.log('incorrect password');
                        return done(null, false, { message: 'Incorrect password.' });
                    }
                    console.log('user validated');
                    return done(null, user);
                });
            }
        ));
    }
    
    function setupSerialization(passport) {
        // serialization
        passport.serializeUser(function(user, done) {
            console.log("serialize user");
            done(null, user.adminId);
        });
    
        // de-serialization
        passport.deserializeUser(function(id, done) {
            dao.retrieveUserById(id, function(err, user) {
                console.log("de-serialize user");
                done(err, user);
            });
        });
    }
    
    // authenticating the user as needed
    exports.authenticateUser = function(req, res, next) {
        console.log(req.user);
        if (!req.user) {
            return res.send("401 unauthorized", 401);
        }
        next();
    }
    
  • Michael
    Michael over 10 years
    Adding req.session.destroy(); worked for me, I had the same problem
  • Hrushikesh
    Hrushikesh over 10 years
    req.session.destroy(); worked for me too and remember to clear you cookies with res.clearCookie('cookiename'). However does anyone knows what exactly req.logout() do?
  • ztirom
    ztirom over 10 years
    @WebHrushi you can find the function in passport/lib/passport/http/request.js. Today both versions stopped working for me, redirect seems in both ways called before the session is finally destroyed. On the redirected page both ways described here seem to be working fine.
  • Gavin
    Gavin almost 10 years
    I don't like the idea of not using req.logout(), for forward compatibility and whatnot, so I just added req.session.destroy(); after req.logout() and that works fine.
  • schlenger
    schlenger over 9 years
    I just get 'Object #<Session> has no method 'destroy'' as errormessage
  • jlmakes
    jlmakes over 9 years
    @schlenger Hmm, the above is using Express 3. Are you using version 4?
  • cyberwombat
    cyberwombat over 9 years
    The current version of Passport accepts either logOut or logout (same with login and logIn)
  • IIllIIll
    IIllIIll about 8 years
    I'm not using Angular so this doesn't make any sense. :/
  • Godfather
    Godfather almost 8 years
    why delay of 2 seconds, when you can use an asynchronous callback
  • Matt Browne
    Matt Browne almost 8 years
    The callback was not firing consistently for me.
  • frooble
    frooble about 7 years
    Be careful because, in the example given here, the usage of session.destroy assumes use of the express-sessions module. Other session modules (e.g. mozilla node-client-sessions) adds a destroy method that returns synchronously only and disregards any callback pararmeter. In which case your code will hang
  • Tom Hughes
    Tom Hughes almost 7 years
    I was having issues with both logout and login occasionally not sticking, and this fixed it for me.
  • Charlie Fish
    Charlie Fish almost 7 years
    @TomHughes Glad it helped!!
  • Fareed Alnamrouti
    Fareed Alnamrouti almost 7 years
    your code can lead to an unexpected behavior if the cache is slow or if you have a middle ware running after the logout logic, you should use req.session.destroy(()=> res.redirect("/"));
  • Green
    Green over 6 years
    session.destroy is asynchronous and requires a callback. You should call res.redirect('/'); in that callback.
  • oztune
    oztune over 6 years
    Are you basing this on client-sessions? It's destroy function doesn't take an argument: github.com/mozilla/node-client-sessions/blob/master/lib/…
  • Paul S
    Paul S almost 6 years
    This solves the issue on the client, rather than on the server, which seems insecure. if connect.sid gets hijacked before you log out it shouldn't allow another party to login as you after you have logged out.
  • Paul S
    Paul S almost 6 years
    logout and logOut were aliased to each other in 2015: github.com/jaredhanson/passport/blame/…
  • emonhossain
    emonhossain about 4 years
    ok I use your second method. The session file in the sessions folder is deleted and the session cookie is removed from the client, but the Node server shows following error message in the console: [session-file-store] will retry, error on last attempt: Error: ENOENT: no such file or directory, open ... how could I fix this?
  • Dinesh Nadimpalli
    Dinesh Nadimpalli almost 4 years
    Welcome! Please refer to this guide for improving your answers stackoverflow.com/help/how-to-answer
  • Azhar Husain
    Azhar Husain over 3 years
    Was searching for 4 hours before ending up on this answer, a silly mistake but thanks for pointing out.
  • Andrew Nolan
    Andrew Nolan over 3 years
    Thanks for trying to support. I would suggest fixing the code formatting in your answer. The main snippet you posted isn't valid JS.
  • 10 Rep
    10 Rep over 3 years
    Why will this code work? Can you provide more detail around that? Thanks!