First published September 12, 2018 in ITNEXT

Implementing JSON Web Tokens & Passport.js in a React App

How and why to use the various auth options Passport.js has to offer.

Passport, JWT, Express, and React logos

Introduction

Before you say it, I know, I know. There’s already a literal ton of MERN tutorials out there showing how to use JSON Web Tokens (JWT, pronounced "jot") and Passport.js with Express.

But here’s what every one of those tutorials failed to mention, and what mine will cover: how and why to use the various authentication flavors Passport offers (including passport-jwt), and the gotchas that tripped me up for hours on end as I put together a user registration application.

I don’t know what your experience has been, but the majority of the projects I’ve worked on professionally have come with authorization already prebuilt by someone else. So in addition to improving my MERN skills, I viewed this as an opportunity to learn more about security and authentication and different ways to do it.

This post will mainly cover how to implement of Passport and JWT on the server side, because that’s where most of the magic happens, but if you want, you can see the source files and download the full MERN project here.

Once it’s correctly hooked up on the backend, the frontend is purely React.

Before I jump into all that though, let me give you a quick run down of JWT and Passport.js authentication.

What is a JSON Web Token?

JWT logo

A JSON Web Token, according to the site is:

“…an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.” — JWT.io

Essentially, JWTs are digitally signed tokens that can be verified and trusted, and they’re becoming more and more popular for security and authorization between parties (like servers and clients) and for exchanging sensitive information, while verifying the token hasn’t been tampered with or decoded.

I won’t go into the details of how the tokens work, you can read all about that from a much more qualified source than I am here, but suffice it to say, I chose to use JWT as part of my user verification strategy along with Passport.js to make my simple user registration application secure (and because I was curious to see if I could do it, since I didn’t have a whole lot of hands on experience implementing authorization).

With that covered, the next piece of my authentication solution is Passport.js.

What is Passport.js & Why Should I Use it?

Passport logo

Passport.js is:

Simple, unobtrusive authentication for Node.js — Passport.js

And it works exceedingly well with Express.js. Passport is authentication middleware for Node, which serves one purpose, to authenticate requests, in a modular way that leaves all other functionality to the application itself, making code cleaner, easier to maintain and provides a clear separation of concerns.

If you type the term "JavaScript authentication middleware" or even just "JavaScript authentication" into the Google search bar, Passport.js ranks within the top 5 search results. That’s how ubiquitous this solution is in the JavaScript ecosystem.

Did I mention it boasts more than 500+ authentication strategies? It does. Whether you wish to log in with a simple username and password, with Github credentials, Facebook, oAuth, etc., there’s probably a Passport.js strategy for it.

So it was a simple decision to choose Passport as part of my authorization strategy.

I’d like to add documentation on the site is pretty good, though some things like custom callbacks require much more careful reading (and trial and error, for me) to correctly set up. But I’m getting ahead of myself, I’ll cover the gotchas in implementation.

How Do I Implement Them in Express.js (and a little React)?

Now on to the fun part, how do you implement both Passport.js and JWT into an Express/Node application? To be honest, it stumped me for a good while. But after numerous tutorials, re-readings of documentation and turning to Stack Overflow for help, I got to a place of understanding and satisfaction.

I should say, one of my goals for this app was to make it modular, make it super simple to add or remove functionality, so when you see my file structure, you’ll notice all the routes for the CRUD functionality (create, read, update, delete) are built into separate files, and all the Passport authentication is handled in one, centralized file as well. Here’s a look at the file structure of the API portion of my app.

The API File Tree

root/
├── api/ 
| ├── config 
| | ├── passport.js 
| | ├── jwtConfig.js
| ├── server.js 
| ├── sequelize.js 
| ├── package.json
| ├── models/ 
| | ├── user.js
| ├── routes/ 
| | ├── deleteUser.js 
| | ├── findUsers.js 
| | ├── loginUser.js 
| | ├── registerUser.js
| | ├── updateUser.js
| ├── node-modules/

The package.json File

And here is the package.json and its dependencies so you can see exactly what I’m using.

package.json

{
  "name": "mysqlregistration-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon server.js --exec babel-node --presets es2015"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/paigen11/mysqlRegistration.git"
  },
  "author": "Paige Niedringhaus",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/paigen11/mysqlRegistration/issues"
  },
  "homepage": "https://github.com/paigen11/mysqlRegistration#readme",
  "dependencies": {
    "babel-cli": "^6.26.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "bcrypt": "^3.0.2",
    "body-parser": "^1.18.3",
    "cors": "^2.8.4",
    "dotenv": "^6.1.0",
    "express": "^4.16.3",
    "helmet": "^3.20.1",
    "jsonwebtoken": "^8.3.0",
    "morgan": "^1.9.0",
    "mysql2": "^1.5.3",
    "nodemailer": "^4.6.8",
    "nodemon": "^1.18.3",
    "passport": "^0.4.0",
    "passport-jwt": "^4.0.0",
    "passport-local": "^1.0.0",
    "sequelize": "^5.3.0",
    "sequelize-cli": "^4.0.0",
    "swagger-jsdoc": "^3.2.9",
    "swagger-ui-express": "^4.0.2"
  }
}

The dependencies include a few extras like babel so I can use ES6 syntax in my Node.js app, bcrypt for password hashing and sequelize as my MySQL ORM, but the things you need to focus on are jsonwebtoken, passport, passport-local and passport-jwt. These are the necessities for this blog.

The server.js File

I’ll start with the server.js file first, as it requires the least explanation. This file is purely for starting the server, initializing the use of Passport in the app and setting up the routes and parsing of requests from the client side.

server.js

import express from 'express';
import Cors from 'cors';
import bodyParser from 'body-parser';
import logger from 'morgan';
import passport from 'passport';
import helmet from 'helmet';

const app = express();

const API_PORT = process.env.API_PORT || 3000;

require('./config/passport');

app.use(Cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(logger('dev'));
app.use(helmet());
app.use(passport.initialize());

require('./routes/loginUser')(app);
require('./routes/registerUser')(app);
require('./routes/findUsers')(app);
require('./routes/deleteUser')(app);
require('./routes/updateUser')(app);

// eslint-disable-next-line no-console
app.listen(API_PORT, () => console.log(`Listening on port ${API_PORT}`));

module.exports = app;

A little ES6 in the imports and consts, but what you should focus on is the passport.initialize() and the routes.

Pretty straightforward right? Great. Moving on.

You might also have noticed the require('./config/passport');, this is the next step I’ll go over. The JWT and Passport configuration, inside of the folder named config/.

The jwtConfig.js File

The JWT config is very simple, it’s the secret required by JWT to encode and decode the tokens. Typically, this would be stored as an environmental variable in a file that’s not checked in to Github, but to show how this works, I’ve set it in here.

jwtConfig.js

module.exports = {
  secret: 'jwt-secret',
};

The passport.js File

passport.js

import bcrypt from 'bcrypt';
import jwtSecret from './jwtConfig';

const BCRYPT_SALT_ROUNDS = 12;

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const JWTstrategy = require('passport-jwt').Strategy;
const ExtractJWT = require('passport-jwt').ExtractJwt;
const User = require('../sequelize');

passport.use(
  'register',
  new LocalStrategy(
    {
      usernameField: 'username',
      passwordField: 'password',
      passReqToCallback: true,
      session: false,
    },
    (req, username, password, done) => {
      try {
        User.findOne({
          where: {
            [Op.or]: [
              {
                username,
              },
              { email: req.body.email },
            ],
          },
        }).then(user => {
          if (user != null) {
            console.log('username or email already taken');
            return done(null, false, {
              message: 'username or email already taken',
            });
          }
          bcrypt.hash(password, BCRYPT_SALT_ROUNDS).then(hashedPassword => {
            User.create({
              username,
              password: hashedPassword,
              email: req.body.email,
            }).then(user => {
              console.log('user created');
              return done(null, user);
            });
          });
        });
      } catch (err) {
        return done(err);
      }
    },
  ),
);

passport.use(
  'login',
  new LocalStrategy(
    {
      usernameField: 'username',
      passwordField: 'password',
      session: false,
    },
    (username, password, done) => {
      try {
        User.findOne({
          where: {
            username,
          },
        }).then(user => {
          if (user === null) {
            return done(null, false, { message: 'bad username' });
          }
          bcrypt.compare(password, user.password).then(response => {
            if (response !== true) {
              console.log('passwords do not match');
              return done(null, false, { message: 'passwords do not match' });
            }
            console.log('user found & authenticated');
            return done(null, user);
          });
        });
      } catch (err) {
        done(err);
      }
    },
  ),
);

const opts = {
  jwtFromRequest: ExtractJWT.fromAuthHeaderWithScheme('JWT'),
  secretOrKey: jwtSecret.secret,
};

passport.use(
  'jwt',
  new JWTstrategy(opts, (jwt_payload, done) => {
    try {
      User.findOne({
        where: {
          id: jwt_payload.id,
        },
      }).then(user => {
        if (user) {
          console.log('user found in db in passport');
          done(null, user);
        } else {
          console.log('user not found in db');
          done(null, false);
        }
      });
    } catch (err) {
      done(err);
    }
  }),
);

Yes, all this is part of the passport.js file.

I realize this is a lot of code to take in. If you’d like to see just the code for Passport and the routes with Passport authentication, I created a few gists here with the code.

I’ll walk through the two types of Passport implementation happening here: passport-local for the register and login methods and passport-jwt for the jwt method.

passport-local uses a username and password, and passport-jwt uses a JWT payload to verify the user is legit.

Like I said earlier, the documentation for both using just Express.js is fairly good, but the trick comes in combining it with the MySQL database and the information passed from the React client on the frontend.

Once the passport-local strategy has been passed a username and password (which I verify that both inputs are at least filled on the client side before ever calling back to the server), the first check I do on the backend is with Sequelize, my SQL ORM (like Mongoose for MongoDB), to determine if that username exists in the database. If it returns null, the authentication fails (no user in the db matches), and typically, a 401 Unauthorized HTTP status would be thrown back from the server to the client with no further information.

Passport.js Gotcha #1: Info in Error Handling

This type of error handling, to me, was the most frustrating thing about Passport. The lack of information to let a user (or myself) know what the actual error was beyond 401 Unauthorized.

But there’s a better solution that requires a little extra work, with custom callbacks. I’ll go into more detail about the callback soon, but for now, you can see if there’s some sort of error, instead of returning a return done(null, user);, Passport can pass back return done(null, false, { message: 'bad username or passwords don't match' });.

This message can then be passed back from the server to the client, actually telling the user what the problem is (which is what I wanted for this app). If you prefer just to tell the user their authentication failed, but not disclose why, that’s cool too. Your choice, but I want to show how error messaging can be done.

The rest of the passport-local strategy is pretty self-explanatory, on register the user’s password is hashed and salted with the encryption package bcrypt, and then when the login method is invoked, it hashes the newly entered password and checks the passwords with bcrypt’s compare function before returning either a positive or negative on the verification.

Now that brings me to the passport-jwt strategy, which is named jwt. This is the authentication that’s called on the protected routes in the application: findUsers, updateUser, deleteUser.

For the jwt strategy, the JWT is passed back from the client with each call to the server (I passed mine in an authorization header with the key: JWT), which is extracted and then decoded using the secret (which is stored in another file, but should really be an environment variable known only to the system).

Once the JWT payload is decrypted, the ID (which for me is the username) can be searched for in the database just like the passport-local strategy does, and either returned with done(null, user);, if the user is found or with done(null, false); if it’s not (which should almost never happen because the JWT includes the username in its encrypted form, so unless that’s somehow been tampered with or the db has been, it should be able to find the user).

Passport.js Gotcha #2: Passport-Local Wants Return, Passport-JWT Does Not

This brings me to my second gotcha which tripped me up for a good bit of time; not all passport strategies require the same resolution.

If you’re looking closely, you’ll see the passport-local strategies both have return done(null, user); but the passport-jwt strategy has done (null, user);. See the difference? It’s minuscule, but having (or removing that return), is the difference between that user data being passed back from the middleware to the server or not.

And it halted my progress for a good few hours, before the kindness of Stack Overflow helped me work through the issue (it’s the first time I’ve actually had to ask SO for an answer I couldn’t find already, but it was well worth it). So be aware of when to return or not.

Ok, so I’ve walked through the two Passport strategies and the three methods I’m using for middleware authentication in my program, now on to after the authentication on the server.

The register.js File

The next step is how to implement these newly minted methods inside of the various routes. Here’s the registerUser route.

registerUser.js

import passport from 'passport';
import User from '../sequelize';

module.exports = app => {
  app.post('/registerUser', (req, res, next) => {
    passport.authenticate('register', (err, user, info) => {
      if (err) {
        console.error(err);
      }
      if (info !== undefined) {
        console.error(info.message);
        res.status(403).send(info.message);
      } else {
        req.logIn(user, error => {
          const data = {
            first_name: req.body.first_name,
            last_name: req.body.last_name,
            email: req.body.email,
            username: user.username,
          };
          User.findOne({
            where: {
              username: data.username,
            },
          }).then(user => {
            user
              .update({
                first_name: data.first_name,
                last_name: data.last_name,
                email: data.email,
              })
              .then(() => {
                console.log('user created in db');
                res.status(200).send({ message: 'user created' });
              });
          });
        });
      }
    })(req, res, next);
  });
};

As you can see, this implementation of Passport looks a little different than most examples out there, and it’s because I’m using the custom callback version of Passport, which requires a closure call. I’m using this so that the error message that’s passed back if Passport’s authentication fails can be sent from the server to the client instead of the obscure 401 Unauthorized.

Because this is being implemented as a callback (which is why you see (req, res, next) not once, but twice in these scripts; it gives me access to the (err, user, info) from the Passport middleware.

The info is what I’m interested in — that’s the message being sent back, so if the info is anything besides null, that means the authentication failed and I can then send the message to the client to let them know why.

Passport.js Gotcha #3: Don't Forget req.logIn() in Your Custom Callback

Now, I’m to the third Passport gotcha, in order to make the closures and custom callbacks work with passport-local, the little method req.logIn() must be called before the user data is handled if it comes back successfully from the middleware.

This is documented in the Passport documentation, but it’s importance is not stressed as much as I would like (and I missed it the first few times trying to get custom callbacks to work), which is why I highlight it now.

It must happen, and once that’s done, I’m able to take the extra inputs from the client side for registering a user, find that same user created in the database during the passport-local register call, and update the user file with the extra info. I could have passed this extra data through to the middleware as well, but I want Passport to only handle authentication, not user creation as well. Modularization, remember.

Plus, if the authentication were to be split out into a separate service with a separate database of just encrypted usernames and passwords, this would make it easier to do so, then use the username or ID to find and update the corresponding user record in this registration service.

Once all that’s been successfully taken care of, a 200 HTTP status and success message are sent back to the client.

The loginUser.js File

Seeing the loginUser route now should make more sense.

loginUser.js

import jwt from 'jsonwebtoken';
import passport from 'passport';
import jwtSecret from '../config/jwtConfig';
import User from '../sequelize';

module.exports = app => {
  app.post('/loginUser', (req, res, next) => {
    passport.authenticate('login', (err, users, info) => {
      if (err) {
        console.error(`error ${err}`);
      }
      if (info !== undefined) {
        console.error(info.message);
        if (info.message === 'bad username') {
          res.status(401).send(info.message);
        } else {
          res.status(403).send(info.message);
        }
      } else {
        req.logIn(users, () => {
          User.findOne({
            where: {
              username: req.body.username,
            },
          }).then(user => {
            const token = jwt.sign({ id: user.id }, jwtSecret.secret, {
              expiresIn: 60 * 60,
            });
            res.status(200).send({
              auth: true,
              token,
              message: 'user found & logged in',
            });
          });
        });
      }
    })(req, res, next);
  });
};

The same style of custom callbacks and closures are used, the biggest difference is that once the user is successfully verified and located in the database, the JWT token is generated using the jwt.sign(); function, which sets the username as the ID passed in the JWT, and encrypted by the secret I set earlier.

Once again, if all this works successfully, a 200 HTTP status is sent with a boolean I named auth set to true for the client side, the newly generated token and a short login success message.

The findUsers.js File

And last, but not least, here’s the findUsers route. I used the same passport-jwt authentication for the updateUser and deleteUser routes, so I’ll just show this one as an example.

findUsers.js

import passport from 'passport';
import User from '../sequelize';

module.exports = (app) => {
  app.get('/findUser', (req, res, next) => {
    passport.authenticate('jwt', { session: false }, (err, user, info) => {
      if (err) {
        console.log(err);
      }
      if (info !== undefined) {
        console.log(info.message);
        res.status(401).send(info.message);
      } else if (user.username === req.query.username) {
        User.findOne({
          where: {
            username: req.query.username,
          },
        }).then((userInfo) => {
          if (userInfo != null) {
            console.log('user found in db from findUsers');
            res.status(200).send({
              auth: true,
              first_name: userInfo.first_name,
              last_name: userInfo.last_name,
              email: userInfo.email,
              username: userInfo.username,
              password: userInfo.password,
              message: 'user found in db',
            });
          } else {
            console.error('no user exists in db with that username');
            res.status(401).send('no user exists in db with that username');
          }
        });
      } else {
        console.error('jwt id and username do not match');
        res.status(403).send('username and jwt token do not match');
      }
    })(req, res, next);
  });
};

I used custom callbacks for all my Passport authenticated routes (I appreciate good, clear error handling), so this code style should look routine by now.

This time, when passport.authenticate() is called, I implement the JWT strategy defined in the passport.js file. The same (err, user, info) gets passed back from the middleware, but there’s no call from req.logIn() this time.

Instead, I just return the user object found during the authentication and pass all the required fields to the client along with the auth boolean and success message. The JWT token is still stored in local storage on the client side so there’s no need to regenerate or pass it to the client again as well. Which brings me to my final Passport gotcha.

Passport.js Gotcha #4: Passing Authorization Headers Correctly

This one’s not exactly a Passport-specific gotcha, but it is yet another thing that tripped me up.

As I said, I pieced together my (now much more whole) understanding of JWT and Passport from a bunch of other tutorials and documentation, and one of those tutorials led me astray. It had me passing the JWT back to the client in something besides true authorization headers, which is the way that I chose to have my passport-jwt strategy extract the JWT payload from the client request.

For this reason, I wanted to touch briefly on one piece of the frontend code, where I pass back the token in the proper format, so you don’t have to waste the time debugging like I did.

Profile.js

 async componentDidMount() {
    const accessString = localStorage.getItem('JWT');
    const {
      match: {
        params: { username },
      },
    } = this.props;

    if (accessString == null) {
      this.setState({
        isLoading: false,
        error: true,
      });
    } else {
      try {
        const response = await axios.get('http://localhost:3003/findUser', {
          params: {
            username,
          },
          headers: { Authorization: `JWT ${accessString}` },
        });
        this.setState({
          first_name: response.data.first_name,
          last_name: response.data.last_name,
          email: response.data.email,
          username: response.data.username,
          password: response.data.password,
          isLoading: false,
          error: false,
        });
      } catch (error) {
        console.error(error.response.data);
        this.setState({
          error: true,
        });
      }
    }
  }

I’m using the popular promise-based HTTP client Axios to make my server calls, but what you really need to notice is the headers section.

Before the call to the server happens, I extract the JWT from local storage using localStorage.getItem('JWT') (the key I set with the JWT value when I passed the token forward from the loginUser() route on the server side), and then I set it using headers: { Authorization: JWT ${accessString} } and a little ES6 string interpolation. That way, if my authorization headers had more than one value to parse out, it would still be easy for passport-jwt to extract the correct JWT payload info by finding the string associated with "JWT".

And that’s it, I just wanted to cover it because it blocked me for a while and only through API testing with Insomnia, could I confirm my JWT token was actually working, and suss out that I was passing authorization headers from the client side incorrectly.

Conclusion

I know I wrote a lot (and provided a bunch of code snippets and gists), but authentication is not a simple concept. Nor is it truly possible to really make an app so secure no one will ever be able to hack it. But if you take precautions like JWT and Passport.js authentication middleware, your Node.js app should have a better chance to withstand people with malicious intent.

Just be sure to look out for the gotchas I highlighted around Passport — and you’ll be on your way to a more secure site.

Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.

Thanks for reading, I hope this proves helpful and gives you a better understanding of implementing Passport authentication and using JSON web tokens.

Further References & Resources

Want to be notified first when I publish new content? Subscribe to my newsletter.