How to Secure Your API With JSON Web Tokens

How to Secure Your API With JSON Web Tokens

A simple way of adding authorization to your Express app
Ferenc AlmasiLast updated 2021 November 11 • Read time 9 min read
When dealing with APIs, we often have to think about restricting resources and routes. This tutorial teaches you how to secure your API with JSON Web Tokens
  • twitter
  • facebook
JavaScript

When dealing with APIs, we often have to think about restricting resources and routes. We can usually do this with the use of sessions. Sessions are stored in memory on the server-side.

But, we can also switch things around and take another approach. Store everything inside a token, which is stored on the client-side. We will take a look at how this can be achieved with the use of JWT.

Okay, so what is JWT?

What is JWT?

JSON Web Tokens are an open, industry-standard RFC 7519 method for representing claims securely between two parties.

The most common scenario for using JWT is for authorization. Once a user is logged in, each subsequent request will include a token. This token allows the user to access routes and make requests that are only permitted to authenticated users.

Each token is made up of three parts:

  • header: contains information about the algorithm and the token type
  • payload: contains arbitrary data. Usually, you would store information that identifies the user. Such as an id alongside with an expiration date. This ensures that the token expires over time and cannot be used indefinitely.
  • signature: this is where your token gets generated. It combines a base64 encoded version of your header and payload with a secret key of your choice.
Generation of JWT token on their official website
You can play around with token generation in the debugger on jwt.io

If I temper with the token on the client-side, it invalidates the signature. So if someone tries to make a request with a forged signature, we know that the user is not authenticated.

Authentication vs Authorization

Before moving on to coding, we must differentiate between authentication and authorization. While they sound similar, they do not mean the same thing. Authentication means we take a username and a password and check if they are correct. Authorization on the other hand is used for verifying any subsequent request to make sure they are originating from the same user we logged in.

In the context of user-based web applications this can be explained with the following examples:

  • Authentication: John logs in with his username and password successfully, therefore he is authenticated.
  • Authorization: He doesn’t have permission to manage user accounts, thus he is not authorized to access that page.

Or in more simple terms, as carefully worded in this post,

authentication is the process of verifying who you are, while authorization is the process of verifying what you have access to.

Setting Up The Project

First, you want to have Express installed and two routes ready. One for the login and one for managing todo items. I’m going to build upon a previous tutorial, where I used Express to build a REST API. You can clone the project repository from GitHub that will serve as a starting point. First, you’ll need to get JWT with npm i jsonwebtoken.

Copied to clipboard!
{
    "name": "express-api",
    "version": "1.0.0",
    "private": true,
    "scripts": {
        "start": "node server.js"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
        "express": "4.17.1",
+       "jsonwebtoken": "8.5.1",
        "node-localstorage": "2.1.5"
    }
}
package.diff

Set up routes

Here we want to set up an additional route for authenticating users. Inside routes/index.js, add the following three new lines:

Copied to clipboard!
const routes = (app) => {
    const todo  = require('../controllers/Todo');
+   const login = require('../controllers/Login');

+   app.route('/login')
+       .post(login.authenticate);

    // Todo Route
    app.route('/todo/:id?/')
        .get(todo.get)
        .post(todo.create)
        .put(todo.update)
        .delete(todo.delete);
};

module.exports = routes;
index.diff

This means that we have to create a new file under controllers. Name it Login with a method called authenticate. It will be called whenever we make a post request to /login.

Copied to clipboard! Playground
module.exports = {
    authenticate(request, response) {
        response.json({
            hello: '🌎'
        });
    }
};
Login.js

For now, this is all we need to see if everything works.

Testing the new route

I’m using Postman to test my changes. After starting the webserver, create a new POST request to /login. You should get back the same JSON response:

Testing the new route with Postman

Signing JWT

Now that we have everything set up, we can start using JWT. First we need to generate a new token whenever a user hits /login. To do that, let’s get rid of the mock response and replace it with the following:

Copied to clipboard! Playground
const jwt = require('jsonwebtoken');

module.exports = {
    authenticate(request, response) {
        if (request.body.email && request.body.password) {
            // Fetch user's data and verify credentials
            const user = getUser(request.body.email);

            jwt.sign(user, process.env.SECRET, (error, token) => {
                response.json({
                    id: user.id,
                    token
                });
            });
        } else {
            response.json({
                error: 'We\'ve couldn\'t sign you in 😔'
            });
        }

    }
};
Login.js

We need to pull in the JWT module first. Inside authenticate, the first thing should be to see if the user provided an email and a password. Then you would fetch the user and do all your verification steps. The last thing is to send back a response inside the callback of jwt.verify. This will accept:

  • A payload: the payload to sign, in this case, the user object
  • A private key: containing the secret for HMAC algorithms
  • A callback: this will return the token to us which we can send down to the client

You shouldn’t expose your secret key to the client. And to further secure it on the server, you can store it in a process variable. This prevents it from getting into source control.

Now if we create a new POST request with the required payload, we should get back a JSON Web Token.

getting back JWT
Looking to improve your skills? Check out our interactive course to master JavaScript from start to finish.
Master JavaScriptinfo Remove ads

Verifying JWT

Now that we have the token we can use it to verify subsequent requests to the API. Say we want to secure every method of the todo route. Right now, we have no problem accessing any of them.

Acessing the todo route without authentication

If we want to authenticate each route with JWT, it would mean we need to duplicate the same code four times. If the application grows, so does code duplication. So instead, let’s create a new function that acts as a wrapper.

Copied to clipboard! Playground
const jwt = require('jsonwebtoken');

module.exports = callback => {
    return (request, response) => {
        jwt.verify(request.headers.token, process.env.SECRET, (error, payload) => {
            if (error) {
                response.sendStatus(403);
            } else {
                callback(request, response);
            }
        });
    };
}
authorize.js

This function needs to return a new function with two parameters; request and response. This is what each route expects. We can use jwt.verify to verify the token. Here I send it as an additional HTTP header.

You also need to provide the same secret key that we used for signing. Lastly in the callback function, we can define our custom functionality. If there’s an error, we return 403. Otherwise, we can call the function we pass to authorize.

You also have access to the signed payload. To use this wrapper function, all we have to do is wrap each HTTP method into it.

Copied to clipboard! Playground
const authorize = require('../authorize');

const routes = (app) => {
    const todo  = require('../controllers/Todo');
    const login = require('../controllers/Login');

    app.route('/login')
        .post(login.authenticate);

    // Todo Route
    app.route('/todo/:id?/')
        .get(authorize(todo.get))
        .post(authorize(todo.create))
        .put(authorize(todo.update))
        .delete(authorize(todo.delete));
};

module.exports = routes;
routes.js

Notice how every todo route is wrapped into authorize. All that’s left to do is verifying if everything works as expected.


Testing Routes

testing the generated jwt

Now the route requires a token header with a valid JWT. If it’s not present we get back 403. If I try to mess with the token, it invalidates the signature and we get back 403 again. These routes will now only be accessible in the presence of a valid JSON Web Token.


Summary

If you would like to learn more about JWT, the introduction section on its homepage goes into depth about how it works. If you are interested in the jsonwebtoken module, NPMJS has documentation with examples on how to use its node implementation.

If you’re looking for an implementation in Java, I recommend checking out how you can use JWT with Spring Security.

Do you have suggestions on how to make the above code even more secure? Let us know in the comments. Thank you for your time and happy coding!

Get yourself an Expresso Sticker

That's right, I'm drinking Expresso

Continue the tutorial, by also learning how to create your very own Express Middleware functions:

How to Make Your Very Own Express Middleware
  • twitter
  • facebook
JavaScript
Did you find this page helpful?
📚 More Webtips
Frontend Course Dashboard
Master the Art of Frontend
  • check Access 100+ interactive lessons
  • check Unlimited access to hundreds of tutorials
  • check Prepare for technical interviews
Become a Pro

Courses

Recommended

This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.