OAuth 2 with Passport - 10 Steps Recipe
Recently I found myself integrating OAuth 2 into a React/node.js app. The web is full of blog posts and questions answered on how to do it, but I still had to scratch my head a few times to get everything right. This post is a simple step by step recipe on how to do it with Passport.
Step 1. Express boilerplate
I am using express so first things first:
1 | const express = require('express'); |
Step 2. Session middleware first
If you are using sessions and plan on using passport with sessions, make sure that session middleware goes first:
1 | const session = require('express-session'); |
Step 3. Passport
Now we can wire in passport:
1 | const passport = require('passport'); |
Step 4. Authentication route
This wasn’t apparent to me from the beginning and from the simple examples that I looked at, but:
passport is designed to only facilitate the authentication process. In other words, the only route that should have passport middleware on it is the
/login
route where you would send your unauthenticated users to
1 | app.get('/login', passport.authenticate('oauth2', { |
Actually, you should also add the passport middleware to your callback route or just make /login
your OAuth callback. That’s what I did. The OAuth strategy looks at ?code=
in the URL to decide whether to initiate the authentication sequence or process the callback:
1 | OAuth2Strategy.prototype.authenticate = function(req, options) { |
Step 5. Protected routes
To protect your routes and ensure authenticated access, you can use something like connect-ensure-login. Passport itself can’t help you with that:
1 | const { ensureLoggedIn } = require('connect-ensure-login'); |
Step 6. OAuth 2 strategy
To be able to do passport.authenticate('oauth2', {...})
as I showed in step 4, you should set up passport with the OAuth 2 strategy first:
1 | const OAuth2Strategy = require('passport-oauth2'); |
Step 7. De/Serialize user
In order not to run the authentication sequence on every request, you would typically store the authenticated user ID in the session and then trust it for as long as the session is active. You need to implement serializeUser
and deserializeUser
to do it. Passport doesn’t do it automatically:
1 | passport.serializeUser((user, done) => { |
Step 8. Tokens
OAuth 2 can send back access_token
and it can also send the id_token
. The latter is always a JWT token and the former is typically an opaque string.
Sometimes all you need is the access_token
that you pass on to the back-end APIs. I, however, needed to authenticate the user and match the user’s identity with the application’s user record.
Two options:
- Use the
/userinfo
endpoint with theaccess_token
to retrieve the profile from your identity provider - Ask for the
id_token
and get profile attributes from there. To receive theid_token
in the callback, you need to addscope=openid
to your authorization request. If you need user’s email or additional attributes like name, for example, you will need to ask for more scopes (scope=openid email
orscope=openid profile
).
OAuth 2.0 is not an authentication protocol, apparently. Read the User Authentication article on oauth.net if you want to learn more. The
id_token
, claims, scopes, and/userinfo
are all part of OpenID Connect.
Step 9. Retrieve Profile
When we set up the OAuth 2 strategy in step 6, we had to supply a tokenToProfile
callback. If you read the documentation, you will see that it has the following signature:
1 | function (accessToken, refreshToken, profile, cb) { |
Don’t be surprised to always receive an empty object in profile
:
OAuth 2 strategy for passport does not implement retrieval of the user profile
Here’s how it looks in the library:
1 | OAuth2Strategy.prototype.userProfile = function(accessToken, done) { |
You can either override it and use /userinfo
endpoint or you can rely on id_token
. Here’s how you would do the former:
1 | const strategy = new OAuth2Strategy({ ... }); |
The latter requires you to not only ask for the id_token
from your identity provider using scope=openid
, but to also have it exposed to you by the OAuth 2 strategy in passport. To do so, you need to set passReqToCallback
to true
when you instantiate the strategy (we did in step 6), and then you can use a different signatue for your callback:
1 | const jwt = require('jsonwebtoken'); |
Step 10. Logout
The easiest and the most effective way to logout a user is to destroy the session:
1 | router.get('/logout', function (req, res) { |
Step 11 (Bonus). Spoof Authentication
If you have gotten this far, I have a bonus step for you. I found it very helpful to be able to spoof authentication in local environment for development and testing.
First, the environment variable in my .env
file to signal that the auth should be bypassed and to tell the app what user to run on behalf of:
1 | AUTH_LOCAL_SPOOF_USER={"user_id": 2, "employeeID": "pavel@dontemailme.com", "role_id": 0} |
And then a bypass strategy:
1 | const strategy = new OAuth2Strategy({ ... }); |
And that’s it! Enjoy!