DEV Community

Cover image for How JSON Web Token (JWT) Secures Your API
Anthony Gore
Anthony Gore

Posted on • Edited on

How JSON Web Token (JWT) Secures Your API

You've probably heard that JSON Web Token (JWT) is the current state-of-the-art technology for securing APIs.

Like most security topics, it's important to understand how it works (at least, somewhat) if you're planning to use it. The problem is that most explanations of JWT are technical and headache inducing.

Let's see if I can explain how JWT can secure your API without crossing your eyes!

API authentication

Certain API resources need restricted access. We don't want one user to be able to change the password of another user, for example.

That's why we protect certain resources make users supply their ID and password before allowing access - in other words, we authenticate them.

The difficulty in securing an HTTP API is that requests are stateless - the API has no way of knowing whether any two requests were from the same user or not.

So why don't we require users to provide their ID and password on every call to the API? Only because that would be a terrible user experience.

JSON Web Token

What we need is a way to allow a user to supply their credentials just once, but then be identified in another way by the server in subsequent requests.

Several systems have been designed for doing this, and the current state-of-the-art standard is JSON Web Token.

There's a great article on the topic which makes a good analogy about how JSON web tokens work:

Instead of an API, imagine you're checking into a hotel. The "token" is the plastic hotel security card that you get that allows you to access your room, and the hotel facilities, but not anyone else's room.

When you check out of the hotel, you give the card back. This is analogous to logging out.

Structure of the token

Normally a JSON web token is sent via the header of HTTP requests. Here's what one looks like:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
Enter fullscreen mode Exit fullscreen mode

In fact, the token is the part after "Authorization: Bearer", which is just the HTTP header info.

Before you conclude that it's incomprehensible gibberish, there are a few things you can easily notice.

Firstly, the token consists of three different strings, separated by a period. These three string are base 64 encoded and correspond to the header, the payload, and the signature.

// Header
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Enter fullscreen mode Exit fullscreen mode
// Payload
eyJzdWIiOiIxMjM0NTY3ODkwIn0
Enter fullscreen mode Exit fullscreen mode
// Signature
dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
Enter fullscreen mode Exit fullscreen mode

Note: base 64 is a way of transforming strings to ensure they don't get screwed up during transport across the web. It is not a kind of encryption and anyone can easily decode it to see the original data.

We can decode these strings to get a better understand of the structure of JWT.

Header

The following is the decoded header from the token. The header is meta information about the token. It doesn't tell us much to help build our basic understanding, so we won't get into any detail about it.

{
  "alg": "HS256",
  "typ": "JWT"
}
Enter fullscreen mode Exit fullscreen mode

Payload

The payload is of much more interest. The payload can include any data you like, but you might just include a user ID if the purpose of your token is API access authentication.

{
  "userId": "1234567890"
}
Enter fullscreen mode Exit fullscreen mode

It's important to note that the payload is not secure. Anyone can decode the token and see exactly what's in the payload. For that reason, we usually include an ID rather than sensitive identifying information like the user's email.

Even though this payload is all that's needed to identify a user on an API, it doesn't provide a means of authentication. Someone could easily find your user ID and forge a token if that's all that was included.

So this brings us to the signature, which is the key piece for authenticating the token.

Hashing algorithms

Before we explain how the signature works, we need to define what a hashing algorithm is.

To begin with, it's a function for transforming a string into a new string called a hash. For example, say we wanted to hash the string "Hello, world". Here's the output we'd get using the SHA256 hashing algorithm:

4ae7c3b6ac0beff671efa8cf57386151c06e58ca53a78d83f36107316cec125f
Enter fullscreen mode Exit fullscreen mode

The most important property of the hash is that you can't use the hashing algorithm to identify the original string by looking at the hash.

There are many different types of hashing algorithms, but SHA256 is commonly used with JWT.

In other words, we can't take the above hash and directly figure out that the original string was "Hello, world". The hash is complicated enough that guessing the original string would be infeasible.

JWT signature

So coming back to the JWT structure, let's now look at the third piece of the token, the signature. This actually needs to be calculated:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  "secret string"
);
Enter fullscreen mode Exit fullscreen mode

Here's an explanation of what's going on here:

Firstly, HMACSHA256 is the name of a hashing function and takes two arguments; the string to hash, and the "secret" (defined below).

Secondly, the string we hash is the base 64 encoded header, plus the base 64 encoded payload.

Thirdly, the secret is an arbitrary piece of data that only the server knows.

Q. Why include the header and payload in the signature hash?

This ensures the signature is unique to this particular token.

Q. What's the secret?

To answer this, let's think about how you would forge a token.

We said before that you can't determine a hash's input from looking at the output. However, since we know that the signature includes the header and payload, as those are public information, if you know the hashing algorithm (hint: it's usually specified in the header), you could generate the same hash.

But the secret, which only the server knows, is not public information. Including it in the hash prevents someone generating their own hash to forge the token. And since the hash obscures the information used to create it, no one can figure out the secret from the hash, either.

The process of adding private data to a hash is called salting and makes cracking the token almost impossible.

Authentication process

So now you have a good idea of how a token is created. How do you use it to authenticate your API?

Login

A token is generated when a user logs in and is stored in the database with the user model.

loginController.js

if (passwordCorrect) {
  user.token = generateToken(user.id);
  user.save();
}
Enter fullscreen mode Exit fullscreen mode

The token then gets attached as the authorization header in the response to the login request.

loginController.js

if (passwordCorrect) {
  user.token = generateToken(user.id);
  user.save();
  res.headers("authorization", `Bearer ${token}`).send();
}
Enter fullscreen mode Exit fullscreen mode

Authenticating requests

Now that the client has the token, they can attach it to any future requests to authentically identify the user.

When the server receives a request with an authorization token attached, the following happens:

  1. It decodes the token and extracts the ID from the payload.
  2. It looks up the user in the database with this ID.
  3. It compares the request token with the one that's stored with the user's model. If they match, the user is authenticated.

authMiddleware.js

const token = req.header.token;
const payload = decodeToken(token);
const user = User.findById(payload.id);
if (user.token = token) {
  // Authorized
} else {
  // Unauthorized
}
Enter fullscreen mode Exit fullscreen mode

Logging out

If the user logs out, simply delete the token attached to the user model, and now the token will no longer work. A user will need to log in again to generate a new token.

logoutController.js

user.token = null;
user.save();
Enter fullscreen mode Exit fullscreen mode

Wrapup

So that's a very basic explanation of how you can secure an API using JSON Web Tokens. I hope your head doesn't hurt too much.

There's a lot more to this topic, though, so here's some additional reading:


Enjoy this article?

Get more articles like this in your inbox weekly with the Vue.js Developers Newsletter.

Click here to join!


Top comments (2)

Collapse
 
otumianempire profile image
Michael Otu
const token = req.header.token;
const payload = decodeToken(token);
const user = User.findById(payload.id);
if (user.token = token) {
  // Authorized
} else {
  // Unauthorized
}

This line, if (user.token = token), assigns user.token to token instead of comparing it

Collapse
 
dpaine20 profile image
David Paine20

A great thanks for sharing the complex topic, in a more easy and understandable way. The words are properly supported by snippets. For the base64 decode, you can also refer to that tool url-decode.com/tool/base64-decode as an alternative. That tool also provides you the access to dozens of other web utilities as well.