Developing Complete Authorization Modules with AWS Lambda & Go & PostgreSQL & Event-Driven SQS Queue

Yunus Kılıç
Analytics Vidhya
Published in
13 min readOct 30, 2019

--

In this tutorial, I am going to share a sample project which contains Signup, Login, Authorization, Email verification functions. These functions can be used to start a new Serverless project. I will share code on GitHub at the end of the article so people can start with the existing code. Technology stack is below:

  • Golang
  • Gorm
  • AWS Lambda
  • AWS API Gateway
  • AWS RDS Postgresql
  • AWS SES
  • AWS SQS

You can develop this project with the AWS Free Tier. You do not need to pay anything to AWS.

Later on this tutorial, I will give some tips&tricks of each of these modules.

Let’s start by creating a project with AWS SAM.

sam init --runtime go1.x --name serverlessExample

I assumed that you have some knowledge about AWS SAM. If you need to look for some information about it, you can find my previous article below.

Sam creates initial function and template.yaml for us.

Let’s start with signup function

SIGNUP FUNCTION

I will collect all lambda functions inside the functions folder. So create below folder structure.

Project Structure

The signup function needs database connection to create a user. I used Amazon RDS Postgresql.

  • Create a database instance then make it publicly accessible to connect from outside AWS.

Database Connector code:

package database

import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
"os"
)

type PostgresConnector struct {
}

func (p *PostgresConnector) GetConnection() (db *gorm.DB, err error) {
username := os.Getenv("db_user")
password := os.Getenv("db_pass")
dbName := os.Getenv("db_name")
dbHost := os.Getenv("db_host")
dbURI := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable password=%s", dbHost, username, dbName, password)
fmt.Println(dbURI)
return gorm.Open("postgres", dbURI)
}

Connector read information from environment variables then opens a connection.

User entity code:

package entity

import "github.com/jinzhu/gorm"

type User struct {
gorm.Model
Email string
Password string `json:"-"`
EmailVerified bool
LoginTry int
}

To simplicity, my model is very raw. But you can easily extend this model.

`json:"-"` means do not show at json

Our gorm model and DB connector are ready so we can easily start to coding.

I created a model to parse the request body.

package main

type SignupRequest struct {
Email string `validate:"required,email"`
Password string `validate:"required"`
}

Signup function’s code

package main

import (
...
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
....
}
....func main() {
lambda.Start(handler)
}

Source code is very long, I want to describe some parts of these blocks.

The below part is used for unmarshalling request body to my signup request.

var signupRequest SignupRequest
jsonErr := json.Unmarshal([]byte(request.Body), &signupRequest)
if jsonErr != nil {
body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.JsonParseError)
return events.APIGatewayProxyResponse{
StatusCode: http.StatusBadRequest,
Body: body.ConvertToJson(),
}, nil
}

I created an error message file to keep all error messages inside a specific file.

package errormessage

const (
Ok = 99
DatabaseError = 100
JsonParseError = 101
UserAlreadyExist = 102
UserNameOrPasswordWrong = 103
CaptchaNeeded = 104
TokenIsNotValid = 105
)

var statusText = map[int]string{
DatabaseError: "DATABASE_ERROR",
JsonParseError: "JSON_PARSE_ERROR",
UserAlreadyExist: "USER_ALREADY_EXIST",
UserNameOrPasswordWrong: "USERNAME_OR_PASSWORD_WRONG",
Ok: "OK",
CaptchaNeeded: "CAPTCHA_NEEDED",
TokenIsNotValid: "TOKEN_IS_NOT_VALID",
}

func StatusText(code int) string {
return statusText[code]
}

After parsing request body to my model then I need to validate that required fields. I used an opensource validator for Golang. The following code block shows how to validate input.

v := validator.New()
validateErr := v.Struct(signupRequest)

if validateErr != nil {
body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.JsonParseError)
return events.APIGatewayProxyResponse{
StatusCode: http.StatusBadRequest,
Body: body.ConvertToJson(),
}, nil
}

Inside my signup request there is a statement

`validate:"required,email"`

which states that this field is required and its type is email.

After validating input, we need to check user exists if not create a new user.

postgresConnector := database.PostgresConnector{}
dbConn, dbErr := postgresConnector.GetConnection()
defer dbConn.Close()
if dbErr != nil {
fmt.Print(dbErr)
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: "",
}, nil
}
dbConn.AutoMigrate(&entity.User{})

var users []entity.User
filter := &entity.User{}
filter.Email = signupRequest.Email
dbConn.Where(filter).Find(&users)

if users != nil && len(users) > 0 {
body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.UserAlreadyExist)
return events.APIGatewayProxyResponse{
StatusCode: http.StatusBadRequest,
Body: body.ConvertToJson(),
}, nil
}

newUser := &entity.User{}
newUser.Email = signupRequest.Email
newUser.Password = hashAndSalt(signupRequest.Password)
dbConn.Create(&newUser)

For security reasons, you must keep password hashed. My hashing function:

func hashAndSalt(pwd string) (hashed string) {
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
log.Println(err)
}

return string(hash)
}

The coding part of the signup is ready. Let's move to the makefile and template.yaml

Makefile

.PHONY: clean build

clean:
rm -rf ./bin/signup/signupl

build:
GOOS=linux GOARCH=amd64 go build -o bin/signup/signup ./functions/signup

Template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
serverlessExample

Sample SAM Template for serverlessExample

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 5

Parameters:
dbname:
Type: String
Default: example
username:
Type: String
Default: postgres
password:
Type: String
Default: password
host:
Type: String
Default: localhost

Resources:
SignupFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: bin/signup
Handler: signup
Runtime: go1.x
Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html
Events:
Signup:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /signup
Method: POST
Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
Variables:
db_user: !Ref username
db_pass: !Ref password
db_name: !Ref dbname
db_host: !Ref host
Outputs:
ApiURL:
Description: "API URL"
Value: !Sub 'https://${ExampleApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'

Above template automatically creates a lambda function and its role with following commands.

$make clean build
$sam package --template-file template.yaml --s3-bucket YOURS3BUCKETNAME --output-template-file packaged.yaml
$aws cloudformation deploy --template-file PATH/packaged.yaml --stack-name serverlessexample --capabilities CAPABILITY_IAM --parameter-overrides dbname=AA username=BB password=CC host=DD

TIPS&TRICKS

An error occurred during deployment, you can see the details at CloudFormation events.

You can test at AWS Console or locally.

LOGIN FUNCTION

The login function will be more complicated than the signup function. Because of the authorization requirements, Also after some wrong entrance, our program requires captcha. For simplicity, the captcha is related to only user id at the moment. But you can improve this logic.

package main

type LoginRequest struct {
Email string `validate:"required,email"`
Password string `validate:"required"`
CaptchaId string
CaptchaResponse string
}
type LoginResponse struct {
AccessToken string
RefreshToken string
}

Login function also starts with parsing and validation as same as signup function. So I skip these parts. Then it checks username and password.

if user.LoginTry >= 5 && !validateCaptcha(loginRequest) {
body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.CaptchaNeeded)
response := events.APIGatewayProxyResponse{
StatusCode: http.StatusUnauthorized,
Body: body.ConvertToJson(),
}
return createApiLoginFailResponse(response, user, dbConn)
}

passwordErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginRequest.Password))

if passwordErr != nil {
body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.UserNameOrPasswordWrong)
response := events.APIGatewayProxyResponse{
StatusCode: http.StatusUnauthorized,
Body: body.ConvertToJson(),
}
return createApiLoginFailResponse(response, user, dbConn)
}

If unsuccessful login count is bigger than equal to five then, captcha needed error occurred. I will describe creating and validating captcha below.

func createApiLoginFailResponse(response events.APIGatewayProxyResponse, user entity.User, dbConn *gorm.DB) (events.APIGatewayProxyResponse, error) {
if user.ID > 0 {
user.LoginTry = user.LoginTry + 1
dbConn.Save(user)
if user.LoginTry >= 5 {
body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.CaptchaNeeded)
return events.APIGatewayProxyResponse{
StatusCode: http.StatusUnauthorized,
Body: body.ConvertToJson(),
}, nil
} else {
return response, nil
}
} else {
return response, nil
}
}

If username and password are correct than access and refresh token will be generated. github.com/dgrijalva/jwt-go used for jwt operations.

func CreateTokens(user entity.User) (model.TokenSet, error) {
accessTokenExpireAt := time.Now().Add(1 * time.Hour)
tokenStr, signErr := CreateToken(user, "Access", accessTokenExpireAt)

if signErr != nil {
return model.TokenSet{}, signErr
}

refreshTokenExpireAt := time.Now().Add(24 * time.Hour)
refreshTokenStr, signErr := CreateToken(user, "Refresh", refreshTokenExpireAt)

if signErr != nil {
return model.TokenSet{}, signErr
}
return model.TokenSet{AccessToken: tokenStr, ExpireAt: accessTokenExpireAt, RefreshToken: refreshTokenStr, RefreshExpireAt: refreshTokenExpireAt}, nil
}

func ValidateToken(token string) (*jwt.Token, error) {

return jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("jwt_key")), nil
})
}

func CreateToken(user entity.User, tokenType string, expireTime time.Time) (string, error) {
var claim model.CustomClaims
claim.Id = string(user.ID)
claim.Type = tokenType
expiresAt := expireTime
claim.ExpiresAt = expiresAt.Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
jwtKey := os.Getenv("jwt_key")
tokenStr, signErr := token.SignedString([]byte(jwtKey))
return tokenStr, signErr
}

Access token is valid for 1 hour. Refresh token is valid for 24 hours.

While signing jwt token, this library gives you a standart claim. But I need a field type to understand token’s type. So I wrote a custom token below.

package model

import "github.com/dgrijalva/jwt-go"

type CustomClaims struct {
jwt.StandardClaims
Type string
}

After successful login, API returns access and refresh tokens to the user.

Creating and Validation Captcha

github.com/dchest/captcha is used for captcha operations.

But our serverless function will be killed after execution. So you need to keep your captchas inside cache. This library supports custom stores but you need to spend some time to develop.

Custom Store code:

package common

import (
"fmt"
"github.com/go-redis/redis"
"log"
"os"
"time"
)

type CustomizeRdsStore struct {
RedisClient *redis.Client
ExpireAt time.Duration
}


func GetStore() *CustomizeRdsStore {
return NewStore(time.Duration(1 * time.Hour))
}

func NewStore(expireAt time.Duration) *CustomizeRdsStore {
client := redis.NewClient(&redis.Options{
Addr: os.Getenv("redis_url"),
Password: "", // no password set
DB: 0, // use default DB
})

c := new(CustomizeRdsStore)
c.RedisClient = client
c.ExpireAt = expireAt
return c
}

func (s CustomizeRdsStore) SetWithOverrideExpire(id string, value string, expireAt time.Duration) {
err := s.RedisClient.Set(id, value, expireAt).Err()
if err != nil {
log.Println(err)
}
}

func (s CustomizeRdsStore) SetWithoutExpire(id string, value string) {
err := s.RedisClient.Set(id, value, 0).Err()
if err != nil {
log.Println(err)
}
}

// customizeRdsStore implementing Set method of Store interface
func (s CustomizeRdsStore) Set(id string, value []byte) {
err := s.RedisClient.Set(id, string(value), s.ExpireAt).Err()
if err != nil {
log.Println(err)
}
}

// customizeRdsStore implementing Get method of Store interface
func (s CustomizeRdsStore) Get(id string, clear bool) (value []byte) {
val, err := s.RedisClient.Get(id).Result()
if err != nil {
log.Println(err)
return []byte{}
}
if clear {
err := s.RedisClient.Del(id).Err()
if err != nil {
log.Println(err)
return []byte{}
}
}
return []byte(val)
}

func (s CustomizeRdsStore) Set(id string, value []byte) and

func (s CustomizeRdsStore) Get(id string, clear bool)

is implemented for Store interface. Other functions are my helper funcs.

Create Captcha code:

package main

import (
"bytes"
"encoding/base64"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/dchest/captcha"
"github.com/yunuskilicdev/serverlessNear/common"
"github.com/yunuskilicdev/serverlessNear/common/errormessage"
"github.com/yunuskilicdev/serverlessNear/common/model"
"net/http"
"time"
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

store := common.NewStore(time.Duration(5 * time.Minute))
captcha.SetCustomStore(store)

captchaResponse := model.CaptchaResponse{}
captchaId := captcha.New()

var ImageBuffer bytes.Buffer
captcha.WriteImage(&ImageBuffer, captchaId, 300, 90)

captchaResponse.Id = captchaId
captchaResponse.Image = base64.StdEncoding.EncodeToString(ImageBuffer.Bytes())

body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.Ok)
body.ResponseObject = captchaResponse
return events.APIGatewayProxyResponse{
Body: body.ConvertToJson(),
StatusCode: http.StatusOK,
}, nil
}

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

Captcha validation will be operated inside login function.

func validateCaptcha(request LoginRequest) bool {
if request.CaptchaId == "" || request.CaptchaResponse == "" {
return false
}
store := common.GetStore()
captcha.SetCustomStore(store)
return captcha.VerifyString(request.CaptchaId, request.CaptchaResponse)
}

Add login function to makefile and template.yaml as same as signup.

Our API returns access token so we can add authorization to our API.

AUTHORIZATION

Aws Lambda Go has an example custom auth function. I refactored this code to check JWT token.

package main

import (
"context"
"errors"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/yunuskilicdev/serverlessExample/common"
)

// Help function to generate an IAM policy
func generatePolicy(principalId, effect, resource string) events.APIGatewayCustomAuthorizerResponse {
authResponse := events.APIGatewayCustomAuthorizerResponse{PrincipalID: principalId}

if effect != "" && resource != "" {
authResponse.PolicyDocument = events.APIGatewayCustomAuthorizerPolicy{
Version: "2012-10-17",
Statement: []events.IAMPolicyStatement{
{
Action: []string{"execute-api:Invoke"},
Effect: effect,
Resource: []string{resource},
},
},
}
}

// Optional output with custom properties of the String, Number or Boolean type.
authResponse.Context = map[string]interface{}{
"stringKey": "stringval",
"numberKey": 123,
"booleanKey": true,
}
return authResponse
}

func handleRequest(ctx context.Context, event events.APIGatewayCustomAuthorizerRequest) (events.APIGatewayCustomAuthorizerResponse, error) {
token := event.AuthorizationToken
parse, e := common.ValidateToken(token)
if e != nil || !parse.Valid {
return events.APIGatewayCustomAuthorizerResponse{}, errors.New("Unauthorized")
}
return generatePolicy("user", "Allow", event.MethodArn), nil
}

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

We need to add an API and Function inside template.yaml

ExampleApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Auth:
DefaultAuthorizer: MyLambdaTokenAuthorizer
Authorizers:
MyLambdaTokenAuthorizer:
FunctionArn: !GetAtt CustomAuthorizerFunction.Arn
CustomAuthorizerFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: bin/authorizer
Handler: authorizer
Runtime: go1.x
Environment:
Variables:
db_user: !Ref username
db_pass: !Ref password
db_name: !Ref dbname
db_host: !Ref host
jwt_key: !Ref jwt
redis_url: !Ref redisurl

Signup function does not require an access token. So you need to state that the signup function has no auth like below.

SignupFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: bin/signup
Handler: signup
Runtime: go1.x
Tracing: Active
Events:
Signup:
Type: Api
Properties:
RestApiId: !Ref ExampleApi
Auth:
Authorizer: 'NONE'

Path: /signup
Method: POST
Environment:
Variables:
db_user: !Ref username
db_pass: !Ref password
db_name: !Ref dbname
db_host: !Ref host
jwt_key: !Ref jwt

USERINFO FUNCTION

To accessing user info requires access token. So I create an userinfo function to demonstrate auth function working.

TIPS&TRICK

Auth do not work when you start your function locally.

Userinfo function

package main

import (
"encoding/binary"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/yunuskilicdev/serverlessExample/common"
"github.com/yunuskilicdev/serverlessExample/common/errormessage"
"github.com/yunuskilicdev/serverlessExample/common/model"
"github.com/yunuskilicdev/serverlessExample/database"
"github.com/yunuskilicdev/serverlessExample/database/entity"
"net/http"
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

token := request.Headers["Authorization"]
userId := common.GetStore().Get(token, false)

postgresConnector := database.PostgresConnector{}
dbConn, dbErr := postgresConnector.GetConnection()
defer dbConn.Close()
if dbErr != nil {
body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.DatabaseError)
response := events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: body.ConvertToJson(),
}
return response, nil
}

var userFilter entity.User
u, _ := binary.Uvarint(userId)
userFilter.ID = uint(u)
var user entity.User
dbConn.Where(userFilter).Find(&user)

body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.Ok)
body.ResponseObject = user
return events.APIGatewayProxyResponse{
Body: body.ConvertToJson(),
StatusCode: http.StatusOK,
}, nil
}

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

SENDING VERIFICATION EMAIL

After signup, I want to send an email to users to verify their emails. Sending emails can be handled by asynchronously. So I will use AWS Simple Queue Service to write sending mail requests. Then another lambda function reads from the queue and completes sending email.

TIPS&TRICKS

Role creation will be trigger when you first deploy your functions to AWS. In order to write SQS, you need some permission for this role.

  • ReceiveMessage
  • DeleteMessage
  • GetQueueAttributes
  • SendMessage

Inside signup function below code will be added

store := common.GetStore()
expireAt := time.Now().Add(1 * time.Hour)
token, jsonErr := common.CreateToken(*newUser, "Mail", expireAt)
store.SetWithOverrideExpire(token, string(newUser.ID), expireAt.Sub(time.Now()))

var mailRequest model.SendVerificationMailRequest
mailRequest.UserId = newUser.ID
mailRequest.Token = token
mailRequest.Email = newUser.Email
emailJsonData, _ := json.Marshal(mailRequest)
s := string(emailJsonData)
u := string(os.Getenv("email_queue_url"))

sess, err := session.NewSession(&aws.Config{
Region: aws.String("eu-west-1")},
)
if err != nil {
fmt.Println(err)
}
sqsClient := sqs.New(sess)
sqsClient.ServiceName = os.Getenv("email_queue")
input := sqs.SendMessageInput{
MessageBody: &s,
QueueUrl: &u,
}
_, jsonErr = sqsClient.SendMessage(&input)
if jsonErr != nil {
fmt.Println(jsonErr)
}

email_queue and email_queue_url parameters will be used for send message.

So you need to add these to template.yaml

Email Sender Function

package main

import (
"context"
"encoding/json"
"fmt"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/yunuskilicdev/serverlessExample/common"
"github.com/yunuskilicdev/serverlessExample/common/model"
)

const (
// Replace sender@example.com with your "From" address.
// This address must be verified with Amazon SES.
Sender = "a@gmail.com"

// Replace recipient@example.com with a "To" address. If your account
// is still in the sandbox, this address must be verified.
Recipient = "b@gmail.com"

// Specify a configuration set. To use a configuration
// set, comment the next line and line 92.
//ConfigurationSet = "ConfigSet"

// The subject line for the email.
Subject = "Amazon SES Test (AWS SDK for Go)"

// The HTML body for the email.
HtmlBody = "<h1>Amazon SES Test Email (AWS SDK for Go)</h1><p>This email was sent with " +
"<a href='https://aws.amazon.com/ses/'>Amazon SES</a> using the " +
"<a href='https://aws.amazon.com/sdk-for-go/'>AWS SDK for Go</a>.</p>"

//The email body for recipients with non-HTML email clients.
TextBody = "This email was sent with Amazon SES using the AWS SDK for Go."

// The character encoding for the email.
CharSet = "UTF-8"
)

func handler(ctx context.Context, sqsEvent events.SQSEvent) error {
for _, message := range sqsEvent.Records {
fmt.Printf("The message %s for event source %s = %s \n", message.MessageId, message.EventSource, message.Body)
var request model.SendVerificationMailRequest
json.Unmarshal([]byte(message.Body), &request)
common.SendMail(request.Token)
}

return nil
}

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

This function is different from others. Because this function will be triggered by SQS. So its template yaml configuration is also different.

SendMailFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: bin/sendemail
Handler: sendemail
Runtime: go1.x
Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html
Events:
UserInfo:
Type: SQS # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Queue: !Ref emailQueue # NOTE: FIFO SQS Queues are not yet supported
BatchSize: 10
Enabled: false
Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
Variables:
db_user: !Ref username
db_pass: !Ref password
db_name: !Ref dbname
db_host: !Ref host
jwt_key: !Ref jwt
redis_url: !Ref redisurl
email_queue: !Ref emailQueue
email_queue_url: !Ref emailQueueUrl
prod_link: !Sub 'https://${ExampleApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'

Common Send Mail function

package common

import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
"os"
)

const (
// Replace sender@example.com with your "From" address.
// This address must be verified with Amazon SES.
Sender = "a@gmail.com"

// Replace recipient@example.com with a "To" address. If your account
// is still in the sandbox, this address must be verified.
Recipient = "b@gmail.com"

// Specify a configuration set. To use a configuration
// set, comment the next line and line 92.
//ConfigurationSet = "ConfigSet"

// The subject line for the email.
Subject = "Please verify your mail"

// The HTML body for the email.
HtmlBody = "<h1>Email verification mail</h1><p>" +
"<a href='%s'>Amazon SES</a>"

//The email body for recipients with non-HTML email clients.
TextBody = "This email was sent to verify your mail"

// The character encoding for the email.
CharSet = "UTF-8"
)

func SendMail(token string) {
sess, err := session.NewSession(&aws.Config{
Region: aws.String("eu-west-1")},
)

if err != nil {
fmt.Println(err)
}

// Create an SES session.
svc := ses.New(sess)

verifyLink := os.Getenv("prod_link") + "verifyemail?token=" + token

// Assemble the email.
input := &ses.SendEmailInput{
Destination: &ses.Destination{
CcAddresses: []*string{},
ToAddresses: []*string{
aws.String(Recipient),
},
},
Message: &ses.Message{
Body: &ses.Body{
Html: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(fmt.Sprintf(HtmlBody, verifyLink)),
},
Text: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(TextBody),
},
},
Subject: &ses.Content{
Charset: aws.String(CharSet),
Data: aws.String(Subject),
},
},
Source: aws.String(Sender),
// Uncomment to use a configuration set
//ConfigurationSetName: aws.String(ConfigurationSet),
}

// Attempt to send the email.
result, err := svc.SendEmail(input)

// Display error messages if they occur.
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case ses.ErrCodeMessageRejected:
fmt.Println(ses.ErrCodeMessageRejected, aerr.Error())
case ses.ErrCodeMailFromDomainNotVerifiedException:
fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
case ses.ErrCodeConfigurationSetDoesNotExistException:
fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}

return
}

fmt.Println("Email Sent to address: " + Recipient)
fmt.Println(result)
}

TIPS&TRICKS

You need to add AmazonSESFullAccess policy to sendemail functions’s role.

TIPS&TRICKS

You need to verify the sender mail address at AWS Console.

VALIDATION EMAIL FUNCTION

package main

import (
"encoding/binary"
"fmt"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/yunuskilicdev/serverlessExample/common"
"github.com/yunuskilicdev/serverlessExample/common/errormessage"
"github.com/yunuskilicdev/serverlessExample/common/model"
"github.com/yunuskilicdev/serverlessExample/database"
"github.com/yunuskilicdev/serverlessExample/database/entity"
"net/http"
)

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

token := request.QueryStringParameters["token"]
validateToken, err := common.ValidateToken(token)
if err != nil {
fmt.Println(err)
}
claims := validateToken.Claims.(*model.CustomClaims)
if validateToken.Valid && claims.Type == "Mail" {
store := common.GetStore()
value := store.Get(token, true)
var userFilter entity.User
u, _ := binary.Uvarint(value)
userFilter.ID = uint(u)
postgresConnector := database.PostgresConnector{}
dbConn, dbErr := postgresConnector.GetConnection()
defer dbConn.Close()
if dbErr != nil {
fmt.Print(dbErr)
return events.APIGatewayProxyResponse{
StatusCode: http.StatusInternalServerError,
Body: "",
}, nil
}
var user entity.User
dbConn.Where(userFilter).Find(&user)
user.EmailVerified = true
dbConn.Save(&user)
body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.Ok)
body.ResponseObject = user
return events.APIGatewayProxyResponse{
Body: body.ConvertToJson(),
StatusCode: http.StatusOK,
}, nil
}

body := model.ResponseBody{}
body.Message = errormessage.StatusText(errormessage.TokenIsNotValid)
body.ResponseObject = nil
return events.APIGatewayProxyResponse{
Body: body.ConvertToJson(),
StatusCode: http.StatusBadRequest,
}, nil

}

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

So all functions are ready :)

TIPS&TRICKS

When I use Elasticache for caching, connection to elasticache was very long. So I prefer to use Redis which is installed on EC2 instance. If your functions and EC2 are inside the same VPC Security Group, then your function can access EC2 instances.

Github

https://github.com/yunuskilicdev/serverlessExample

--

--

Yunus Kılıç
Analytics Vidhya

I have 10 years of experience in high-quality software application development, implementation, and integration.