Setting up a basic sign-up/sign-in flow with Amazon Cognito

GarryPas
7 min readJun 29, 2023

Introduction

I’m currently looking for an authentication provider for my side-project, to avoid having to manage user profiles, passwords etc. One such service is Cognito, available from AWS (Amazon Web Services).

I’ve done a spike to try and get the sign-up and sign-in flows working, just with username and password for now. The application is a web app built with React. I thought I’d share my learnings here.

User Pools

User pools are a user directory service that allows you to easily manage and scale user registration, sign-in, and authentication for your applications. Auth is usually pretty generic and not something you want to reinvent for every application, so it makes sense to offload some of this to a third party, even for a small fee.

The first step towards getting setup was to login to the AWS Management Console and search ‘Cognito’. From here there is an option called ‘Create user pool’.

You are presented with a wizard which has quite a bewildering number of options, including things like SES (Simple E-mail Service) integration, necessary for things like forgotten password use cases. For this exercise I just wanted the simplest configuration possible. Luckily AWS have built a tool that simplifies this sort of thing — The AWS Amplify toolchain.

AWS Amplify

I used AWS amplify, which allowed me to quickly get my web app code up and running. I used the @aws-amplify/cli tool to create the User Pool on the backend, and the aws-amplify library for the code in my web app.

npm install --save aws-amplify
npm install --save-dev @aws-amplify/cli
npm install --save @aws-amplify/ui-react

These commands initialised the files needed.

mkdir amplify && cd amplify
npx amplify init
npx amplify add auth
npx amplify push

A bit of explanation as to what is going on here; amplify has created a Cloud Formation Stack which is used to manage resources such as the User Pool, Lambdas and quite possibly some things you don’t need. Even if you don’t use it for a ‘real’ product it’s a great way to get started and build a good mental model before going off and trying to Terraform everything

React UI — Sign Up

To tell the amplify code where your User Pool lives and how to interact with it, you need to create an amplify config document. I used this template from the amplify docs and adapted accordingly. I called configureAuth() from my application entry point.

import { Amplify, Auth } from 'aws-amplify';

Amplify.configure({
Auth: {
// REQUIRED - Amazon Cognito Region
region: 'eu-west-1',

// OPTIONAL - Amazon Cognito User Pool ID
userPoolId: 'XX-XXXX-X_abcd1234',

// OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string)
userPoolWebClientId: 'a1b2c3d4e5f6g7h8i9j0k1l2m3',

// OPTIONAL - This is used when autoSignIn is enabled for Auth.signUp
// 'code' is used for Auth.confirmSignUp, 'link' is used for email link verification
signUpVerificationMethod: 'code', // 'code' | 'link'

// OPTIONAL - Configuration for cookie storage
// Note: if the secure flag is set to true, then the cookie transmission requires a secure protocol
cookieStorage: {
// REQUIRED - Cookie domain (only required if cookieStorage is provided)
domain: '.yourdomain.com',
// OPTIONAL - Cookie path
path: '/',
// OPTIONAL - Cookie expiration in days
expires: 365,
// OPTIONAL - See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
sameSite: 'strict' | 'lax',
// OPTIONAL - Cookie secure flag
// Either true or false, indicating if the cookie transmission requires a secure protocol (https).
secure: true,
},

// OPTIONAL - Manually set the authentication flow type. Default is 'USER_SRP_AUTH'
authenticationFlowType: 'USER_PASSWORD_AUTH',

// OPTIONAL - Manually set key value pairs that can be passed to Cognito Lambda Triggers
clientMetadata: {myCustomKey: 'myCustomValue'},

// OPTIONAL - Hosted UI configuration
oauth: {
domain: 'your_cognito_domain',
scope: [
'phone',
'email',
'profile',
'openid',
'aws.cognito.signin.user.admin',
],
redirectSignIn: 'http://localhost:3000/',
redirectSignOut: 'http://localhost:3000/',
responseType: 'code', // or 'token', note that REFRESH token will only be generated when the responseType is code
},
},
});


// You can get the current config object
export const configureAuth = () => Auth.configure();

Then I implemented the sign-up code:

export const register = async (username: string, password: string) => {
try {
const { user } = await Auth.signUp({
username,
password,
attributes: {
email: username,
},
autoSignIn: {
enabled: false,
}
});
} catch (error) {
console.log('error signing up:', error);
}
}

And I wired this up to a React form I knocked up previously (I’d been using miragejs to mock out the backend up until now).

After signing up I check in the Cognito user pool to ensure the user had been created.

One further step is required; confirm your email. As you can see from the confirmation status, the user is not yet confirmed. Luckily Cognito gives you an option where it can send up to 50 emails a day without requiring you to set up SES. After that many registrations in 1 day you probably need a rest anyway! I checked my inbox and the email was there.

Unfortunately I had no way to verify yet. The code to do this is pretty trivial (see below).

export const confirmRegistration = async ({ username, code}: { username: string, code: string }) => {
try {
await Auth.confirmSignUp(username, code);
} catch (error) {
console.log('error confirming sign up', error);
}
};

…and we also the ability to re-send the code…

export const resendSignUpInfo = async ({ username}: { username: string }) => {
try {
await Auth.resendSignUp(username);
} catch (error) {
console.log('error resending registration code', error);
}
};

Again a fairly simple React component was needed to wire up to this code.

When I did this first time around I got the error USER_PASSWORD_AUTH flow not enabled for this client. Turns out user/password auth isn’t enabled by default. I needed to go into the AWS management console and enable the ALLOW_USER_PASSWORD_AUTH authentication flow for the web client app.

React UI — Sign-in

Now was the time to look at sign-in. The code for this was pretty simple and the UI was again another simple forms component.

    try {
await Auth.signIn(username, password);
} catch (error) {
console.log('error signing in', error);
}

What happens next isn’t well documented; if you take a look in dev tools in Chrome you’ll see some cookies have been set automatically.

With the username, client ID and other stuff in the name they look a bit unwieldy — how do you get the token out? To get to the values you might be tempted to use document.cookies and some parsing, but the correct way is instead like this.

const { accessToken, refreshToken } = await Auth.currentSession();

The accessToken can now be used to make API calls to your backend.

Making a call

The following code can now be used to verify that a token is authentic on the backend using theaws-jwt-verify library (see here).

const verifier = CognitoJwtVerifier.create({
userPoolId: 'eu-west-1_TlRHxqHgs',
clientId: '3bho05m8t5ihern2eov5togtv2',
tokenUse: 'access',
});
const result = await verifier.verify(token);

An error is thrown if it fails, you can catch this and handle accordingly (e.g. return a 403 response code).

Conclusions

I used the AWS Amplify toolchain to get up and running. I suspect the CLI commands to spin up the Cloud Formation stack has created some resources I don’t really need; I see Identity Pools in there, and I’ll need to investigate further why these were created as I don’t think I need these for my simple use case (the backend will just be a monolith for now). So why use AWS Amplify at all?

The tool allowed me to get setup extremely fast — this exercise was completed in a few hours in an evening (including a couple of UI components and some extra routes in react-router). When I think about how much work is needed to build your own auth service with something like PassportJS this is definitely a good option if you just need to get something off the ground quickly, and probably a serious candidate beyond this too.

--

--

GarryPas

Software engineering consultant specialising in NodeJS, DotNET and Java. Primarily backend these days.