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.
TL;DR
Before jumping in head first, you can severely hurt my feelings and only read this TL;DR. Or, continue reading the whole article. ❤
Ready? Let’s jump in!
Creating the API
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
Lovely!
Adding a database
For the persistent data store, we’ll just grab a hosted MongoDB instance on MongoDB Atlas. Here’s a reference for an article where I explained it in detail.
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
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.
- 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
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.
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
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!
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
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 auth
folder.
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.
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
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.
Testing
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.
Monitoring
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!
Wrapping up
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.
Disclaimer: Tracetest is sponsoring this blogpost. Use observability to reduce time and effort in test creation + troubleshooting by 80%.