loading...
Cover image for Building Serverless CRUD services in Go with DynamoDB - Part 6 (Bonus)

Building Serverless CRUD services in Go with DynamoDB - Part 6 (Bonus)

wingkwong profile image Wing-Kam WONG ・5 min read

Welcome to the part 6. This is the last part of this series. In this post, we will create loginHandler.go.

Getting started

First, let's add the config under functions in serverless.yml

login:
  handler: bin/handlers/loginHandler
  package:
    include:
      - ./bin/handlers/loginHandler
  events:
    - http:
        path: iam/login
        method: post
        cors: true
Enter fullscreen mode Exit fullscreen mode

Create a file loginHandler.go under src/handlers

Similarly, we have the below structure.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "os"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"

    utils "../utils"
)

type Credentials struct {
    // TODO1 
}

type User struct {
    ID            string `json:"id,omitempty"`
    UserName      string `json:"user_name,omitempty"`
    FirstName     string `json:"first_name,omitempty"`
    LastName      string `json:"last_name,omitempty"`
    Age           int    `json:"age,omitempty"`
    Phone         string `json:"phone,omitempty"`
    Password      string `json:"password,omitempty"`
    Email         string `json:"email,omitempty"`
    Role          string `json:"role,omitempty"`
    IsActive      bool   `json:"is_active,omitempty"`
    CreatedAt     string `json:"created_at,omitempty"`
    ModifiedAt    string `json:"modified_at,omitempty"`
    DeactivatedAt string `json:"deactivated_at,omitempty"`
}

type Response struct {
    Response User `json:"response"`
}

var svc *dynamodb.DynamoDB

func init() {
    region := os.Getenv("AWS_REGION")
    // Initialize a session
    if session, err := session.NewSession(&aws.Config{
        Region: &region,
    }); err != nil {
        fmt.Println(fmt.Sprintf("Failed to initialize a session to AWS: %s", err.Error()))
    } else {
        // Create DynamoDB client
        svc = dynamodb.New(session)
    }
}

func Login(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    var (
        tableName = aws.String(os.Getenv("IAM_TABLE_NAME"))
    )

    //TODO2
}

func main() {
    lambda.Start(Login)
}

Enter fullscreen mode Exit fullscreen mode

Basically we are going to make an API with POST method. Users are expected to pass their credentials to iam/login to authorise their identities.

In this tutorials, we only send username and password. You may change username to email if you want. Let's update the below code and remove the comment // TODO1.

type Credentials struct {
    UserName string `json:"user_name"`
    Password string `json:"password"`
}
Enter fullscreen mode Exit fullscreen mode

As we only need to return a single object, we can use same response struct used in getHandler.go


type Response struct {
    Response User `json:"response"`
}
Enter fullscreen mode Exit fullscreen mode

The next step is to write our logic under //TODO2. The general idea is that users send their credentials which are further used to check the data in Amazon DynamoDB. It then returns the user object if it matches.

First, we need to initialise Credentials to hold our users input.

creds := &Credentials{}
Enter fullscreen mode Exit fullscreen mode

Like what we did previously, parse the request body to creds

json.Unmarshal([]byte(request.Body), creds)
Enter fullscreen mode Exit fullscreen mode

The next step is to utilise Query API operation for Amazon DynamoDB. In this tutorial, it finds items based on primary key values. You can also query any table or secondary index which has a composite primary key.

Query takes QueryInput. It should includes TableName, IndexName, KeyConditions.

TableName is a required field which tells the client service which table you want to perform Query.

IndexName is the name of an index to query. It can be local secondary index or global secondary index on the table.

KeyConditions includes Condition which is used when querying a table or an index with comparison operators such as EQ | LE | LT | GE | GT | BEGINS_WITH | BETWEEN. It can also apply QueryFilter.

result, err := svc.Query(&dynamodb.QueryInput{
    TableName: tableName,
    IndexName: aws.String("IAM_GSI"),
    KeyConditions: map[string]*dynamodb.Condition{
        "user_name": {
            ComparisonOperator: aws.String("EQ"),
            AttributeValueList: []*dynamodb.AttributeValue{
                {
                    S: aws.String(creds.UserName),
                },
            },
        },
    },
})
Enter fullscreen mode Exit fullscreen mode

Like other handler, we retrieve the value of IAM_TABLE_NAME in our configuration file and set it to tableName.

tableName = aws.String(os.Getenv("IAM_TABLE_NAME"))
Enter fullscreen mode Exit fullscreen mode

We select IAM_GSI to query, which is also defined in serverless.yml in part 1.

GlobalSecondaryIndexes:
  - IndexName: IAM_GSI
    KeySchema:
      - AttributeName: user_name
        KeyType: HASH
    Projection:
      ProjectionType: ALL
    ProvisionedThroughput:
      ReadCapacityUnits: 5
      WriteCapacityUnits: 5
Enter fullscreen mode Exit fullscreen mode

We then define a dynamodb.Condition struct holding our condition. As you can see, we only have one condition which is to check if user_name and creds.UserName are equal (EQ).

Check if there is an error

if err != nil {
    fmt.Println("Got error calling Query:")
    fmt.Println(err.Error())
    // Status Internal Server Error
    return events.APIGatewayProxyResponse{
        Body:       err.Error(),
        StatusCode: 500,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

If there is no error, we can see a User object in result.Items. However, if there is no item returned from Amazon DynamoDB, we can return an empty object in response.

user := User{}

if len(result.Items) == 0 {
    body, _ := json.Marshal(&Response{
        Response: user,
    })

    // Status OK
    return events.APIGatewayProxyResponse{
        Body:       string(body),
        StatusCode: 200,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

The response should look like this

{
  "response": {}
}
Enter fullscreen mode Exit fullscreen mode

If there is a record found, we can pass it to user.

if err := dynamodbattribute.UnmarshalMap(result.Items[0], &user); err != nil {
    fmt.Println("Got error unmarshalling:")
    fmt.Println(err.Error())
    return events.APIGatewayProxyResponse{
        Body:       err.Error(),
        StatusCode: 500,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

Now we only check if the password in user input matches with the one in the record.

Remember we've created utils/password.go in Part 1? We've only created HashPassword. We use this function to hash the password. In order to compare a bcrypt hashed password with its possible plaintext equivalent, we need another function here.

func CheckPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}
Enter fullscreen mode Exit fullscreen mode

It's simple. We also use bcrypt to perform checking by using CompareHashAndPassword.

Back to loginHandler.go

match := utils.CheckPasswordHash(creds.Password, user.Password)
Enter fullscreen mode Exit fullscreen mode

If it matches, then we can return user to Response.

if match {
    body, _ := json.Marshal(&Response{
        Response: user,
    })
    // Status OK
    return events.APIGatewayProxyResponse{
        Body:       string(body),
        StatusCode: 200,
    }, nil
}
Enter fullscreen mode Exit fullscreen mode

If not, we just return an empty user

body, _ := json.Marshal(&Response{
    Response: User{},
})

// Status Unauthorized
return events.APIGatewayProxyResponse{
    Body:       string(body),
    StatusCode: 401,
}, nil
Enter fullscreen mode Exit fullscreen mode

Run the below command to deploy our code

./scripts/deploy.sh
Enter fullscreen mode Exit fullscreen mode

Testing

If you go to AWS Lambda Console, you will see there is a function called serverless-iam-dynamodb-dev-login

image

Go to API Gateway Console to test it,

{
    "user_name": "wingkwong",
    "password": "password"
}
Enter fullscreen mode Exit fullscreen mode

You should see the corresponding data.

{
  "response": {
    "id": "6405bc74-a706-4987-86a9-82cf69d386c2",
    "user_name": "wingkwong",
    "password": "$2a$15$abtf69CeWZwGPJxIS/D/teXV26kBfY3SmHFNSNTbhP8gNa1OUeoiy",
    "email": "wingkwong.me@gmail.com",
    "role": "user",
    "is_active": true,
    "created_at": "2020-03-07 07:29:23.336349405 +0000 UTC m=+0.087254950",
    "modified_at": "2020-03-07 07:30:47.531266176 +0000 UTC m=+0.088812866"
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's try a wrong password

{
    "user_name": "wingkwong",
    "password": "password2"
}
Enter fullscreen mode Exit fullscreen mode

You should see an empty object

{
  "response": {}
}
Enter fullscreen mode Exit fullscreen mode

Cleanup

As mentioned in Part 1, serverless provisions / updates a single CloudFormation stack every time we deploy. To cleanup, we just need to delete the stack.

image

Click Delete stack

image

You should see the status is now DELETE_IN_PROGRESS.

image

Once it's done, you should see the stack has been deleted.

image

Source Code

GitHub logo go-serverless / serverless-iam-dynamodb

Building serverless CRUD services in Go with DynamoDB

serverless-iam-dynamodb

Building serverless CRUD services in Go with DynamoDB

Project structure

/.serverless

It will be created automatically when running serverless deploy in where deployment zip files, cloudformation stack files will be generated

/bin

This is the folder where our built Go codes are placed

/scripts

General scripts for building Go codes and deployment

/src/handlers

All Lambda handlers will be placed here

/src/utils

General functions go here

Prerequisites

You need to install serverless cli

npm install -g serverless

You need to install aws cli

pip install awscli

and setup your aws credentials

aws configure

Of course you need to install Go

Getting started

By default, a custom authorizer has been enabled for create, list, update, delete and get. Please replace <YOUR_JWT_SECRET_KEY> with your JWT Secret Key in serverless.yml (Line 35).

Building the code

This script compiles functions to bin/handlers/.

./scripts/build.sh

Deploying to AWS

This script includes…

Discussion

pic
Editor guide