loading...
Cover image for A peep beneath the hood of PassportJS' OAuth flow

A peep beneath the hood of PassportJS' OAuth flow

anabella profile image anabella ・6 min read

Libraries are great. They provide standardized, widely compatible and clean way of doing common tasks, abstracting away the details we usually don't care about. They help us not to worry about the specifics of authentication, database handling or form validation and just write the code for that thing we have in mind.

But then, what happens when something doesn't work as expected? How do you know what went wrong if you're just pushing buttons on a black box?

Sooner or later it's necessary to understand how our borrowed libraries do that little thing they do 🎶 to figure out where we (or them) took a wrong turn and be able to correct it.

This was the case for me when I decided to learn about PassportJS for a personal project. And in this article I intend to dig into what I found most elusive about handling an OAuth flow with PassportJS.

If you need a full tutorial on how to implement PassportJS for OAuth, as always, I recommend the one by the wonderful Net Ninja on YouTube. Or if you just want a refresher about how to dance the OAuth, you can visit my previous article on the subject.

Index

  • Basic PassportJS setup
  • Calling authenticate
  • Road to the verify callback
  • WTF is serialize and deserialize
  • The complete login flow
  • The authenticated request flow

Basic PassportJS setup

These are the only things we need to get started: a passport strategy that's properly configured and 2 endpoints (one for authorizing, the other for redirecting) .

Calling authenticate

The great thing about Passport is that you can register any number of strategies with it and then tell it which one to use according to the route that's being called, using the authenticate method, like so:

passport.authenticate('github');

When you configure one of the strategies you'll have to define some parameters and also a verify callback function that will handle the user data it gets back from the provider.

The weird thing, at least for me, was the reason behind having to call passport.authenticate() in two different routes.

But here's the trick:

The first time authenticate() is called passport will try to find if it has a strategy by the name of the string you pass to the function previously registered. If it does, it'll start the OAuth dance by hitting the provider's authorize endpoint. If it doesn't find it, it'll just throw an error saying that strategy is unknown.

Now, the second time it's called is within a callback from the provider's OAuth server, in the redirect route. This time, though it looks exactly the same, Passport will detect that it's on the second stage of the OAuth flow and tell the strategy to use the temporary code it just got to ask for an OAuth token. The strategy knows exactly how and where to ask for that. 

What happens after this?

Road to the verify callback

Have a look at my latest hand drawn creation, a diagram about the OAuth flow in PassportJS. At this point we're reaching that red bubble that says getProfile():

Hand drawn diagram

If this makes you more confused than before, read on; I promise it gets better!

The first thing that happens after we get the OAuth token is that the strategy fetches that user's profile. This is an internal mechanism of the strategy that knows where to ask for it on that specific provider.
 
Right after that, the strategy will try to parse the profile into a model it has defined internally for that provider, and then pass it with all the other data it has (accessToken, refreshToken and profile) to our verify callback.

Remember we defined the verify callback when we configured the strategy? Now is the first time our custom code in there gets executed by the strategy. In this instance we could check the database for that user, create a record for it if necessary and verify anything else that's needed.

Once we've checked all that we needed, we'll call done (or the callback of the verify callback) which is its fourth and last function argument. We'll pass it null (for no errors) and the user with all the information we find relevant.

(accessToken, refreshToken, profile, done) => {
  // verify things here and then...
  done(null, {accessToken, profile})
}

And finally, Passport will execute its very own req.login() which will save that user into req.user for further use.
 
Check that diagram up there again, you should understand it much better now.

Up next comes serializeUser 👇

WTF is serialize and deserialize

Serialization is the process of translating data structures or object state into a format that can be stored or transmitted and reconstructed later.
––Wikipedia's Serialization Article

In our case, "the data" is that user we've been tossing around. Our own custom code in Passport's serializeUser method should define what pieces of information we need to persist into the session in order to be able to retrieve the full user later by passing it to serializeUser's done callback. 

This is Passport's serialize user method in a very simple form:

 

passport.serializeUser((user, done) => done(null, {
  id: user.profile.id,
  accessToken: user.access_token
}))

☝️this object will end up in req.user and req.session.passport.user for subsequent requests to use.

Now for deserializeUser, this function will receive the user data present in the session and use it to get all of the user's data from our DB. For example:

passport.deserialize((user, done) => {
  dbHelper.getUser(user.id)
    .then(profile => done(profile))
})

Whatever gets passed to done here will be available in req.user.
 

The complete login flow

Let's do a zoom-in of the previous diagram, specifically after the OAuth dance is over. I wanted to dig deeper into that because I remember it being particularly mysterious when I was starting to use PassportJS for OAuth.

Close up of previous drawn diagram

So this is what happens after the user says "yes, allow" and our app gets their access token:

  • Passport receives an OAuth token from the provider
  • It uses it to fetch the user's profile information
  • The verifyCallback runs, and when it's done it passes the user object to its own done callback
  • Passport calls its own method req.login() which then calls serializeUser(). serializeUser extracts some user info to save in the session and then continues with the following handlers of the redirect route.

The authenticated request flow

Now, this is all very nice, but how does our app know that the user is still authenticated on further requests and that it can provide private information safely? 

This is not a full tutorial, but if you've been following one, you probably have something like this in your server code:

server.use(passport.initialize())
server.use(passport.session())

These lines configure two middlewares that will run on every request that our server gets.

When an authenticated request is made, Express will load the session into the req, making our serialized user data available at req.session.passport.user.

Then, the first middleware, initialize(), will try to find that user in the request, or create it as an empty object if it doesn't exist (which would mean the user is not authenticated). 

And then, session() will kick in which to determine if the request is authenticated by trying to find a serialized object in it.
 
When it finds it, it'll pass it to deserializeUser which will use it to get the whole user data (maybe from the DB) and add it to req.user where we can use it to create other requests.

So, even though serializeUser is only called on log in, deserializeUser is a global middleware that'll get executed on every single request to make the full user object available for authenticated requests.


This concludes my deep dive into the OAuth flow which I hope has helped you to understand what is going on behind the scenes of PassportJS at least a bit better. It surely helped me clear some doubts to write it. Thanks for reading!

While I was researching for this article I came across this wonderful unofficial documentation for PassportJS by J. Walton's which will surely help you in any other doubts you might have.

Posted on by:

anabella profile

anabella

@anabella

Full-stack developer, recovering ex-QA

Discussion

markdown guide
 

Nice explanation. Hardly people talk about these things.

 

Thank you! I could have really used this when I started trying to use Passport 😅