Controlling Cognito with the Go AWS SDK

by Hadley Bradley

I’m working on a web application that requires user authentication. Rather than reinvent something myself, I’ve decided to let Amazon’s Cognito service handle authentication.

Amazon Cognito is a fully managed service that provides a secure user directory that scales to hundreds of millions of users. Cognito is easy to set up without having to worry about provisioning server infrastructure. Amazon Cognito is a standards-based Identity Provider and supports identity and access management standards, such as Oauth 2.0, SAML 2.0, and OpenID Connect.

If you’re planning to use any AWS service, always review the quotas and limits that come with standard accounts. For example, my application will require several custom attributes to be added and recorded against each user account. One of the things I checked was the maximum number of custom attributes that a user pool can have configured. Luckily it’s fifty, more than enough for what I need for my application.

The first step is to install the AWS software development kit (SDK) for Go, and this can be achieved from the terminal using the following command:

go get github.com/aws/aws-sdk-go/...

After successfully installing the AWS SDK, you’ll need to import the relevant sections into your program so you interact with the Cognito service.

package main

import (
    "fmt"
    "os"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
)

The first step in our interaction with any AWS service is to initialise a new session. Go programs can instantiate new sessions by providing a valid AWS region and your AWS access & secret keys. The keys shouldn’t be hardcoded within your source code for security reasons. If your private keys are hardcoded in your source code, you may unintentionally leak them on sites like GitHub.

For this reason, it’s considered best practice to define your secret key values as operating system environment variables and use Go’s os.Getenv command to obtain the values.

After calling the NewSession function we need to check the error state. If an error has occurred, we terminate the program and log the error message to the terminal.

s, err := session.NewSession(&aws.Config{
    Region: aws.String("eu-west-2"),
    Credentials: credentials.NewStaticCredentials(
        os.Getenv("AccessKeyID"),
        os.Getenv("SecretAccessKey"),
        ""),
})

if err != nil {
    log.Fatal(err.Error())
}

Listing all users within a Cognito User Pool

Before looking at authenticating users, let’s look at the commands and methods you’ll need to develop a custom interface to manage your user directory. Why would you want to create a custom interface to manage your user directory?. For sure, you could grant users to the AWS Cognito web console directly. However, suppose your user directory contains custom attributes that are industry-specific. In that case, it might be beneficial to develop a custom web application that staff can use to manage the user directory. If you need help in creating such an interface, get in touch as I’ll be able to develop a serverless dashboard using Lamda functions.

If you’re not using the AWS web console to manage your user directory, you will have to list the users currently registered in the directory so that that staff can interact with them. You can retrieve a list of all users by using the .ListUsers function. The example below shows how to instantiate a Cognito client using the session already established.

Nearly all the functions described below need to know the Cognito user pool id. While the examples show it hardcoded into the source code, it would be best practice to include this as an environment variable, like the secret keys.

cognitoClient := cognitoidentityprovider.New(session)

ListUsersOutput, err := cognitoClient.ListUsers(
    &cognitoidentityprovider.ListUsersInput{
        UserPoolId: aws.String(`eu-west-2_gtpxQQmErxT`),
    })

if err != nil {
    fmt.Println("Error listing users : " + err.Error())
    os.Exit(1)
}

To iterate over each user returned in the results, you can use a range function to process each users record in turn.

for _, user := range ListUsersOutput.Users {
    fmt.Println(user.GoString())
}

Listing users with a filter

You can reduce the number of results returned by the .ListUsers function by providing a filter. The filter parameter needs to know which field or custom attribute you wish to filter on, together with the intended search value. The search value, in this case, “Hadley”, must be enclosed in speech marks. The example below demonstrates filtering the results to just those users with a given name of Hadley.

cognitoClient := cognitoidentityprovider.New(session)

ListUsersOutput, err := cognitoClient.ListUsers(
    &cognitoidentityprovider.ListUsersInput{
        UserPoolId: aws.String(`eu-west-2_gtpxQQmErxT`),
        Filter:     aws.String(`given_name = "Hadley"`),
    })

if err != nil {
    fmt.Println("Error listing users : " + err.Error())
    os.Exit(1)
}

Adding a user to a group

If your application requires fine-grained role-based access, then you’re going to want to use the groups feature within Cognito. A users group membership can then define what the user can do within your application. As such, your staff management application will need the ability to add users to a specific group. The example below demonstrates adding a particular user to a group called “Clinical”.

AdminAddUserToGroupOutput, err := cognitoClient.AdminAddUserToGroup(
    &cognitoidentityprovider.AdminAddUserToGroupInput{
        UserPoolId: aws.String("eu-west-2_gtpxQQmErxT"),
        Username:   aws.String("f45fe6a6-f06b-4a4f-aebe-d7168861a7e4"),
        GroupName:  aws.String("Clinical"),
    })

if err != nil {
    fmt.Println(err.Error())
    os.Exit(1)
}

fmt.Println(AdminAddUserToGroupOutput.GoString())

Updating a user field or custom attribute

When setting up new users within your directory, you might need the capability of updating the values stored within custom attributes. The example below demonstrates updating a custom attribute called “organisation_name”. You can use the AdminUpdateUserAttributes function to update one or more attributes at once.

AdminUpdateUserAttributesOutput, err := cognitoClient.AdminUpdateUserAttributes(
    &cognitoidentityprovider.AdminUpdateUserAttributesInput{
        UserPoolId: aws.String("eu-west-2_gtpxQQmErxT"),
        Username:   aws.String("f45fe6a6-f06b-4a4f-aebe-d7168861a7e4"),
        UserAttributes: []*cognitoidentityprovider.AttributeType{
            {
                Name:  aws.String("custom:organisation_name"),
                Value: aws.String("Heartbeat Northwest Cardiac Rehabilitation"),
            },
        },
    })

if err != nil {
    fmt.Println(err.Error())
    os.Exit(1)
}

Authenticating users

To authenticate users, we need to compute a secret hash. The secret hash is an HMAC (Keyed-Hash Message Authentication Code) generated using the Cognito clients secret. We then take the user’s username we’re authenticating and combine it with the Cognito client’s ID. The output from the HMAC operation is converted to a base64 encoded string. The base64 encoded string then becomes our secret hash value.

With the computed secret hash, we can use the .InitiateAuthInput function to authenticate the user.

mac := hmac.New(sha256.New, []byte(os.Get(ClientSecret)))
mac.Write([]byte(username + os.Get(ClientID)))
secretHash := base64.StdEncoding.EncodeToString(mac.Sum(nil))

cognitoClient := cognitoidentityprovider.New(session)

authTry := &cognitoidentityprovider.InitiateAuthInput{
    AuthFlow: aws.String("USER_PASSWORD_AUTH"),
    AuthParameters: map[string]*string{
        "USERNAME":    aws.String(username),
        "PASSWORD":    aws.String(password),
        "SECRET_HASH": aws.String(secretHash),
    },
    ClientId: aws.String(clientId),
}

res, err := cognitoClient.InitiateAuth(authTry)
if err != nil {
    fmt.Println("NOT authenticated")
    fmt.Println(err.Error())
} else {
    fmt.Println("authenticated")
    fmt.Println(res.AuthenticationResult)
}

If the user has supplied the correct credentials and the authentication works, you get back a JSON structure containing two JSON Web Tokens. Some of the detail has been redacted from the example below for brevity. Essentially you get back an AccessToken and an IdToken. The IdToken contains claims for all the data fields associated with the user.

{
  AccessToken: "eyJra... RKxfR1C2Q",
  ExpiresIn: 3600,
  IdToken: "eyJr ... rx1ezR9w",
  TokenType: "Bearer"
}

Need Help or Advice

Remember, if you need any help or advice in using AWS Cognito with Go then please get in touch I’d be happy to help.