A crash course on securing Serverless APIs with JSON web tokens

Adnan Rahić
May 3, 2018 · 11 min read

What a mouthful of a title. Wouldn’t you agree? In this walkthrough you’ll learn about securing your Serverless endpoints with JSON web tokens.

This will include a basic setup of a Serverless REST API with a few endpoints, and of course an authorizer function. This authorizer will act as the middleware for authorizing access to your resources.

During the creation process, we’ll use the Serverless framework for simulating a development environment just like you’re used to. Wrapping up the guide we’ll also set up a monitoring tool called Dashbird. It will allow us to simulate the debugging capabilities and overview of a regular Node.js application in a way that’s natural and easy to comprehend. It also has a free tier and doesn’t require a credit card to set up.

If anything I just mentioned above is new to you, don’t worry. I’ll explain it all below. Otherwise you can freshen up your knowledge by taking a look at these tutorials:

TL;DR

Ready? Let’s jump in!

Creating the API

The installation process is incredibly simple. You set up an AWS management role in your AWS account, and link it to your installation of the Serverless framework. The actual installation process is just running one simple command.

Fire up a terminal window and run the command below.

$ npm install -g serverless

Moving on, once you have it installed, there’s only one more command to run in the terminal to get a boilerplate Serverless service on your local development machine.

$ sls create -t aws-nodejs -p api-with-auth

The command above will generate the boilerplate code you need.

Change to the newly created directory called api-with-auth and open it up with your code editor of choice.

$ cd api-with-auth

Once open, you’ll see two main files. A handler.js and a serverless.yml file. The handler.js contains our app logic while the serverless.yml defines our resources.

Now it’s time to install some dependencies in order to set up our needed authentication/authorization methods, password encryption and ORM for the database interaction.

$ npm init -y
$ npm install --save bcryptjs bcryptjs-then jsonwebtoken mongoose

There’s what we need for production, but for development we’ll grab the Serverless Offline plugin.

$ npm install --save-dev serverless-offline

Lovely!

Adding a database

In the root of the service folder let’s create a db.js file to keep our logic for the database connection. Go ahead and paste in this snippet of code.

This is a rather simple implementation of establishing a database connection if no connection exists. But, if it exists, I’ll use the already established connection. You see the process.env.DB? We'll use a custom secrets.json file to keep our private keys out of GitHub by adding it to the .gitignore. This file will then be loaded in the serverless.yml. Actually, let's do that now.

Add your MongoDB connection string to the db field.

With this file created, let’s move on to the serverless.yml. Open it up and delete all the boilerplate code so we can start fresh. Then, go ahead and paste this in.

As you can see, it’s just a simple setup configuration. The custom section tells the main configuration to grab values from a secrets.json file. We'll add that file to the .gitignore because pushing private keys to GitHub is a mortal sin punishable by death! Not really, but still, don't push keys to GitHub. Seriously, please don't.

Adding the functions

There are a total of five functions.

  • The VerifyToken.js will contain an .auth method for checking the validity of the JWT passed along with the request to the server. This will be our authorizer function. The concept of how an authorizer works is much like how a middleware works in plain old basic Express.js. Just a step between the server receiving the request and handling data to be sent back to the client.
  • The login and register functions will do the basic user authentication. We'll add business logic for those in the AuthHandler.js file.
  • However, the me function will respond with the current authenticated user based on the provided JWT token. Here's where we'll use the authorizer function.
  • The getUsers function is just a generic public API for fetching registered users from the database.

From the serverless.yml file above you can make out a rough project structure. To make it clearer, take a look at the image above.

Makes a bit more sense now? Moving on, let’s add the logic for fetching users.

Adding business logic for the users

Pretty straightforward if you’ve written a Node app before. We require Mongoose, create the schema, add it to Mongoose as a model, finally exporting it for use in the rest of the app.

Once the model is done, it’s time to add basic logic.

This is a bit tricky to figure out when you see it for the first time. But let’s start from the top.

By requiring the db.js we have access to the database connection on MongoDB Atlas. With our custom logic for checking the connection, we've made sure not to create a new connection once one has been established.

The getUsers helper function will only fetch all the users, while the module.exports.getUsers Lambda function will connect to the database, run the helper function, and return the response back to the client. This is more than enough for the UserHandler.js. The real fun starts with the AuthProvider.js.

Adding the authentication

First we require the dependencies, and add the module.exports.register function. It's pretty straightforward. We're once again connecting to the database, registering the user and sending back a session object which will contain a JWT token. Take a closer look at the local register() function, because we haven't declared it yet. Bare with me a few more seconds, we’ll get to it in a moment.

With the core structure set up properly, let’s begin with adding the helpers. In the same AuthHandler.js file go ahead and paste this in as well.

We’ve created three helper functions for signing a JWT token, validating user input, and creating a user if they do not already exist in our database. Lovely!

With the register() function completed, we still have to add the login(). Add the module.exports.login just below the functions comment.

Once again we have a local function, this time named login(). Let's add that as well under the helpers comment.

Awesome! We’ve added the helpers as well. With that, we’ve added authentication to our API. As easy as that. Now we have a token-based authentication model with the possibility of adding authorization. That’ll be our next step. Hang on!

Adding the authorization

We have a single function exported out of the file, called module.exporst.auth with the usual three parameters. This function will act as a middleware. If you're familiar with Node.js you'll know what a middleware is, otherwise, check this out for a more detailed explanation.

The authorizationToken, our JWT, will be passed to the middleware through the event. We're just assigning it to a local constant for easier access.

All the logic here is just to check whether the token is valid and send back a generated policy by calling the generatePolicy function. This function is required by AWS, and you can grab it from various docs on AWS and from the Serverless Framework examples GitHub page.

It’s important because we pass along the decoded.id along in the callback. Meaning, the next Lambda Function which sits behind our VerifyToken.auth authorizer function will have access to the decoded.id in its event parameter. Awesome, right!?

Once we have the token verification completed, all that’s left if to add a route to sit behind the authorizer function. For the sake of simplicity, let’s add a /me route to grab the currently logged user based on the JWT passed along the GET request.

Jump back to the AuthHandler.js file and paste this in.

Awesome! The last Lambda Function we’ll add in this tutorial will be module.exports.me. It'll just grab the userId passed from the authorizer and call the me helper function while passing in the userId. The me function will grab the user from the database and return it back. All the module.exports.me Lambda does is just retrieves the currently authenticated user. But, the endpoint is protected, meaning only a valid token can access it.

Great work following along so far, let’s deploy it so we can do some testing.

Deployment

$ sls deploy

Voila! Wait for it to deploy, and start enjoying your Serverless API with JWT authentication and authorization.

You’ll get a set of endpoints sent back to you in the terminal once the functions have been deployed. We’ll be needing those in the next section.

Testing

Note: If you want to start by testing everything locally, be my guest. You can always use serverless-offline.

In your terminal, run a simple command:

$ sls offline start --skipCacheInvalidation

But I like to go hardcore! Let’s test directly on the deployed endpoints.

Starting slow, first hit the /register endpoint with a POST request. Make sure to send the payload as JSON. Hit Send and you'll get a token back! Nice, just what we wanted.

Copy the token and now hit the /me endpoint with a GET request. Don't forget to add the token in the headers with the Authorization key.

You’ll get the current user sent back to you. And there it is. Lovely.

Just to make sure the other endpoints work as well, go ahead and hit the /login endpoint with the same credentials as with the /register endpoint you hit just recently.

Does it work? Of course it does. There we have it, a fully functional authentication and authorization system implemented in a Serverless environment with JWT and Authorizers. All that’s left is to add a way to monitor everything.

Monitoring

Pressing on one of the Lambda Functions, let’s say register, you’ll see the logs for that particular function. The bottom will show a list of invocations for the function. You can even see which were crashes and cold starts.

Pressing on the cold start invocation will take you to the invocation page and you’ll see a nice log which says => using new database connection.

Now backtrack a bit, and pick one of the invocations which is not a cold start. Checking the logs for this invocation will show you => using existing database connection.

Nice! You have proper insight into your system!

Wrapping up

The approach of using authorizers to simulate middleware functions is incredibly powerful for securing your Serverless APIs. It’s a technique I use on a daily basis. Hopefully you’ll find it of use in your future endeavors as well!

If you want to take a look at all the code we wrote above, here’s the repository. Or if you want to dig deeper into the lovely world of Serverless, have a look at all the tools I mentioned above, or check out a course I authored.

Hope you guys and girls enjoyed reading this as much as I enjoyed writing it. Do you think this tutorial will be of help to someone? Do not hesitate to share. If you liked it, smash the clap below so other people will see this here on Medium.

We’ve moved to freeCodeCamp.org/news

We’ve moved to https://freecodecamp.org/news and publish tons of tutorials each week. See you there.