How JSON Web Token (JWT) Secures Your API
Anthony Gore | April 15th, 2019 | 4 min read
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
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
// Payload
eyJzdWIiOiIxMjM0NTY3ODkwIn0
// Signature
dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U
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"
}
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"
}
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
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"
);
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();
}
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();
}
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:
- It decodes the token and extracts the ID from the payload.
- It looks up the user in the database with this ID.
- 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
}
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();
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:
About Anthony Gore
If you enjoyed this article, show your support by buying me a coffee. You might also enjoy taking one of my online courses!
Click to load comments...