AWS SDK for Go and Cognito
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:
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: ®Flow{},
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 inregFlow
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.
Feedback
Was this page helpful?
Awesome! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.