PassportJS — The Confusing Parts Explained
PassportJS is awesome. It provides an abstraction layer over logging in with various providers such as Facebook, Google, Github, Twitter and more.
When first getting started though it can be a little challenging to understand what’s going on and why. Their documentation is pretty good but leaves out some specifics that I initially found difficult to understand. Hopefully this helps answer some of your questions and clears up some confusion. I assume you’ve read the docs a little and have maybe tried implementing it. Even if you haven’t though, you should still be able to follow along. Even if you have, this should still prove useful in understanding PassportJS a little better.
Here’s what’s covered
- The Callback Function in Strategy Setup
- Why passport.authenticate() is needed in the callback
- Checking if the User is Logged in
- Understanding serializeUser() and deserializeUser()
Alright, lets go!
Issue 1: Understanding the Callback in Strategy Setup
Take a look at the code below. We require our modules, set up our google strategy and create two routes, one for handling logging in and another for the callbackURL.
When a user visits /lgoin/google, we authenticate with google and in this case, request their email and profile information.
One key part though is, as part of passport.authenticate(), the function on line 9 is called before the console.log() is on line 15.
This function is the first time you have access to the profile information. Typically this is where you would add the user to your DB if it doesn’t already contain it (See code example below). But how do you exit this function to get back to your code, specifically to the route handler for the callbackURL specified on line 19.
To do this you call done().
What is done()? An internal PassportJS function. You pass it two arguments, null and the profile info like this:
This takes the profile info and attaches it on the request object so its available on your callback url as req.user. This will be available for the route /login/google/return as defined on line 41 and this is where you would set the session for the user and then send them on their way to another part of your site.
Issue 2: Why the passport.authenticate() in the callbackURL?
If a user makes it back to /login/google/return, why do we need another passport.authenticate()? This is partly to make sure the user didn’t just go to the route directly and verifies that they are in fact logged in. This is handled internally by PassportJS.
As a side note, you should not use passport.authenticate()as a method to check if a user is logged in. This should only be used to actually log a user in which is needed in this case since they might not yet be logged in if they just typed in the url and route and hit enter.
Issue 3: Check if a User is Logged In
On line 36 in the code block above we set req.session.user to equal the profile of the person who just logged in. To check if someone is logged in we can just see if this value has been set and then use this function as middleware on any route we want where the user has to be logged in. Here is an example of the middleware function.
Then if you want to ensure a user is logged in for a route, just do this
This will call isLoggedIn. If req.session.user is set then the request continues on due to the next() call. Otherwise the user is redirected to /login.
Issue 4: What is this Serialization and Deserialization Business?
From above we said that passport attaches the profile information to req.user. This occurs as a direct result of the serializeUser() and deserializeUser() functions.
When you write done() in the callback function specified when setting up the strategy (look at the second code block all the way at the top where we do done(null, dbUserRecord or done(null, newUser)), we pass in the entire user profile object (in this example as either dbUserRecord or as newUser).
The reason for this is because it is received by serializeUser(). This function then calls done(null, user.id). Passport takes that user id and stores it internally on req.session.passport which is passport’s internal mechanism to keep track of things.
Then in deserializeUser(), the first argument is an id which is the same id that was passed in done(null, user.id) of serializeUser(). deserializeUser() then makes a request to our DB to find the full profile information for the user and then calls done(null, user). This is where the user profile is attached to the request handler at req.user. Then finally after all this occurs, the user is routed back to the /login/google/return route handler where we can finally access the user profile information on req.user.
To understand this user flow a little better take a look at the diagram on this StackOverflow answer: https://stackoverflow.com/questions/27637609/understanding-passport-serialize-deserialize?answertab=votes#tab-top.
You might ask yourself now, why is all this necessary. Handling user info in this manner means PassportJS only has to store the user id and not the entire user profile. This decreases the likelihood of confusing req.session.user which is something we set and req.session.passport.user which is PassportJS’s way of keeping track of what’s going on.
This can be seen by logging req.session which will return something like this
Notice how passport.user is the same as user._id. They have to be so we can perform the database lookup with the right information. Further passport.user only stores the id and nothing else like name or googleID which we see in the user object.
PassportJS makes it really easy to get setup but understanding its data flow can be confusing. I hope this helps explain some of the underlying principles it uses. If you have any questions please leave them in the comments.
Thanks for reading!