How to secure a REST API using JWT

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.

This is image title
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 the explained with the following examples:

  • Authentication: John logs in with his username and password successfully, therefore he is authenticated.
  • Authorization: He doesn’t have permissions 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 a 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.

{
    "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 setup an additional route for authenticating users. Inside routes/index.js, add the following three new lines:

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.

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:

This is image title

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:

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.

This is image title

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.

This is image title

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

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.

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

This is image title

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 how it works. If you are interested in the jsonwebtoken module, NPMJS has a documentation with examples on how to use its node implementation.

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!

#javascript #nodejs

How to secure a REST API using JWT
38.50 GEEK