{ Authentication with JWTs. }

Objectives:

By the end of this chapter, you should be able to:

  • Describe how to secure an API
  • Understand what a JWT is and how to create one
  • Authenticate an API using JSON Web Tokens

Authenticating an API

So how do we secure our API? We can do what we have been doing before and store information in the session and in cookies, but this can become problematic. This is especially true when you're working with lots of different servers (lots of small applications written in different languages) or when you're building mobile applications, which have severe limitations with cookies.

Instead of using cookies/sessions to authenticate users, we will be using tokens. This process involves creating an encrypted token on the server and sending it to the client where it will either be stored in a cookie or in localStorage (more commonly done with localStorage), and on every future request, the token will be placed in the header and will be decrypted on the server.

The server will store a secret key used to encrypt and decrypt the token so that there can not be tampering with the token in the client. This secret key is simply just a string of characters, but NEVER something you should make public when your application is in production.

You can read more about tokens versus cookies here.

The type of token that we will be using is a JSON Web Token, or JWT (pronounced "jot").

Using JWTs

Before we continue, read through this introduction to JWTs so you can understand a bit more about what they are and how they work. JWTs are a means of securing information between two parties and consist of three parts:

  • Header - metadata about the token (the type of algorithm used to sign and the type of token)
  • Payload - data to be stored in the token (an object with the data we want to store like a user id)
  • Signature - the result of the algorithm specified in the header (we will be using HMAC-SHA256) with an encoded header, encoded payload, and a secret passed to it.

Example App

You can find the complete code for this example here

The module we will be using to create secure tokens is jsonwebtoken.

We will start with the application we built in the previous section. The code for that can be found here.

The first thing we need to do is install the jsonwebtoken module, so in the Terminal let's make sure to run npm install jsonwebtoken

Let's make sure to add the following to our routes/users.js

const bcrypt = require("bcrypt");
const express = require("express");
const router = express.Router();
const db = require("../db");
const jsonwebtoken = require("jsonwebtoken");

const SECRET = "NEVER EVER MAKE THIS PUBLIC IN PRODUCTION!";

Notice here the secret is something we do not want to expose in production so we'll make this an environment variable, which you will learn about later! For now, let's create a JSON Web Token when the user successfully logs in. Here's what that looks like in our routes/users.js

// npm packages
const bcrypt = require("bcrypt");
const express = require("express");
const router = express.Router();
const db = require("../db");
const jsonwebtoken = require("jsonwebtoken");

const SECRET = "NEVER EVER MAKE THIS PUBLIC IN PRODUCTION!";

router.post("/login", async (req, res, next) => {
  try {
    // try to find the user first
    const foundUser = await db.query(
      "SELECT * FROM users WHERE username=$1 LIMIT 1",
      [req.body.username]
    );
    if (foundUser.rows.length === 0) {
      return res.json({ message: "Invalid Username" });
    }
    // if the user exists, let's compare their hashed password to a new hash from req.body.password
    const hashedPassword = await bcrypt.compare(
      req.body.password,
      foundUser.rows[0].password
    );
    // bcrypt.compare returns a boolean to us, if it is false the passwords did not match!
    if (hashedPassword === false) {
      return res.json({ message: "Invalid Password" });
    }

    // let's create a token using the sign() method
    const token = jwt.sign(
      // the first parameter is an object which will become the payload of the token
      { username: foundUser.rows[0].username },
      // the second parameter is the secret key we are using to "sign" or encrypt the token
      SECRET,
      // the third parameter is an object where we can specify certain properties of the token
      {
        expiresIn: 60 * 60 // expire in one hour
      }
    );
    // send back an object with the key of token and the value of the token variable defined above
    return res.json({ token });
  } catch (e) {
    return res.json(e);
  }
});

Let's test this out!

http localhost:3000/users
http POST localhost:3000/users/ username='elie' password='secret'
http POST localhost:3000/users/login username='elie' password='secret'
http POST localhost:3000/users/login username='elie' password='secretz'

Notice in the third request we get back a token! This token is simply just a string of characters that the client we are sending to would save (in the case of the browser, that would commonly be localStorage). Now that we're sending back tokens, let's verify them on future requests! Let's add the following to our routes/users.js

router.get("/secret", async function(req, res, next) {
  try {
    // let's try to get the token out of the header
    const authHeaderValue = req.headers.authorization;
    // let's verify that this was a token signed with OUR secret key
    const token = jwt.verify(authHeaderValue, SECRET);
    // if so - awesome!
    return res.json({ message: "You made it!" });
  } catch (e) {
    // otherwise send back a status code of 401 (Unauthorized) with a message
    return res.status(401).json({ message: "Unauthorized" });
  }
});

Let's test this out!

http POST localhost:3000/users/login username='elie' password='secret' # to get a token

http localhost:3000/users/secret Authorization:WHATEVER_TOKEN_YOU_JUST_GOT

The second response should give us an object with the key of message and a value of "You made it!". Let's try an incorrect request!

http localhost:3000/users/secret Authorization:hackedyou

We should now see a status code of 401 and a message of Unauthorized.

Refactoring to use Middleware

Imagine we wanted to make a couple more protected routes, it can be pretty tedious to have to repeat the extracting of a token from headers and verification - so let's put that in a helper function! As you have quite a few of these or want to re-use them in different files, it's nice to have a folder that contains this middleware. For now, we only need it in the users routes so let's keep it simple. In our routes/users.js

// helpful middleware to make sure the user is logged in
function ensureLoggedIn(req, res, next) {
  try {
    const authHeaderValue = req.headers.authorization;
    const token = jwt.verify(authHeaderValue, SECRET);
    return next();
  } catch (e) {
    return res.status(401).json({ message: "Unauthorized" });
  }
}

router.get("/secret", ensureLoggedIn, async function(req, res, next) {
  try {
    return res.json({ message: "You made it!" });
  } catch (err) {
    return res.json(err);
  }
});

module.exports = router;

Ensuring the correct user

So far we are able to protect routes that depend on having a valid JWT, but let's imagine we have a route that displays information about a user who is logged in. If we want to authorize only users with that username to be able to access their information there's a bit more we might have to do. Let's add the following to our routes/users.js

// helpful middleware to make sure the username stored on the token is the same as the request
function ensureCorrectUser(req, res, next) {
  try {
    const authHeaderValue = req.headers.authorization;
    const token = jwt.verify(authHeaderValue, SECRET);
    if (token.username === req.params.username) {
      return next();
    } else {
      return res.status(401).json({ message: "Unauthorized" });
    }
  } catch (e) {
    return res.status(401).json({ message: "Unauthorized" });
  }
}

router.get("/:username", ensureCorrectUser, async function(req, res, next) {
  try {
    return res.json({ message: "You made it!" });
  } catch (err) {
    return res.json(err);
  }
});

module.exports = router;

Notice here we are adding some middleware to check if the username we are storing on the token is the same as what is passed in the URL. If those are different that means a user with a different username is trying to access someone elses information.

You can find the complete code for this example here

Specifying the type of Authorization

One last point that you must know, is that very commonly the Authorization header does not come in this fashion like we've seen:

Authorization: Value

Instead, we first specify the type of authorization, which in our case is called Bearer. There are other types of Authorization like Basic, which we will not be using. So you may see the Authorization header look like this:

Authorization: Bearer Value

What this means is instead of the following:

const authHeaderValue = req.headers.authorization;

We need to simply just split the string and get the second value separated by a space:

const authHeaderValue = req.headers.authorization.split(" ")[1];

When you're ready, move on to Input Validation with JSONSchema

Continue

Creative Commons License