How to create Auth API with Go and AWS Cognito


This post is written 24 months ago. Probably the information might be out of date.

Auth with Cognito

There are several ways to create Authentification with Cognito such as

  • Implementing Hosted UI
  • Using AWS Amplify
  • Creating Auth API with your backend

I think the easiest way is implementing Hosted UI on Frontend and getting an access token with Authorization code from Hosted UI. However, I research how to create an auth api with Go, Gin, and Cognito so I will describe it.

Create the project with Go

Run this command in order to start a Go project.

$ go mod init {package-name}

# Installing libraries
$ go get github.com/aws/aws-sdk-go-v2/aws
$ go get github.com/aws/aws-sdk-go-v2/config
$ go get github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider
$ go get github.com/joho/godotenv
$ go get github.com/gin-gonic/gin
copied!

After installing all libraries, I made directories as shown below.

.
├── config
│   └── config.go
├── controllers
│   ├── authController.go
│   └── webServer.go
├── go.mod
├── go.sum
└── main.go
copied!

Create config.go and .env

I created .env file and config.go. If you set up 3 environmental variables below in .env,

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_REGION

LoadDefaultConfig(context.TODO()) of aws-sdk-go-v2/config will read them by itself. However, we have to write code with godotenv to read other variables.

USER_POOL_ID=
COGNITO_CLIENT_ID=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
copied!
package config

import (
	"context"
	"fmt"
	"os"

	"github.com/aws/aws-sdk-go-v2/aws"
	awsConfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
	"github.com/joho/godotenv"
)

type ConfigList struct {
	UserPoolId      string
	CognitoClientId string
}

var Config ConfigList

func init() {
	err := godotenv.Load()
	if err != nil {
		fmt.Println(err.Error())
	}

	Config = ConfigList{
		UserPoolId:      os.Getenv("USER_POOL_ID"),
		CognitoClientId: os.Getenv("COGNITO_CLIENT_ID"),
	}
}

func LoadAwsConfig() aws.Config {
	cfg, err := awsConfig.LoadDefaultConfig(context.TODO())
	if err != nil {
		panic(err)
	}
	return cfg
}

func NewCognitoConfig() *cognitoidentityprovider.Client {
	return cognitoidentityprovider.NewFromConfig(LoadAwsConfig())
}
copied!

Create authController.go

I made 4 functions

  • CreateNewUser: When you create a new user, he receives an email with a temporary password.
  • ActivateUser: The new user needs to activate the account with it and the new password.
  • Login: When a user login after the activation, the response of login has an accessToken.
  • ChangePassword: users can change after the activation and log in
package controllers

import (
	"apiauth/config"
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
	"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types"
	"github.com/gin-gonic/gin"
)

var userPoolId = config.Config.UserPoolId

type UserCreateBody struct {
	Email    string `json:"email"`
}

type LoginBody struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type ActivateUserBody struct {
	NewPassword string `json:"newPassword"`
	Username    string `json:"username"`
	Session     string `json:"session"`
}

type ChangePasswordBody struct {
	PreviousPassword string `json:"previousPassword"`
	NewPassword      string `json:"newPassword"`
	AccessToken      string `json:"accessToken"`
}

func CreateNewUser(ctx *gin.Context) {
	var userCreateBody UserCreateBody
	client := config.NewCognitoConfig()

	if err := ctx.ShouldBindJSON(&userCreateBody); err != nil {
		fmt.Println(err.Error())
	}

	newUserData := &cognitoidentityprovider.AdminCreateUserInput{
		UserPoolId: &config.Config.UserPoolId,
		Username:   &userCreateBody.Email,
		UserAttributes: []types.AttributeType{
			{
				Name:  aws.String("email"),
				Value: aws.String(userCreateBody.Email),
			},
		},
	}

	result, err := client.AdminCreateUser(ctx, newUserData)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
		return ctx.Abort()
	}

	ctx.JSON(http.StatusCreated, gin.H{
		"data": result,
	})
}

func Login(ctx *gin.Context) {
	var loginBody LoginBody
	client := config.NewCognitoConfig()

	if err := ctx.ShouldBindJSON(&loginBody); err != nil {
		fmt.Println(err.Error())
	}

	params := &cognitoidentityprovider.AdminInitiateAuthInput{
		UserPoolId: &config.Config.UserPoolId,
		AuthFlow:   types.AuthFlowTypeAdminUserPasswordAuth,
		AuthParameters: map[string]string{
			"USERNAME": loginBody.Username,
			"PASSWORD": loginBody.Password,
		},
		ClientId: &config.Config.CognitoClientId,
	}

	result, err := client.AdminInitiateAuth(ctx, params)

	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
		return ctx.Abort()
	}

	ctx.JSON(http.StatusOK, gin.H{
		"data": result,
	})
}

func ActivateUser(ctx *gin.Context) {
	var activateUserBody ActivateUserBody
	client := config.NewCognitoConfig()

	if err := ctx.ShouldBindJSON(&activateUserBody); err != nil {
		fmt.Println(err.Error())
	}

	params := &cognitoidentityprovider.AdminRespondToAuthChallengeInput{
		UserPoolId:    &config.Config.UserPoolId,
		ClientId:      &config.Config.CognitoClientId,
		ChallengeName: "NEW_PASSWORD_REQUIRED",
		ChallengeResponses: map[string]string{
			"USERNAME":     activateUserBody.Username,
			"NEW_PASSWORD": activateUserBody.NewPassword,
		},
		Session: aws.String(activateUserBody.Session),
	}

	result, err := client.AdminRespondToAuthChallenge(ctx, params)

	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
		return ctx.Abort()
	}

	ctx.JSON(http.StatusOK, gin.H{
		"data": result,
	})

}

func ChangePassword(ctx *gin.Context) {
	var changePasswordBody ChangePasswordBody
	client := config.NewCognitoConfig()

	if err := ctx.ShouldBindJSON(&changePasswordBody); err != nil {
		fmt.Println(err.Error())
	}

	params := &cognitoidentityprovider.ChangePasswordInput{
		AccessToken:      aws.String(changePasswordBody.AccessToken),
		PreviousPassword: aws.String(changePasswordBody.PreviousPassword),
		ProposedPassword: aws.String(changePasswordBody.NewPassword),
	}

	result, err := client.ChangePassword(ctx, params)

	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
		return ctx.Abort()
	}

	ctx.JSON(http.StatusOK, gin.H{
		"data": result,
	})
}
copied!

Create main.go

I wrote routing in GetRouters and imported in main()

package main

import (
	controller "api/controllers"
)

func main() {
	router := controller.GetRouters()
	router.Run(":8080")
}
copied!
package controllers

import (
	"github.com/gin-gonic/gin"
)

func GetRouters() *gin.Engine {
	router := gin.Default()

	router.POST("/users", CreateNewUser)
	router.POST("/login", Login)
	router.POST("/activation", ActivateUser)
	router.PUT("/password", ChangePassword)

	return router
}
copied!

Summary

This is a signup and login with Gin and Cognito. I created MFA verification with this stack as well and I will share about that later.

buy me a coffee