Serverless Authentication with AWS Lambda
Tags: serverless, authentication, aws, dynamodb
Published: 2021-04-06 12:25:14

When it comes to authentication it is highly recommended to use a third party service. There are multiple options like [Auth0](https://auth0.com) or [Magic.link](https://magic.link/) that provide an easy to integrate authentication that allows us developers to not worry about security issues.

But, for those getting a bit curious about how authentication really works in serverless applications, I have come up with this easy tutorial that will guide you through the process.

We’ll be using serverless functions to configure our backend. I also like using the [serverless framework](https://serverless.com), since it simplifies a lot the process of developing and deploying serverless functions, while the serverless [offline plugin](https://www.serverless.com/plugins/serverless-offline/) emulates AWS Lambda and API Gateway on your local machine.

With all of that said, let’s start!

## Setup

As I’ve said, the serverless framework makes it easy to set up our serverless environment.

Just run these commands in your project's folder:

Install serverless CLI globally:
```bash
npm install -g serverless
```

Add serverless offline plugin:
```bash
npm install --save-dev serverless-offline
```

Make sure to [set up your AWS credentials](https://www.serverless.com/framework/docs/providers/aws/guide/credentials/) and run `serverless` to verify that everything is working properly.

We can now install our dependencies: [`jsonwebtoken`](https://npm.im/jsonwebtoken), [`bcrypt`](https://npm.im/bcrypt) and the [`cookie`](https://npm.im/cookie) package.

```bash
npm install jsonwebtoken bcrypt cookie
```

## serverless.yml

Briefly, the `serverless.yml` serves as a schema for our serverless application. It will configure our functions and create our users database in DynamoDB. We'll be configuring something like this:

![authentication](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0bwukwth41edz0nvkeds.jpg)

Looking to our basic schema, we will be needing three lambda functions:

- /signup and /login: handle `POST` requests with the user’s information from the login and sign up form.
- /profile: handles `GET` request of our user’s profile retrieved from the database.

Regarding our database, to keep it simple we’ll create a single-table DynamoDB database with a partition key (HASH key) of `userId` and a `sortKey` (RANGE) which we’ll be our user’s profile. We won't be using the `sortKey` now but it will help us access data in the future once we start scaling our application. 

```yml
service: users
frameworkVersion: "2"
provider:
 name: aws
 runtime: nodejs12.x
 environment:
   USERS_TABLE: { Ref: usersTable }
 iamRoleStatements:
   - Effect: Allow
     Action:
       - dynamodb:PutItem
       - dynamodb:GetItem
     Resource: { Fn::GetAtt: [usersTable, Arn] }
functions:
 signup:
   handler: handler.signup
   events:
     - http:
         path: signup
         method: post
         cors: true
 login:
   handler: handler.login
   events:
     - http:
         path: login
         method: post
         cors: true
 profile:
   handler: handler.profile
   events:
     - http:
         path: profile
         method: get
         cors: true
resources:
 Resources:
   usersTable:
     Type: AWS::DynamoDB::Table
     Properties:
       TableName: "users"
       AttributeDefinitions:
         - AttributeName: userId
           AttributeType: S
         - AttributeName: sortKey
           AttributeType: S
       KeySchema:
         - AttributeName: userId
           KeyType: HASH
         - AttributeName: sortKey
           KeyType: RANGE

plugins:
 - serverless-offline
```

If we run `serverless deploy` in our command line, serverless will deploy our functions and for now, it will create our database inside DynamoDB.

## Creating JWT and generating a Cookie 

Before we start building our functions, we need to ensure that our users are able to stay logged in while they browse through our application. To do so, we’ll create a [JSON Web Token](https://jwt.io/) containing our `userId` and we’ll store it inside a cookie in the users browser, these tokens can then be verified and decoded allowing the user to browse the private routes from our application.

**Important:** Don’t forget to add your JWT secret and store it safely inside your [`.env` file](https://www.serverless.com/framework/docs/environment-variables/). It is important that you save this key in a safe place since it is what keeps your application secure and prevents unauthenticated users from impersonating others and accessing your private routes.
Also note that in production cookies should have a `httpOnly` attribute to prevent [XSS](https://en.wikipedia.org/wiki/XSS), a `Secure` attribute to guarantee cookies are only transferred via HTTPS, and a `SameSite=Strict` attribute to prevent [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery). 

```js
const jwt = require("jsonwebtoken");
const cookie = require("cookie");

const DAY = 24 * 60 * 60; // 1 day in seconds

const { JWT_SECRET } = process.env;

module.exports.generateCookie = (userId, expireTimeInDays) => {
  const token = jwt.sign({ userId }, JWT_SECRET, {
    expiresIn: expireTimeInDays + "d",
  });

  return cookie.serialize("token", token, {
    maxAge: expireTimeInDays * DAYS,
    httpOnly: true,
  });
};

```

Also, we should never store our user’s password in plain text inside our database so we’ll use `bcrypt` to hash it. We'll use `bcrypt.hash` to hash our user's password before we store it in our database and later use `bcrypt.compare` to check if the password the user provided when logging in is matching the hash.

```js
const bcrypt = require("bcrypt");
const ITERATIONS = 12;

module.exports.hashPassword = async (password) => {
 const hash = await bcrypt.hash(password, ITERATIONS);
 return hash;
};

module.exports.matchPassword = async (password, hash) => {
 const match = await bcrypt.compare(password, hash);
 return match;
};

```
## Setting up our handler functions

Let’s start with the signup handler. We will first parse the request body containing our user’s data, hash the password calling the `hashPassword` function and add the user’s inputs and hashed password inside DynamoDB.

Once that’s done, we’ll create a cookie that will be sent inside the response headers to the browser. 

Also, we'll be using the user's email as our `userId`. 

```js
module.exports.signup = async (event) => {
  const { name, email, password } = JSON.parse(event.body);

  if (name && email && password) {
    const userId = email;
    const hash = await hashPassword(password);

    await dynamoDb
      .put({
        TableName: "users",
        Item: {
          userId: email,
          sortKey: "profile",
          name: name,
          password: hash,
        },
      })
      .promise();

    const cookie = generateCookie(userId, 1);

    return {
      statusCode: 200,
      headers: {
        "Set-Cookie": cookie,
      },
      body: JSON.stringify({ success: true }),
    };
  } else {
    return {
      statusCode: 401,
      body: JSON.stringify({
        success: false,
        error: "Enter a valid name/email/password",
      }),
    };
  }
};

```

Our login handler will check if the user's email exists in the database. We then check if the user’s password matches the hashed password in the database and create and send a cookie inside the response headers.

```js
module.exports.login = async (event) => {
  const { email, password } = JSON.parse(event.body);

  if (email && password) {
    const { Item } = await dynamoDb
      .get({
        TableName: "users",
        Key: { userId: email, sortKey: "profile" },
      })
      .promise();

    if (!Item) {
      return {
        statusCode: 404,
        body: JSON.stringify({ success: false, err: "user not found" }),
      };
    }

    const { userId, password: hashedPassword } = Item;
    const matchedPassword = await matchPassword(password, hashedPassword);

    if (matchedPassword) {
      const cookie = generateCookie(userId, 1);
      return {
        statusCode: 200,
        headers: {
          "Set-Cookie": cookie,
        },
        body: JSON.stringify({ success: true }),
      };
    } else {
      return {
        statusCode: 401,
        body: JSON.stringify({ success: false, err: "incorrect password" }),
      };
    }
  }
};
```

Finally the profile handler, which is a protected route, gets the cookie from the headers, we parse it with the help of the `cookie` package and verify if the JSON Web Token is still valid and hasn't expired, if so, the user is authenticated.

```js
module.exports.verifyCookie = (cookieHeader) => {
  const { token } = cookie.parse(cookieHeader);
  return jwt.verify(token, JWT_SECRET);
};

```

If the `verifyCookie` function is successful, it will return the `userId` from the token and we can use that to get the user's data from the database.

```js
module.exports.profile = async (event) => {
  const cookieHeader = event.headers.Cookie;
  try {
    const decoded = verifyCookie(cookieHeader);
    const data = await dynamoDb
      .get({
        TableName: "users",
        Key: { userId: decoded.userId, sortKey: "profile" },
      })
      .promise();
    return {
      statusCode: 200,
      body: JSON.stringify({ success: true, name: data.Item.name }),
    };
  } catch (error) {
    return {
      statusCode: 401,
      body: JSON.stringify({ success: false, err: "not authorized" }),
    };
  }
};

```
## Conclusion

That's it for now! I hope you found this post useful and it gave you an idea of how authentication in a serverless environment could work.

Here's also a link to the Github [repo](https://github.com/lauracarballo/serverless-authentication)   

Please let me know your thoughts in the comments below. 

Get to know me better! 
[Twitter](https://twitter.com/lcarb14) [Github](https://github.com/lauracarballo) [Website](http://lauracarballo.com/)