passport: `req.isAuthenticated()` returning false immediately after login
I’ve been fighting for quite awhile with this bug: immediately after the user authenticates with, say, Google, req.isAuthenticated() is returning false. I’m persisting sessions to a Postgres DB, which seems to be working fine. It’s just the call to isAuthenticated which leads me to wonder if my Passport configuration might be wrong, or something.
My code for the callback looks like:
const redirects = {
successRedirect: '/success',
failureRedirect: '/failure'
};
app.get('/auth/google/callback', passport.authenticate('google', redirects));
My very last middleware logs the value of req.isAuthenticated(). It logs false when Google redirects back to my page, but if the user manually refreshes then it returns true.
Here are detailed logs of the logging in process:
# this is the first request to the login page. Not logged in yet.
4:59:54 PM web.1 | -----New request-----
4:59:54 PM web.1 | route: /login authenticated: false
# they've clicked the link to `/login`, and are immediately forwarded to Google
4:59:59 PM web.1 | -----New request-----
# they've granted across through Google; Google redirects back to app.
# Using the Google profile, we get the user's account from the user account table
5:00:06 PM web.1 | -----New request-----
5:00:06 PM web.1 | about to fetch from DB
5:00:07 PM web.1 | retrieved user from DB
# redirection to the success page...`authenticated` is false? It should now be true!
5:00:07 PM web.1 | -----New request-----
5:00:07 PM web.1 | route: /success authenticated: false
# here's a manual refresh...now they're showing as authenticated
5:05:34 PM web.1 | successful deserialize
5:05:34 PM web.1 | -----New request-----
5:05:34 PM web.1 | route: /success authenticated: true
It looks like deserialize isn’t being called when Google redirects back to my app; could that be the source of the issue?
Source code:
About this issue
- Original URL
- State: closed
- Created 8 years ago
- Reactions: 1
- Comments: 33
Links to this issue
Commits related to this issue
- Explicityly save the session before redirect after login https://github.com/jaredhanson/passport/issues/482#issuecomment-230594566 — committed to syonfox/I_Comander by deleted user 5 years ago
- Fixing immediate redirect but didn't show flash issue. ref: https://github.com/jaredhanson/passport/issues/482 . sol: Manually adding req.session.save(). — committed to hengjenchiang/RamenLog by hengjenchiang 2 years ago
@dougwilson ultimately provided the answer over here.
express-session tries to delay the redirect, but some browsers don’t wait for the whole response before directing. So you need to manually save before redirecting. In my app, this looks like:
I’ve dedicated this whole day to solving this issue. So far, here’s what I’ve got. Versions of the libs I’m using:
Passport
0.3.2Google Strategy for Passport1.0.0Express-session1.14.0node-connect-pg-simple3.1.0(for persisting sessions to Postgres)Description of the problem
When the user signs in with Google, they are sent back to my application. However, m application shows them as logged out. If they refresh the app, then they are displayed as logged in.
The problem begins with the callback route from logging in through Google. In my app, that URL is
auth/google/callback. Let’s walk through the middleware to see if we can find out where unexpected behavior occurs.The first relevant middleware is
express-session. Sometimes, there’s an existing session in the DB. Other times, there isn’t. Either way, it doesn’t matter. If there is a session, then there is no user data because Passport hasn’t confirmed that the user is logged in. We would expect the session to get updated after Passport does its thing.Next up is the Passport middleware. Passport automatically has a Session Strategy set up (you, as the developer, do not need to do anything). Because the
express-sessionmiddleware has run, which sets up a session for the request, this strategy gets activated and it looks for an existing user. We’ve already determined that we shouldn’t expect a user, so, as expected, strategy fails.Next up is the Google Strategy that we’ve configured for this route. This one succeeds, because the user clicked “Allow” on the Google page. The Passport success process begins.
This is where things get interesting, so I’m going to slow down a bit.
strategy.succeed(src)You can wind your way through Passport’s API, but the important stuff begins with this method. This is called when a Strategy succeeds. It’s a big function, but we’re only concerned about a few things.
What we need to know is that at this time, the Google Strategy successfully parsed the response from Google, and knows who you are. It then calls this method, passing the
userto it. Everything is good so far. Next up, we let Passport log us in.req.logIn(src)This is the first interesting thing that strategy.succeed does.
Before we talk about it, an important thing to know is that Passport maintains a special attr on the session called
passport. And by default it sets who the user is under the keyuser. So a very important piece of our request isreq.session.passport.user.The role of
logInis to set that up for us. It does that usingserializeUser, which delegates to the method that you, the developer, configure in your app (example here). For me, this is a synchronous operation that just returnsuser.id.my
deserializeUser()(click to expand)Alright, so, what’s going on now is that our session has been written to. It has a key that can be used to identify our user in the future. Pretty dope. We’re sent back to the
strategy.succeedmethod…strategy.succeed (logIn callback)(src)Here’s where the issue comes in (I think). If you set up a redirect URL via the
successRedirectoption, then it’s immediately called. If you don’t set one up, then you’re probably using another middleware that immediately redirects like so. This is then called.Alright, so, let’s assume that we’re redirecting somehow, and jump over to Express.
response.redirect(src)The important bit here is that the request is ended, always.
express-session: endSomewhat surprisingly, this lands us back into the very first middleware: express-session. This middleware replaces res.end with its own version, which is used to persist session data.
I don’t think the the source of express-session was optimized for readability, but the important thing to know is that the session will save itself if its been modified. Back in
logIn, the session was modified, so the save begins now.This happens in
connect-pg-simple, but the important bit is that the Express redirect happens before the save completes.Instead, a new request begins while the save is in progress…
/successLet’s start over. The first thing that happens is that the session is initialized. It immediately begins a request for the session, which hits the DB.
This is where the race condition stuff comes in. A get and a save are in flight at the same time. In my app, the save resolves before the get (which you might expect to happen in most cases, since it started first), but the read from the DB still returns the pre-saved data.
If you remember, the pre-saved data didn’t have a user (because Passport never logged them in), so the user ends up being considered logged off.
The cause of the issue
The ultimate cause of the issue seems to be that Express begins the new request before the old request is completely done. I’m not sure why this is!
All thanks goes to @dougwilson honestly : )
I’ve got it on my todo to spend more time trying to figure this out. I’ll post an update when I’ve got one ✌️
That’d be great.
Yeah, that might be.
Npnp. Yeah, that’s definitely a possibility. I’ve tried a few different configurations based on existing projects. They all had the same problem.
The solution works for me. I’m using a local strategy with passport, a custom callback and saving the session manually fix the bug. I tested on localhost and it works fine. Thanks to @jamesplease and @dougwilson.
Here is my code: this is my passport config:
This is my login route:
I hope this will help other to solve this bug. This is the the repository passport_auth
@jmeas Thanks for all your hard work and investigation which lead to @dougwilson. I was stuck on this for a long time.
i think when use express-session and store session to db will cause this issue.i can resolve it by call ‘req.session.save’ before res.redirect;but i think you should call ‘req.session.save’ when call ‘failureRedirect’ or ‘successRedirect’ function too.if i set failureFlash:true, the failureRedirect can not read req.flash(‘error’) too. sorry for my poor english! @jmeas
Very slowly working my way through the issue. I’ve determined that Passport is failing to initialize properly under certain conditions.
The furthest back I’ve tracked the difference between a successful login and a bad one is these lines.
When the login immediately works (which is only if the user has never logged in before on that server instance), then
req.session[passport._key].useris set in that conditional.When the login fails until the user refreshes, then
req.session[passport._key].useris undefined.The downstream consequences of this are as follows:
sessionstrategy never findssureq[property]isAuthenticated()can’t find the property, and returnsfalseThe search continues…
I’ve gone a little further back.
The order goes:
http.req#logInauthenticate#initializesession#authenticateStep 1:
logIntakesreq._passport.sessionand assigns it toreq.session._passport. Step 2:authenticatetakesreq.session._passportand assigns it toreq._passport.sessionStep 3:sessionsearches forreq._passport.session.userI have no idea why 1 and 2 are circular, or where any additional value comes from outside of this loop. @jaredhanson , any guidance on how I can make sense of this? 😛
I have opted to add an “unsecured” redirect route that is redirected to from the ‘/login/callback’ route:
app.get( '/redirect', function (req, res) { res.status(200).send('<html><head><meta http-equiv="refresh" content="0; url=' + req.query['redirect_uri'] + '" /></head><body>Redirecting...</script></body></html>') } )It appears that once the callback route fully completes, the passport.isAuthenticated() method will finally return true.It’s not a solution…it’s way to diagnosis problem. Is race condition with async calls. Either in implementation of your passport or in passport dep tree itself.