Implementing Key and Token Authentication for a NodeJS API on AWS SAM
Tags: node, aws, serverless, awssam
Published: 2023-11-05 23:03:51

***
In this article, we will look at how to create a securely authenticated serverless NodeJs API leveraging on AWS API Gateway key and custom JWT token. AWS SAM (Serverless Application Model) creates secure, high-performing APIs and provides developers with a simplified development environment that allows them to solely focus on writing code without worrying about server management, infrastructure scaling, or hardware failures.

## Designs

**High-Level Architecture Diagram**
- The API calls from a mobile or web client are routed through an API Gateway to a Lambda function for processing. Data is persisted in a DynamoDb and logs stored on CloudWatch.

![High-Level architecture diagram for a NodeJS API deployed on AWS SAM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l6npax4oy49p0ecnzcl2.png)

**Sequence Diagram**
- The user first creates an account and then login using their credentials i.e. username and a password to get an access token. Making use of the access token, the user is then able to create a new event, update details and view all the events available.

![Sequence diagram for a NodeJS API deployed on AWS SAM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/94phbewcvfkd605up7cv.png)

**Prerequisite**

- Basic skill in NodeJs
- AWS Account
- AWS CLI

Follow this guide ([AWS CLI](https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-install.html)) to install.

```
$ aws --version
aws-cli/2.7.32 Python/3.9.11 Darwin/23.0.0 exe/x86_64 prompt/off
```
- SAM CLI

Follow this guide ([SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html)) to install.

```
$ sam --version
SAM CLI, version 1.73.0
```

**Source**
You can get the full source code here.

```
$ git clone https://github.com/bensonmacharia/sam-node-api.git
```

## Let's Build
**1. Environment Setup**

- Create the project folder and initialise the sam project.

```
$ mkdir sam-projects  && cd sam-projects
$ sam init -r nodejs18.x -d npm -n sam-node-api
Which template source would you like to use?
Choice: 1
Choose an AWS Quick Start application template
Template: 6
Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: N
Would you like to enable monitoring using CloudWatch Application Insights? [y/N]: N
```
- We are specifying to use nodejs version 18, npm as the dependency manager for our Lambda runtime and sam-node-api as the name of our project. We have also chosen not to enable X-Ray tracing and CloudWatch Application Insights.

**2. Let's do some clean up**
- Open the generated source code in your favourite IDE. In this case, vscode.

```
$ cd sam-node-api && code .
```

- Delete the files that we will not be using.

```
$ rm -rf sam-node-dev events && rm -rf __tests__
```

**3. Time now to edit the template.yaml file**
- In AWS SAM, `template.yaml` serves as the foundational configuration file for defining our serverless application. Clear the content on the template.yaml file and add the following below content.

- `AWSTemplateFormatVersion`: Specifies the AWS CloudFormation template version. For SAM templates, this is usually set to '2010-09-09'.
```
AWSTemplateFormatVersion: 2010-09-09
```

- `Transform`: Specifies the macro (transform) that AWS CloudFormation uses to process the template. For SAM templates, this should be set to 'AWS::Serverless-2016-10-31'.
```
- AWS::Serverless-2016-10-31
```

- `Description`: Provides a description of the CloudFormation stack and the serverless application.
```
Description: >-
  sam-node-api
```

- `Resources`: This section defines the AWS resources that make up your serverless application, such as AWS Lambda functions, API Gateway APIs, DynamoDB tables, and more. In this case we have an API Gateway, Secrets Manager, Lambda functions and DynamoDB as below.

***API Gateway***
```
Resources:
  # Create an API Gateway and add an API Key
  ApiGatewayEndpoint:
    Type: 'AWS::Serverless::Api'
    Properties:
      StageName: Prod
      Auth:
        # Require API Key for all endpoints
        ApiKeyRequired: true
        UsagePlan:
          CreateUsagePlan: PER_API
          UsagePlanName: GatewayAuthorization
```

***Secret Manager***
```
JWTUserTokenSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: JWTUserTokenSecret
      Description: "This secret has a dynamically generated secret password."
      # Generate a random string 30 charatcers long for the JWT secret
      GenerateSecretString:
        GenerateStringKey: 'jwt_secret'
        PasswordLength: 30
        ExcludeCharacters: '"@/\:;+*'''
        SecretStringTemplate: '{"secret_name": "sam-node-app-jwt-secret"}'
```

***Get All Events Lambda Function***
```
# This is a Lambda function config associated with the source code: get-all-events.js for getting all the Events.
  getAlleventsFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/get-all-events.getAllEventsHandler
      Runtime: nodejs18.x
      Architectures:
        - x86_64
      MemorySize: 128
      Timeout: 100
      Description: Includes a HTTP get method to get all events from a DynamoDB table.
      Policies:
        # Give Create/Read/Update/Delete Permissions to the EventsTable
        - DynamoDBCrudPolicy:
            TableName: !Ref EventsTable
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !Ref JWTUserTokenSecret
      Environment:
        Variables:
          # Make table name accessible as environment variable from function code during execution
          SAMPLE_TABLE: !Ref EventsTable
      Events:
        Api:
          Type: Api
          Properties:
            RestApiId:
              Ref: ApiGatewayEndpoint
            # Expose the API through the path /v1/api/events
            Path: /v1/api/events
            Method: GET
```

***Get Single Event By ID Lambda Function***
```
# This is a Lambda function config associated with the source code: get-event.js for getting a single Event by its ID
  getEventByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/get-event.getEventByIdHandler
      Runtime: nodejs18.x
      Architectures:
        - x86_64
      MemorySize: 128
      Timeout: 100
      Description: Includes a HTTP get method to get one event by id from a DynamoDB table.
      Policies:
        # Give Create/Read/Update/Delete Permissions to the EventsTable
        - DynamoDBCrudPolicy:
            TableName: !Ref EventsTable
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !Ref JWTUserTokenSecret
      Environment:
        Variables:
          # Make table name accessible as environment variable from function code during execution
          SAMPLE_TABLE: !Ref EventsTable
      Events:
        Api:
          Type: Api
          Properties:
            RestApiId:
              Ref: ApiGatewayEndpoint
            # Expose the API through the path /v1/api/event/<id>
            Path: /v1/api/event/{id}
            Method: GET
```

***Create an Event Lambda Function***
```
# This is a Lambda function config associated with the source code: put-item.js for posting an Event into the database
  addEventFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/add-event.addEventHandler
      Runtime: nodejs18.x
      Architectures:
        - x86_64
      MemorySize: 128
      Timeout: 100
      Description: Includes a HTTP post method to add one event to a DynamoDB table.
      Policies:
        # Give Create/Read/Update/Delete Permissions to the EventsTable
        - DynamoDBCrudPolicy:
            TableName: !Ref EventsTable
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !Ref JWTUserTokenSecret
      Environment:
        Variables:
          # Make table name accessible as environment variable from function code during execution
          SAMPLE_TABLE: !Ref EventsTable
      Events:
        Api:
          Type: Api
          Properties:
            RestApiId:
              Ref: ApiGatewayEndpoint
            # Expose the API through the path /v1/api/event
            Path: /v1/api/event
            Method: POST
```

***User Registration Lambda Function***
```
# This is a Lambda function config associated with the source code: user-register.js for adding a new user record into the database
  registerUserFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/user-register.registerUserHandler
      Runtime: nodejs18.x
      Architectures:
        - x86_64
      MemorySize: 128
      Timeout: 100
      Description: A HTTP post method to register a user and add record to a DynamoDB table.
      Policies:
        # Give Create/Read/Update/Delete Permissions to the UsersTable
        - DynamoDBCrudPolicy:
            TableName: !Ref UsersTable
      Environment:
        Variables:
          # Make table name accessible as environment variable from function code during execution
          USER_TABLE: !Ref UsersTable
      Events:
        Api:
          Type: Api
          Properties:
            RestApiId:
              Ref: ApiGatewayEndpoint
            # Expose the API through the path /v1/auth/user/register
            Path: /v1/auth/user/register
            Method: POST
            Auth:
              ApiKeyRequired: false
```

***User Login Lambda Function***
```
# This is a Lambda function config associated with the source code: user-login.js for fetching a user record from the database
  loginUserFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/user-login.loginUserHandler
      Runtime: nodejs18.x
      Architectures:
        - x86_64
      MemorySize: 128
      Timeout: 100
      Description: A HTTP post method to login a user and fetch record from a DynamoDB table.
      Policies:
        # Give Create/Read/Update/Delete Permissions to the UsersTable
        - DynamoDBCrudPolicy:
            TableName: !Ref UsersTable
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !Ref JWTUserTokenSecret
      Environment:
        Variables:
          # Make table name accessible as environment variable from function code during execution
          USER_TABLE: !Ref UsersTable
      Events:
        Api:
          Type: Api
          Properties:
            RestApiId:
              Ref: ApiGatewayEndpoint
            # Expose the API through the path /v1/auth/user/login
            Path: /v1/auth/user/login
            Method: POST
            Auth:
              ApiKeyRequired: false
```

***Events DynamoDB Table***
```
# DynamoDB table to store Event details
  EventsTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 2
        WriteCapacityUnits: 2
```

***Users DynamoDB Table***
```
 # DynamoDB table to store User details
  UsersTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: username
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 2
        WriteCapacityUnits: 2
```

- `Outputs`: Specifies the values to be exported from the stack once it's created. Outputs can include endpoint URLs, resource IDs, or any other information you want to make accessible after the deployment.
```
Outputs:
  ApiGateway:
    Description: "The URL is:"
    Value: !Sub "https://${ApiGatewayEndpoint}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
  ApiKey:
    Description: "You can find your API Key in the AWS console: (Put in the request HEADER as 'x-api-key')"
    Value: !Sub "https://console.aws.amazon.com/apigateway/home?region=${AWS::Region}#/api-keys/${ApiGatewayEndpointApiKey}"
```

**4. Edit the Environment Variables**
- Modify the `env.json` file to define our variables for the database names as below.
```
{
    "getAllEventsFunction": {
        "SAMPLE_TABLE": "EventsTable"
    },
    "getEventByIdFunction": {
        "SAMPLE_TABLE": "EventsTable"
    },
    "addEventFunction": {
        "SAMPLE_TABLE": "EventsTable"
    },
    "registerUserFunction": {
        "USER_TABLE": "UsersTable"
    },
    "loginUserFunction": {
        "USER_TABLE": "UsersTable"
    }
  }
```

**5. Install Dependencies**
```
# Install API Gateway SDK
$ npm install @aws-sdk/client-api-gateway

# Install Secrets Manager SDK
$ npm install @aws-sdk/client-secrets-manager 

# Install jsonwebtoken package for generating JWT tokens
$ npm install jsonwebtoken

# Install bcryptjs package for password encryption
$ npm install bcryptjs

# Install uuid package for generating uuidv4 ids
$ npm install uuid
```

**6. Let's Handle User Registration**
- Create a new file `user-register.mjs` under `sam-node-api/src/handlers`
```
$ touch src/handlers/user-register.mjs 
```

- Add the user registration logic on `user-register.mjs`
```
// Import bcrypt for encrypting user password
import bcrypt from 'bcryptjs';
// Import uuid for generating unique user ID
import { v4 as uuidv4 } from 'uuid';

// Create a DocumentClient that represents the query to add an item
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient({});
const ddbDocClient = DynamoDBDocumentClient.from(client);

// Get the DynamoDB table name from environment variables
const tableName = process.env.USER_TABLE;

/**
 * A HTTP post method to add one user to a DynamoDB table.
 */
export const registerUserHandler = async (event) => {
    if (event.httpMethod !== 'POST') {
        throw new Error(`postMethod only accepts POST method, you tried: ${event.httpMethod} method.`);
    }
    // All log statements are written to CloudWatch
    console.info('received:', event);

    // Get username and password from the body of the request
    const body = JSON.parse(event.body);
    // Use a random uuidv4 string as a User ID 
    const id = uuidv4();

    const username = body.username;
    
    // Generate a hashed password string
    const password = bcrypt.hashSync(body.password, 8);

    // Creates a new user, or replaces an old user record with a new one
    var params = {
        TableName: tableName,
        Item: { id: id, username: username, password: password }
    };

    try {
        const data = await ddbDocClient.send(new PutCommand(params));
        console.log("Success - user registered or updated", data);
    } catch (err) {
        console.log("Error", err.stack);
    }

    const responseData = {
        message: "Registration successful. Proceed to login",
        username: body.username
    
    }

    const response = {
        statusCode: 201,
        body: JSON.stringify(responseData)
    };

    // All log statements are written to CloudWatch
    console.info(`response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`);
    return response;
};

```

**7. Handle User Login**
- Create a new file `user-login.mjs` under `sam-node-api/src/handlers`
```
$ touch src/handlers/user-login.mjs 
```

- Add the user login logic on `user-login.mjs`
```
// Import bcrypt for encrypting user password and comparing the password hash
import bcrypt from 'bcryptjs';
// Create a DocumentClient that represents the query to add an item
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
// Import jwt for generating the user token
import jwt from 'jsonwebtoken';
const client = new DynamoDBClient({});
const ddbDocClient = DynamoDBDocumentClient.from(client);
const clientsecret = new SecretsManagerClient();

// Get the DynamoDB table name from environment variables
const tableName = process.env.USER_TABLE;

/**
 * A HTTP post method for user login.
 */
export const loginUserHandler = async (event) => {
    if (event.httpMethod !== 'POST') {
        throw new Error(`postMethod only accepts POST method, you tried: ${event.httpMethod} method.`);
    }
    // All log statements are written to CloudWatch
    console.info('received:', event);

    // Get username and password from the body of the request
    const body = JSON.parse(event.body);
    const username = body.username;
    const password = body.password;

    // Fetch the JWT secret string from AWS Secrets Manager 
    const secret_value = await clientsecret.send(new GetSecretValueCommand({
        SecretId: "JWTUserTokenSecret",
    }));
    const jwt_secret = JSON.parse(secret_value.SecretString);

    var params = {
        TableName: tableName,
        Key: { username: username },
    };

    var token = "";
    var message = "";
    var status_code = 200;

    try {
        const data = await ddbDocClient.send(new GetCommand(params));
        var item = data.Item;
        // Comprate a hash of the received password from the request body with the password hash from the database, if they match login the user and generate a JWT token
        if (bcrypt.compareSync(password, item.password)) {
            // create JWT token
            const jwttoken = jwt.sign(
                {
                    userId: item.id,
                    userName: body.username,
                },
                jwt_secret.jwt_secret,
                { expiresIn: "1h" }
            );
            message = "Login successful.";
            token = jwttoken;
        } else {
            status_code = 403;
            message = "Wrong password. Try again";
            token = "NOT OKAY";
        }
    } catch (err) {
        console.log("Error", err);
    }

    const responseData = {
        message: message,
        user: {
            username: body.username,
            token: token
        }
    }

    const response = {
        statusCode: status_code,
        body: JSON.stringify(responseData)
    };

    // All log statements are written to CloudWatch
    console.info(`response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`);
    return response;
};
```

**8. Add Events Logic**
- Rename the file `put-item.mjs` in `sam-node-api/src/handlers` to `add-event.mjs`
```
$ mv src/handlers/put-item.mjs src/handlers/add-event.mjs
```

- Edit the `add-event.mjs` file
```
// Create a DocumentClient that represents the query to add an item
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb';
import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
// Import jwt for validating the user token
import jwt from 'jsonwebtoken';

const client = new DynamoDBClient({});
const ddbDocClient = DynamoDBDocumentClient.from(client);
const clientsecret = new SecretsManagerClient();

// Get the DynamoDB table name from environment variables
const tableName = process.env.SAMPLE_TABLE;

/**
 * A HTTP post method to add one Event item to a DynamoDB table.
 */
export const addEventHandler = async (event) => {
    if (event.httpMethod !== 'POST') {
        throw new Error(`postMethod only accepts POST method, you tried: ${event.httpMethod} method.`);
    }
    // All log statements are written to CloudWatch
    console.info('received:', event);

    // Get token from the authorization header
    const token = await event.headers.Authorization.split(" ")[1];
    if (!token) {
        throw new Error(`Authorization token required.`);
    }

    // Get Event id, title and description from the body of the request
    const body = JSON.parse(event.body);
    const id = body.id;
    const title = body.title;
    const description = body.description;

    // Fetch the JWT secret string from AWS Secrets Manager 
    const secret_value = await clientsecret.send(new GetSecretValueCommand({
        SecretId: "JWTUserTokenSecret",
    }));
    const jwt_secret = JSON.parse(secret_value.SecretString);

    //Check if the token is valid
    const decodedToken = jwt.verify(token, jwt_secret.jwt_secret);
    if (!decodedToken) {
        throw new Error(`Authorization token invalid or it has expired.`);
    }

    // Decode the JWT token to get user data
    const userID = decodedToken.userId;
    const userName = decodedToken.userName;

     console.info('decode-username:', userName);
     console.info('decode-userid:', userID);

    // Creates a new Event item, or replaces an old item with a new item
    var params = {
        TableName : tableName,
        Item: { id : id, title: title, description: description, added_by:  userName, added_by_id: userID}
    };

    try {
        const data = await ddbDocClient.send(new PutCommand(params));
        console.log("Success - event added successfully", data);
      } catch (err) {
        console.log("Error", err.stack);
      }

    const response = {
        statusCode: 200,
        body: JSON.stringify(body)
    };

    // All log statements are written to CloudWatch
    console.info(`response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`);
    return response;
};
```

**9. View All Events Login**
- Rename the file `get-all-items.mjs` in `sam-node-api/src/handlers` to `get-all-events.mjs`
```
$ mv src/handlers/get-all-items.mjs src/handlers/get-all-events.mjs
```

- Edit the `get-all-events.mjs` file
```
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
// Import jwt for validating the user token
import jwt from 'jsonwebtoken';

const client = new DynamoDBClient({});
const ddbDocClient = DynamoDBDocumentClient.from(client);
const clientsecret = new SecretsManagerClient();

// Get the DynamoDB table name from environment variables
const tableName = process.env.SAMPLE_TABLE;

/**
 * A HTTP get method to get all events from a DynamoDB table.
 */
export const getAllEventsHandler = async (event) => {
    if (event.httpMethod !== 'GET') {
        throw new Error(`getAllItems only accept GET method, you tried: ${event.httpMethod}`);
    }
    // All log statements are written to CloudWatch
    console.info('received:', event);

    // Get token from the authorization header
    const token = await event.headers.Authorization.split(" ")[1];
    if (!token) {
        throw new Error(`Authorization token required.`);
    }

    // Fetch the JWT secret string from AWS Secrets Manager 
    const secret_value = await clientsecret.send(new GetSecretValueCommand({
        SecretId: "JWTUserTokenSecret",
    }));
    const jwt_secret = JSON.parse(secret_value.SecretString);

    //Check if the token is valid
    const decodedToken = jwt.verify(token, jwt_secret.jwt_secret);
    if (!decodedToken) {
        throw new Error(`Authorization token invalid or it has expired.`);
    }

    // Get all items from the table (only first 1MB data, you can use `LastEvaluatedKey` to get the rest of data)
    var params = {
        TableName : tableName
    };

    try {
        const data = await ddbDocClient.send(new ScanCommand(params));
        var items = data.Items;
    } catch (err) {
        console.log("Error", err);
    }

    const response = {
        statusCode: 200,
        body: JSON.stringify(items)
    };

    // All log statements are written to CloudWatch
    console.info(`response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`);
    return response;
}
```

**10. View Event By ID Login**
- Rename the file `get-by-id.mjs` in `sam-node-api/src/handlers` to `get-event.mjs`
```
$ mv src/handlers/get-by-id.mjs src/handlers/get-event.mjs
```

- Edit the `get-event.mjs` file
```
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
// Import jwt for validating the user token
import jwt from 'jsonwebtoken';

const client = new DynamoDBClient({});
const ddbDocClient = DynamoDBDocumentClient.from(client);
const clientsecret = new SecretsManagerClient();

// Get the DynamoDB table name from environment variables
const tableName = process.env.SAMPLE_TABLE;

/**
 * A HTTP get method to get one Event item by id from a DynamoDB table.
 */
export const getEventByIdHandler = async (event) => {
  if (event.httpMethod !== 'GET') {
    throw new Error(`getMethod only accept GET method, you tried: ${event.httpMethod}`);
  }
  // All log statements are written to CloudWatch
  console.info('received:', event);

  // Get the token from the authorization header
  const token = await event.headers.Authorization.split(" ")[1];
  if (!token) {
      throw new Error(`Authorization token required.`);
  }

  // Fetch the JWT secret string from AWS Secrets Manager 
  const secret_value = await clientsecret.send(new GetSecretValueCommand({
      SecretId: "JWTUserTokenSecret",
  }));
  const jwt_secret = JSON.parse(secret_value.SecretString);

  //Check if the token is valid
  const decodedToken = jwt.verify(token, jwt_secret.jwt_secret);
  if (!decodedToken) {
      throw new Error(`Authorization token invalid or it has expired.`);
  }
 
  // Get id from pathParameters from APIGateway because of `/{id}` at template.yaml
  const id = event.pathParameters.id;
 
  // Get the item from the table
  var params = {
    TableName : tableName,
    Key: { id: id },
  };

  var scode = 200;
  var bdy = "";

  try {
    const data = await ddbDocClient.send(new GetCommand(params));
    var item = data.Item;
    bdy = JSON.stringify(item);
    if (!bdy) {
      scode = 404;
      bdy = JSON.stringify("Event details not found.");
    }
  } catch (err) {
    console.log("Error", err);
    scode = 400;
    bdy = err;
  }
 
  const response = {
    statusCode: scode,
    body: bdy
  };
 
  // All log statements are written to CloudWatch
  console.info(`response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`);
  return response;
}
```

**11. Build and Deploy the API**
- Build the SAM App
```
$ sam build
Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml
```

- Deploy the SAM App
```
$ sam deploy --guided
Stack Name [sam-app]: sam-node-app
AWS Region [us-east-1]: us-east-1
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: Y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: N
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]: 
SAM configuration environment [default]: 

Deploy this changeset? [y/N]: y

CloudFormation outputs from deployed stack
---------------
Outputs                                                                                                                                                                                                           
---------------
Key                 ApiKey                                                                                                                                                                                        
Description         You can find your API Key in the AWS console: (Put in the request HEADER as 'x-api-key')                                                                                                      
Value               https://console.aws.amazon.com/apigateway/home?region=us-east-1#/api-keys/w22y9chd81                                                                                                          

Key                 ApiGateway                                                                                                                                                                                    
Description         The URL is:                                                                                                                                                                                   
Value               https://d2io9v58l4.execute-api.us-east-1.amazonaws.com/Prod/                                                                                                                                  
-----------------

Successfully created/updated stack - sam-node-app in us-east-1
```

**12. Test the API**
- Test user registration
```
$ curl -X POST --header "Content-Type: application/json" --data '{"username":"bmacharia", "password":"pAssW0rd@s3cr3t"}' https://d2io9v58l4.execute-api.us-east-1.amazonaws.com/Prod/v1/auth/user/register

HTTP/1.1 200 OK
{"message":"Registration successful. Proceed to login","username":"bmacharia"}%  
```

- Test user login
```
$ curl -X POST --header "Content-Type: application/json" --data '{"username":"bmacharia", "password":"pAssW0rd@s3cr3t"}' https://d2io9v58l4.execute-api.us-east-1.amazonaws.com/Prod/v1/auth/user/login

HTTP/1.1 200 OK
{"message":"Login successful.","user":{"username":"bmacharia","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI4NDMxMTU5Ny05YTlkLTQ2NDEtYjBjMS1mYTUzMGIzNTkwNzYiLCJ1c2VyTmFtZSI6ImJtYWNoYXJpYSIsImlhdCI6MTY5ODk2MjM2OSwiZXhwIjoxNjk4OTY1OTY5fQ.keoma2P6fdXnkrRklWX1vVoyaUXB_G06p6gvQLyOHHY"}}% 
```

- Test creating event
```
$ curl -X POST --header "Content-Type: application/json" --header "x-api-key: Laqoh0N6kCao2yw4SeDxCa9pwRdjxwra267VGjMH" --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI4NDMxMTU5Ny05YTlkLTQ2NDEtYjBjMS1mYTUzMGIzNTkwNzYiLCJ1c2VyTmFtZSI6ImJtYWNoYXJpYSIsImlhdCI6MTY5ODk2MjM2OSwiZXhwIjoxNjk4OTY1OTY5fQ.keoma2P6fdXnkrRklWX1vVoyaUXB_G06p6gvQLyOHHY" --data '{"id":"2", "title":"Cybersecurity Summit", "description":"Downtown Hotel, 31st December 2023"}' https://d2io9v58l4.execute-api.us-east-1.amazonaws.com/Prod/v1/api/event

HTTP/1.1 200 OK
{"id":"2","title":"Cybersecurity Summit","description":"Downtown Hotel, 31st December 2023"}%   
```

- Test getting all events
```
$ curl --header "Content-Type: application/json" --header "x-api-key: Laqoh0N6kCao2yw4SeDxCa9pwRdjxwra267VGjMH" --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI4NDMxMTU5Ny05YTlkLTQ2NDEtYjBjMS1mYTUzMGIzNTkwNzYiLCJ1c2VyTmFtZSI6ImJtYWNoYXJpYSIsImlhdCI6MTY5ODk2MjM2OSwiZXhwIjoxNjk4OTY1OTY5fQ.keoma2P6fdXnkrRklWX1vVoyaUXB_G06p6gvQLyOHHY" https://d2io9v58l4.execute-api.us-east-1.amazonaws.com/Prod/v1/api/events

HTTP/1.1 200 OK
[{"description":"Downtown Hotel, 31st December 2023","id":"2","title":"Cybersecurity Summit","added_by":"bmacharia","added_by_id":"84311597-9a9d-4641-b0c1-fa530b359076"},{"description":"Whitesands Hotel, 1st January 2024","id":"1","title":"GRC Conference","added_by":"bmacharia","added_by_id":"84311597-9a9d-4641-b0c1-fa530b359076"}]%    
```

- Get Event By ID
```
$ curl --header "Content-Type: application/json" --header "x-api-key: Laqoh0N6kCao2yw4SeDxCa9pwRdjxwra267VGjMH" --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI4NDMxMTU5Ny05YTlkLTQ2NDEtYjBjMS1mYTUzMGIzNTkwNzYiLCJ1c2VyTmFtZSI6ImJtYWNoYXJpYSIsImlhdCI6MTY5ODk2MjM2OSwiZXhwIjoxNjk4OTY1OTY5fQ.keoma2P6fdXnkrRklWX1vVoyaUXB_G06p6gvQLyOHHY" https://d2io9v58l4.execute-api.us-east-1.amazonaws.com/Prod/v1/api/event/1

HTTP/1.1 200 OK
{"description":"Whitesands Hotel, 1st January 2024","id":"1","title":"GRC Conference","added_by":"bmacharia","added_by_id":"84311597-9a9d-4641-b0c1-fa530b359076"}%     
```

**13. Clean Up**
- Delete the SAM App
```
$ sam delete --stack-name sam-node-app
Are you sure you want to delete the stack sam-node-app in the region us-east-1 ? [y/N]: y
Are you sure you want to delete the folder sam-node-app in S3 which contains the artifacts? [y/N]: y

Deleted successfully
```