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
After installing all libraries, I made directories as shown below.
.
├── config
│ └── config.go
├── controllers
│ ├── authController.go
│ └── webServer.go
├── go.mod
├── go.sum
└── main.go
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=
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())
}
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,
})
}
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")
}
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
}
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.