What is cognito?
The authentication of an application is something very important in the system, but also very sensitive, there are various implementations, security, validation to be considered.
I decided to make a post demonstrating a about .
Let's create the following files:
The entrypoint of our application
main.goat the root of the project.envTo save cognitive credentialsA paste called cognitoClient and inside a file called
cognito.goThere is a file called
request.http, to complete your requests.
The structure will be as follows:
I need to configure some more things, let's go!
Password policy mode allows you to select a specific policy, let's deixar the Cognito defaults.
Multi-factor authentication allows our login to have two-factor authentication, let's go without, but you can implement it if desired, you can opt for No MFA.- Finally, or User account recovery, you can choose ways to recover your account, you can just choose email.
Next step:
Self-service sign-up, we will allow any person to do so, leave selected.
Cognito-assisted verification and confirmation, allow cognito to be responsible for confirming the user's identity, check it, and also select the option Send email message, verify email address.
Verifying attribute changes, check this option, so that updating the user's email needs to be validated again.
Required attributes, select the fields that you want to make mandatory to create a new user, you will select the options, email (and the name) and the name will also be required by your father.
Custom attributes, it is optional, but you can add custom fields, for example, you will create a field calledcustom_idwhich will be anyuuid.
This stage also occurred:
for aws, for access to the created pool App integration > App client list and see our Client ID:
how to do it.
Implementing cognito
Let's separate the cognito part into another file.
Registering the user
Inside the cognito.go file, we will initialize our cognito and create our interface:
package congnitoClient
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
cognito "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
"github.com/google/uuid"
)
type User struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type CognitoInterface interface {
SignUp(user *User) error
}
type cognitoClient struct {
cognitoClient *cognito.CognitoIdentityProvider
appClientID string
}
func NewCognitoClient(appClientId string) CognitoInterface {
config := &aws.Config{Region: aws.String("us-east-1")}
sess, err := session.NewSession(config)
if err != nil {
panic(err)
}
client := cognito.New(sess)
return &cognitoClient{
cognitoClient: client,
appClientID: appClientId,
}
}
func (c *cognitoClient) SignUp(user *User) error {
return nil
}
First we create a struct called User, this struct will have the user fields that we need to save in cognito.
Then we create an interface called CognitoInterface, we will have the methods that we will use, first we will only have SignUp which will receive a pointer to the User struct.
Then we will have another struct called cognitoClient that will contain our instance for NewCognitoClient that will be our constructor.
As mentioned, NewCognitoClient will be like our constructor, it is where we will create the session with AWS and return this connection. This connection could be a global variable, in our case we will not do this, it is up to you to check which is the best approach for your use case.
Now let's implement SignUp:
func (c *cognitoClient) SignUp(user *User) error {
userCognito := &cognito.SignUpInput{
ClientId: aws.String(c.appClientID),
Username: aws.String(user.Email),
Password: aws.String(user.Password),
UserAttributes: []*cognito.AttributeType{
{
Name: aws.String("name"),
Value: aws.String(user.Name),
},
{
Name: aws.String("email"),
Value: aws.String(user.Email),
},
{
Name: aws.String("custom:custom_id"),
Value: aws.String(uuid.NewString()),
},
},
}
_, err := c.cognitoClient.SignUp(userCognito)
if err != nil {
return err
}
return nil
}
We will use the AttributeType from Cognito to assemble the parameters that we will send to the SignUp of the AWS SDK, note that the custom_id which is our custom field, needs to be placed custom before, without this it will not be accepted, we just created a uuid with the Google package, this field is just to show how to use custom attributes.
The ClientId field refers to the COGNITO_CLIENT_ID of our env, we will pass it on when starting main.go.
This is what we need to save the user, simple isn't it?
Don't forget to start the project with:
go mod init <your project name>
And install the necessary packages:
go mod tidy
Confirming the account
Let's create another function to verify the user's account via email. To verify the account, the user will need to enter the code sent by email. Let's create a new struct and add the new ConfirmAccount method to the interface:
type UserConfirmation struct {
Email string `json:"email" binding:"required,email"`
Code string `json:"code" binding:"required"`
}
type CognitoInterface interface {
SignUp(user *User) error
ConfirmAccount(user *UserConfirmation) error
}
Now let's implement:
func (c *cognitoClient) ConfirmAccount(user *UserConfirmation) error {
confirmationInput := &cognito.ConfirmSignUpInput{
Username: aws.String(user.Email),
ConfirmationCode: aws.String(user.Code),
ClientId: aws.String(c.appClientID),
}
_, err := c.cognitoClient.ConfirmSignUp(confirmationInput)
if err != nil {
return err
}
return nil
}
It's very simple, we will use the ConfirmSignUpInput from the cognito package to assemble the parameters, remembering that the Username is the user's email. Finally, we will call ConfirmSignUp passing the confirmationInput.
Remembering that we only returned the error, you could improve and check the types of error messages.
Login
This should be the functionality that will be used the most, let's create a method called SignIn and a struct:
type UserLogin struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
type CognitoInterface interface {
SignUp(user *User) error
ConfirmAccount(user *UserConfirmation) error
SignIn(user *UserLogin) (string, error)
}
Our SignIn will receive a UserLogin.
Let's implement:
func (c *cognitoClient) SignIn(user *UserLogin) (string, error) {
authInput := &cognito.InitiateAuthInput{
AuthFlow: aws.String("USER_PASSWORD_AUTH"),
AuthParameters: aws.StringMap(map[string]string{
"USERNAME": user.Email,
"PASSWORD": user.Password,
}),
ClientId: aws.String(c.appClientID),
}
result, err := c.cognitoClient.InitiateAuth(authInput)
if err != nil {
return "", err
}
return *result.AuthenticationResult.AccessToken, nil
}
We will use the InitiateAuth function from the aws cognito package, we need to pass the username (user's email), password and the AuthFlow, this field refers to the type of access that we will allow, in our case USER_PASSWORD_AUTH.
If you receive an error like this:
You trusted all proxies, this is NOT safe. We recommend you to set a value
It will be necessary to enable the ALLOW_USER_PASSWORD_AUTH flow, to configure it access cognito on the aws panel, go to:
User pools > Selecione seu pool > App integration > App client list > Selecione um client, will open this screen:
We will also pass Permanent, informing that it is a permanent password, you could pass false, so Cognito would create a temporary password for the user, this will depend on the strategy you will use in your application.
Creating the main
Let's create our main.go, this will be the file where we will start cognito and create our routes.
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
cognitoClient := congnitoClient.NewCognitoClient(os.Getenv("COGNITO_CLIENT_ID"))
r := gin.Default()
fmt.Println("Server is running on port 8080")
err = r.Run(":8080")
if err != nil {
panic(err)
}
}
First we will load our envs with the
Confirming a user
Note that the user we created above is not confirmed, let's confirm it!
Create a function called ConfirmAccount in the main.go file:
func ConfirmAccount(c *gin.Context, cognito congnitoClient.CognitoInterface) error {
var user congnitoClient.UserConfirmation
if err := c.ShouldBindJSON(&user); err != nil {
return errors.New("invalid json")
}
err := cognito.ConfirmAccount(&user)
if err != nil {
return errors.New("could not confirm user")
}
return nil
}
Same concept we used before, let's convert the body to the UserConfirmation struct and pass it to ConfirmAccount in cognito.go.
Let's create the endpoint:
r.POST("user/confirmation", func(context *gin.Context) {
err := ConfirmAccount(context, cognitoClient)
if err != nil {
context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
context.JSON(http.StatusCreated, gin.H{"message": "user confirmed"})
})
It's also simple, we just handle the error and return a message, let's create our call and test it:
POST http://localhost:8080/user/confirmation HTTP/1.1
content-type: application/json
{
"email": "[email protected]",
"code": "363284"
}
We will receive the message:
{
"message": "user confirmed"
}
Now accessing Cognito again on the AWS panel, notice that the user is confirmed, remembering that you need to enter a valid email, you can use a temporary email to play around, but it needs to be valid, as Cognito will send the confirmation code and it needs to be a valid code to confirm successfully.
If we get the jwt token we can see what's inside, using the website .
Repository link
Project
Subscribe and receive notification of new posts, participate
SOCIAL SHARE CARD GENERATOR