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.
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:
- Securing Node.js RESTful APIs with JWT — Authentication and Authorization explained.
- A crash course on Serverless with Node.js— Serverless basics explained.
- Building a Serverless REST API with Node.js and MongoDB — Serverless REST APIs explained.
Before jumping in head first, you can severely hurt my feelings and only read this TL;DR. Or, continue reading the whole article. ❤
- Creating the API
Ready? Let’s jump in!
First of all, we need to set up the Serverless framework for our local development environment. This framework is the de facto framework for all things related to Serverless architectures. Jump over to their site and follow the instructions to set it up, or reference back to the article I linked above.
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
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.
Just a tiny bit of configuring left to do before jumping into the business logic! We need to add the function definitions in the
serverless.yml right below the providers section we added above.
There are a total of five functions.
VerifyToken.jswill contain an
.authmethod 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
- However, the
mefunction will respond with the current authenticated user based on the provided JWT token. Here's where we'll use the authorizer function.
getUsersfunction is just a generic public API for fetching registered users from the database.
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.
Back in your code editor, delete the
handler.js file and create a new folder, naming it user. Here you'll add a
User.js file for the model, and a
UserHandler.js for the actual logic.
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.
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
In the root of your service, create a new folder called auth. Add a new file called
AuthHandler.js. This handler will contain the core authentication logic for our API. Without wasting any more time, go ahead and paste this snippet into the file. This logic will enable user registration, saving the user to the database and returning a JWT token to the client for storing in future requests.
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!
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!
With the addition of a
VerifyToken.js file, we can house all the authorization logic as a separate middleware. Very handy if we want to keep separation of concerns. Go ahead and create a new file called
VerifyToken.js in the
We have a single function exported out of the file, called
module.exports.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.
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
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.
Hopefully, you’ve configured your AWS account to work with the Serverless Framework. If you have, there’s only one command to run, and you’re set.
$ 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.
The last step in any development process should ideally be making sure it all works like it should. This is no exception. One of the two tools I use for testing my endpoints is Insomnia. So, I’ll go ahead and open it up. But, you can use Postman, or any other tool you like.
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.
I usually monitor my Lambdas with Dashbird. It’s been working great for me so far. My point for showing you this is for you too see the console logs from the Lambda Function invocations. They’ll show you when the Lambda is using a new or existing database connection. Here’s what the main dashboard looks like, where I see all my Lambdas and their stats.
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!
Amazing what you can do with a few nice tools. Creating a REST API with authentication and authorization is made simple with Serverless, JWT, MongoDB, and Dashbird. Much of the approach to this tutorial was inspired by some of my previous tutorials. Feel free to check them out below.
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 unicorn below so other people will see this here on Dev.to.
If you liked this tutorial, you can support my writing by buying me a coffee.