AWS SDK for Go and Cognito

How to authenticate users in Golang Web with AWS Cognito
Share this page:

So far, we created the Cognito User Pool and App Client using Cognito. Now it’s time to create Login, Logout and Register page in Unicorn Pursuit, so our users can authenticate.

If you’ve just landed here, we’re doing a “Become a Cloud Architect” Unicorn Workshop by building a Unicorn Pursuit Web App step by step, and you’re more then welcome to join!

Requirements

  • We want our users to login using their e-mail address as a username.
  • e-mail address and phone number will be verified.
  • Once user verifies their phone number, they will be able to see the entire web.
  • Once they confirm their email address, they will be able to submit new project ideas, or vote.
  • Register and Login page will appear only to non-logged-in users.
  • Once user Registers and gets OTP, they need to be redirected to Login Successful.

To do all this, we will need to create 3 variables:

  • is_logged_in boolean, to present the correct menu to the user.
  • email_confirmed boolean, to authorize user for voting.
  • token to make sure user doesn’t have to keep authenticatnig on each page.

Code

Before we get into Code, let’s understand what we’re building. Check out the diagram below:

Cognito CloudFront Route53

We already created a variable called ClientIDValue by retrieving it from the AWS SSM Parameter Store where Cognito Client Application ID is stored.

Before getting to the actual configuration, be sure to check if the SSM is returning the correct value. A neat trick is to print it, so you can compare if everything is Ok.

// ClientIDValue stores the value of Cognito Application Client ID
ClientIDValue := *param.Parameter.Value
fmt.Println(ClientIDValue)

We first need to create a struct type variable, that we will use for our Cognito Example of Authentication. We need to import cognito module from AWS SDK for GO, and we’ll just call it Cognito. This is all done in the main.go package.

import cognito "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"

And now let’s identify a struct (or class) that includes all the info needed for our Authentication Flow. Remember in the Cloud Infra part we configured Auth Flow to use Username which is email, and Password. We’ll use regFlow to later attach different calls (like OTP) to the correct username.

// CognitoExample holds internals for auth flow.
type CognitoExample struct {
 CognitoClient *cognito.CognitoIdentityProvider
 RegFlow       *regFlow
 UserPoolID    string
 AppClientID   string
}

type regFlow struct {
 Username string
}

In routes.go we’ll need to call functions that execute Register and Login, which is why we’ll instantiate an object called ce, and assign an AWS session variable created here, regFlow with the username, UserPoolId called CognitoUnicornUserPool and the AppClientID with an ID retrieved from SSM:

ce := CognitoExample{
 CognitoClient: cognito.New(sess),
 RegFlow:       &regFlow{},
 UserPoolID:    "CognitoUnicornUserPool",
 AppClientID:   ClientIDValue,
}

ce is our main cognito object, holding all the info needed for the functions to execute the user Login and Registration (together with the info functions will get directly from the respective HTML forms). Therefore, in the routes.go we can call the Login function using the API POST to /login route:

userRoutes.POST("/login", ensureNotLoggedIn(), performLogin(ce))

We can do a User Register doing a API POST to /register route:

userRoutes.POST("/register", ensureNotLoggedIn(), register(ce))

Awesome. Now let’s go to the package handlers.user.go, in charge of handling users, and create the two functions, performLogin and register, getting ce CognitoExample object as an input. Get ready, turn your brain ON.

In /templates/ folder, there is a file called login.html. From this file we need to get an email, which we’ll treat as username, and a Password.

func performLogin(ce CognitoExample) gin.HandlerFunc {
 fn := func(c *gin.Context) {
  // Obtain the POSTed username (email) and password values from HTML login.html template
  username := c.PostForm("email")
  password := c.PostForm("password")

  // Authentication Flow for Login to Cognito
  flow := aws.String(flowUsernamePassword)
  params := map[string]*string{
   "USERNAME": aws.String(username),
   "PASSWORD": aws.String(password),
  }

  // Create the authTry cognito object, that we'll use to POST the authentication request
  authTry := &cognito.InitiateAuthInput{
   AuthFlow:       flow,
   AuthParameters: params,
   ClientId:       aws.String(ce.AppClientID),
  }

  // Execute the Authentication passing authTry object to AWS, handling the error below.
  result, err := ce.CognitoClient.InitiateAuth(authTry)

  if err != nil {
   // If the username/password combination is invalid,
   // show the error message on the login page
   c.HTML(http.StatusBadRequest, "login.html", gin.H{
    "ErrorTitle":   "Login Failed",
    "ErrorMessage": "Invalid credentials"})

  } else {

  // Success!!! Set token and is_logged_in
   fmt.Println(result)
   // Set Token and ïs_logged_in Boolean, and jump to a different Menu
   // NEXT STEP: Import TOKEN variable from Login

  // temporarily we're using an internal function to generate Token. Don't do this in production! We'll get Token from Cognito call response.
   token := generateSessionToken()

  // Set Cookie with the token
   c.SetCookie("token", token, 3600, "", "", false, true)
   c.Set("is_logged_in", true)

  // handle Gin redirection to Login Successful page
   render(c, gin.H{
    "title": "Successful Login"}, "login-successful.html")
   return
  }
 }
 return gin.HandlerFunc(fn)
}

Awesome, that was the Login function. Now the similar way we need to handle register(ce). This function is also in User Handler package, and from the /template/register.html we need to get: e-mail, Full Name, Password that meets the rules we defined in Cognito User Pool, and the Phone Number that includes the Country Code, which will be verified using OTP (One Time Password), a SMS code received via SMS, that we’ll insert into /template/otp.html. This means that our flow looks like this:

  • User inputs the required data.
  • If all is OK - redirect to otp.html. This is where Username stored in regFlow struct will play a role.
  • If OTP is confirmed - redirect to login-successful.html

Here is our code:

func register(ce CognitoExample) gin.HandlerFunc {
 fn := func(c *gin.Context) {

  // Obtain the values from register.html form
  username := c.PostForm("email")
  password := c.PostForm("password")
  fullname := c.PostForm("name")
  emailAddress := c.PostForm("email")
  phoneNumber := c.PostForm("phone_number")
  
  // create a Cognito user object
  user := &cognito.SignUpInput{
   Username: aws.String(username),
   Password: aws.String(password),
   ClientId: aws.String(ce.AppClientID),
   UserAttributes: []*cognito.AttributeType{
    {
     Name:  aws.String("email"),
     Value: aws.String(emailAddress),
    },
    {
     Name:  aws.String("name"),
     Value: aws.String(fullname),
    },
    {
     Name:  aws.String("phone_number"),
     Value: aws.String(phoneNumber),
    },
   },
  }

  // attempt SignUp with the created user object
  _, err := ce.CognitoClient.SignUp(user)
  if err != nil {
   fmt.Println(err)
   c.HTML(http.StatusBadRequest, "register.html", gin.H{
    "ErrorTitle":   "Registration Failed",
    "ErrorMessage": err.Error()})
  } else {
  // is Success, store username in the regFlow and redirect to OTP
   ce.RegFlow.Username = username
   render(c, gin.H{
    "title": "Redirected to One Time Password"}, "otp.html")

  }

 }
 return gin.HandlerFunc(fn)
}

And the only function we have left to make, is the one that would confirm our OTP code, and redirect us to Login Successful if the code is correct. Here is the code:

// OTP is Handling One Time Password
func OTP(ce CognitoExample) gin.HandlerFunc {
 fn := func(c *gin.Context) {

// We need to get the code that user typed in the html form
  otp := c.PostForm("otp")

  // Creating a user object to ConfirmSignUp
  user := &cognito.ConfirmSignUpInput{
   ConfirmationCode: aws.String(otp),
   Username:         aws.String(ce.RegFlow.Username),
   ClientId:         aws.String(ce.AppClientID),
  }

  // Confirm SignUp with Cognito Application Client
  result, err := ce.CognitoClient.ConfirmSignUp(user)

  if err != nil {
   fmt.Println(err)
   c.HTML(http.StatusBadRequest, "otp.html", gin.H{
    "ErrorTitle":   "OTP Failed",
    "ErrorMessage": err.Error()})
  } else {
  
  // If OK, generate Token, set "is_logged_in" to True, and redirect to Login Success.
   fmt.Println(result)
   token := generateSessionToken()
   c.SetCookie("token", token, 3600, "", "", false, true)
   c.Set("is_logged_in", true)

   render(c, gin.H{
    "title": "Successful registration & Login"}, "login-successful.html")

  }
 }
 return gin.HandlerFunc(fn)
}

That’s it, our users should be able to Register and Login.

More cool stuff we could do:

  • Use local storage for the Login Response Token.
  • Confirm e-mail address for a Domain.

If you wish to improve the code - you know where to find it.




Last modified June 8, 2020: AWS Diagrams Added (c98aff0)