Passport provides authentication middleware for Express-based applications. We’re going to use it to implement social sign-on using three popular platforms: Facebook, Twitter, and Google. All three of these offer an OAuth flow for federated authentication, and there are Passport plugins to help simplify the configuration. Even with Passport’s help, configuring all of these services takes some finesse.
Of course, you can use Passport to implement regular old username/password authentication as well, but this introduces new complications, like where to store user identities, how to encrypt passwords, how to support users who have forgotten their passwords, etc. Since users frequently already have an account with at least one authentication provider like Facebook, Twitter, or Google, social sign-in is an increasingly popular way to outsource these concerns.
The basic setup of Passport for your application is the same either way. Let’s get Passport installed and configured, then use it to connect to the authentication providers.
To start, install the Passport Node.js module.
| | $ npm install --save -E passport@0.4.0 |
Next, open your server.js file for editing. After the part on configuring Express sessions that you added earlier, insert this new Passport setup section.
| | // Passport Authentication. |
| | const passport = require('passport'); |
| | passport.serializeUser((profile, done) => done(null, { |
| | id: profile.id, |
| | provider: profile.provider, |
| | })); |
| | passport.deserializeUser((user, done) => done(null, user)); |
| | app.use(passport.initialize()); |
| | app.use(passport.session()); |
Passport requires you to implement methods to serialize and deserialize users. That is, you have to tell Passport how to go from a user’s identity token to an actual user object and vice versa. Typically, this is where you’d reach out to a database of some kind to load up an account object based on the user’s ID. Both serializeUser and deserializeUser take a single callback function.
The callback function you pass to serializeUser should take a Passport User Profile object,[92] and call done with the minimum amount of data necessary to identify that user. For example, this function could query a database, then return a simple user ID string. In our case, we’re not going to store per-user data in a database (aside from book bundles, which we’ll get to), so all we need is to keep track of the user’s ID and which provider the user authenticated with. This is sufficient to uniquely identify a user in our system.
The deserializeUser method takes a function that does the opposite. Given the identifier produced by your serializeUser callback, your deserializeUser function should retrieve the full object. In our case, the identifier is an object with the id of the user and the provider the user signed in with. Since this is all we know about the user, there’s no need to perform any kind of lookup and we can just send this object to done straight away. For more info on how to implement these callbacks, check out Passport’s session documentation.[93]
After that, we call app.use on the output of passport.initialize and passport.session. Order is important here! The passport.session middleware must come after the expressSession that you added earlier. Otherwise sessions may not be restored properly.
The next step is to set up a couple of generic session routes: /auth/session for info about the session and /auth/signout to allow users to sign off. Let’s add those.
We’ll need two Express routes irrespective of which authentication provider the user signs in with. The first, /api/session, returns information about the current user session. We’ll call this asynchronously from the front end in app/index.ts.
Open your server.js file for editing and navigate to the bottom. Just before the call to app.listen, insert the following:
| | app.get('/api/session', (req, res) => { |
| | const session = {auth: req.isAuthenticated()}; |
| | res.status(200).json(session); |
| | }); |
Passport adds an isAuthenticated method to the Express request object, req. Here, the /api/session route returns an object with an auth property that’ll be either true or false. After you save, you can try it out in a terminal with curl.
| | $ curl -s b4.example.com:60900/api/session |
| | {"auth":false} |
Now, after the /auth/session route, add the following to set up the /auth/signout route.
| | app.get('/auth/signout', (req, res) => { |
| | req.logout(); |
| | res.redirect('/'); |
| | }); |
Along with isAuthenticated, Passport adds a logout method to the req object. After calling it, we redirect the user back to the main page.
With these routes in place, we can now wire them into the front end.
Now that we have a /api/session route that can return a session object, let’s pass this object to the templates. This is necessary so we can conditionally show session-based info to the user (like the Sign Out link).
In your app directory, open the index.ts file for editing. To simplify requests to the back end, start by adding this utility function toward the top of the file:
| | /** |
| | * Convenience method to fetch and decode JSON. |
| | */ |
| | const fetchJSON = async (url, method = 'GET') => { |
| | try { |
| | const response = await fetch(url, {method, credentials: 'same-origin'}); |
| | return response.json(); |
| | } catch (error) { |
| | return {error}; |
| | } |
| | }; |
The fetchJSON async function takes two parameters: a required url string to fetch, and an optional method that defaults to GET. This function fetches the desired URL using the fetch API.[94]
Note the use of the credentials option. This ensures that credential information (cookies) is sent with the request. Without this option, by default fetch will not send cookies, meaning the back end would treat the request as unauthenticated.
If the fetch call succeeds, then we send back the Promise returned by response.json. Callers of fetchJSON will await the result and receive the deserialized JSON object. If anything went wrong, we return an object containing the error.
Now to use fetchJSON to get the session information. Scroll down in your index.ts file to the showView method. In the switch block, update the #welcome case to be this:
| | case '#welcome': |
| | const session = await fetchJSON('/api/session'); |
| | mainElement.innerHTML = templates.welcome({session}); |
| | if (session.error) { |
| | showAlert(session.error); |
| | } |
| | break; |
Now, instead of calling the welcome template with an empty session object, we use fetchJSON to GET /api/session, then pass the resulting object to the template. If there was an error, we use showAlert to display it.
Scroll to the bottom of the index.ts file and find the anonymous async function that performs the page setup. Update it to read as follows:
| | // Page setup. |
| | (async () => { |
| | const session = await fetchJSON('/api/session'); |
| | document.body.innerHTML = templates.main({session}); |
| | window.addEventListener('hashchange', showView); |
| | showView().catch(err => window.location.hash = '#welcome'); |
| | })(); |
Instead of calling main with an empty object, here we pass it the user’s session object.
It may seem odd to you that we’re reaching out to /api/session twice instead of doing it just once and reusing that object throughout. The reason is that the session may change over time.
Cookies do not last forever. Eventually they expire. By default, the cookies that the Express session stack creates last about a day. This means that if users revisit the welcome page a day later, they’ll be signed out.
The right thing to do in that case is show them the sign-in buttons so they can reauthenticate. Grabbing a fresh session object from /api/session inside of the showView method ensures they’re seeing session-appropriate content.
Once you save index.ts, nodemon will automatically restart your service. You won’t see any visible changes in the UI yet; for that we’ll need to implement at least one authentication provider and sign in with it. Let’s start with Facebook, then proceed to Twitter and Google.