Loading...
react aws cognito terraform

React <> Cognito

CZ
Carlos Zuniga Published on November 5, 2021
Find the full source code for this post in Xtages/poc-react-cognito.

At Xtages, we use Amazon Cognito to manage our users and their authentication.

Cognito collects a user’s attributes, it enables simple, secure user authentication, authorization and user management for web and mobile apps.

Overview of Cognito

In this section, we’ll take a 5000 feet view of how Cognito integrates with a web app.

Cognito sign up and log in sequence diagram

Cognito requires users to verify their email or phone number to enable password recovery flow. The way that verification works is by sending a numeric code to the user’s email or phone number (using SMS) and the user the app then calling CognitoAuth.confirmSignUp like step #6 of the diagram above.

Next, we’ll see how to:

  • Create a new React webapp using create-react-app
  • Add a useAuth hook and <AuthProvider> Context
  • Create authenticated and unauthenticated Routes
  • Provision a Cognito user pool using Terraform

Integration with React

We can use Cognito to secure certain sections of our React app, making sure that only authenticated users have access to them.

Setup

We’ll start by creating a new React web app using create-react-app:

npx create-react-app demo-app --template typescript

useAuth hook

The core piece of this integration is the useAuth hook which is based on https://usehooks.com/useAuth/ and adapted to use Cognito’s API.

After installing the Cognito API package from Amazon, and the use-async-effect package

npm install --save @aws-amplify/auth use-async-effect

we’ll go ahead and create a new file for our useAuth hook under src/hooks/ and we’ll call it useAuth.tsx.

Next, we’ll configure the Cognito Auth object with the AWS Region, Cognito User Pool Id and Cognito Web Client Id. You can see these values are being included from environment variables, so it’s easy to configure our app depending on where it runs (local development, continuous integration, staging, production, etc.).

Now let’s create some types for our hook’s API:

The User type has the properties we expect for a user. The User.country property is an example of a Cognito custom attribute .

The Credentials type will be used in our logIn and signUp functions.

Notably the CognitoUserWithChallenge is a bit of a crutch that we need, to make the Typescript compiler happy. The object returned by the CognitoAuth.signIn function (from the amazon-cognito-identity-js package) can contain a property called challengeName however in the typings for the CognitoAuth.signIn that property is not present therefore we have to supplement the type in our code. The challengeName property is used to convey that the user needs to respond to a challenge to verify their identity. So for example when the user signs up they are emailed a code that they must input as a challenge response. For more information see https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-authentication-flow.html

Finally, we’ll get to the implementation of our hook. In reality, we have a private hook useProvideAuth and a Context and all are exposed through the public useAuth hook.

First, we’ll dissect the useProvideAuth private hook which has the bulk of the code:

The useProvideAuth hook has three public functions:

  • logIn: wraps CognitoAuth.signIn and if the result of the call has a challengeName returns that, otherwise converts the returned CognitoUser sets the user state and also toggles the authInProgress state to false.
  • logOut: sets the user state to null and calls CognitoAuth.signOut to invalidate the user’s token.
  • signUp: calls CognitoAuth.signUp with the required user attributes plus their password.

A couple of other interesting points about this hook is the use of useAsyncEffect (from the use-async-effect package) which has the same semantics of plain useEffect but allows for using async functions. We need this hook because we determine, on render, if the user is logged in by calling the getUser function which in itself is async. We also set a listener through the AWS Hub class which will notify us of different auth-related events that have occurred.

Now that our hook is ready, we’ll go ahead and set up a Context so our app can take advantage of this infrastructure:

In the snippet above, we create a Context of type Auth | null, where Auth is ReturnType<typeof useProvideAuth>, we also created a tiny component <AuthProvider> which passes an instance of Auth to the <AuthContext.Provider>. Finally useAuth wraps useContext(AuthContext) and ensures that the returned Auth is never null.

Usage

After having the auth infrastructure in place, we now need to start using in our app, the first step is to wrap our component hierarchy in an <AuthProvider>, like such:

You’ll notice a few points in the snippet above:

  • we are using react-router-dom for our navigation.
  • we have wrapped our app in the <AuthProvider> that we just created, which means that the useAuth hook is now usable to all our components.
  • we have a couple of utility route components that are not part of react-router-dom, namely <UnauthenticatedOnlyRoute> and <AuthenticatedRoute>.

Secure routing

Below, <AuthenticatedRoute> is using useAuth to determine if the authentication is in progress (and if so display an empty page), if we have an authenticated user we then route to the specified component and if we don’t have an authenticated user then we redirect to the /login page, preserving the location in the state, so we can redirect to it once the user successfully logs in.

<UnauthenticatedOnlyRoute> is basically a mirror of <AuthenticatedRoute> in that it will only render the Route if the user is not authenticated, redirecting to / otherwise.

Login page

The following is a very simple log-in page, for illustration purposes only, using the useAuth hook, although you’ll probably want to use a proper react form library and maybe some kind of UI library too.

Using Terraform to provision Cognito

The following Terraform module was used to provision the Cognito user pool used in this blog post:

Some interesting sections of this module:

  • Lines 24-34: The password_policy block is where we configure the requirements of a valid password. As a tip for better UX, make sure that your signup page form, validates the user’s password client-side using the same requirements. You can also leverage a package like react-password-strength-bar to nudge the user to create strong passwords.
  • Line 37-73: The schema block is where we define attributes for the user’s profile. Custom attributes must use the custom: prefix on their names.
  • Line 80: The username_attributes array indicates that we are using the email attribute as the username for our users.

The aws_cognito_user_pool_client resource declares a web client for our user pool, which is, you guessed it, our React app. This is where we configure the validity periods for the tokens returned by Cognito in lines 104-111. Line 119, generate_secret = false ensures that a secret is not generated for this client which is necessary because the browser Cognito js library doesn’t support secrets.

Lines 123-134 indicate which attributes, as defined in the user pool itself, are readable and/or writable by the web client.

Conclusion

AWS Cognito is a good option when it comes to user authentication and management. It’s specially useful when you are already bought into the AWS ecosystem and it’s also cheaper than some other alternatives.

It’s not too difficult to integrate React and Cognito, however Cognito’s reference documentation for their Javascript/Typescript package lacks some in-depth detail, they also have several seemingly overlapping packages (amplify, @aws-amplify/auth and amazon-cognito-identity-js) that make it hard to pinpoint exactly which one is necessary for Cognito to work. Amazon also tends to push Amplify as a whole for the auth solution when it’s not really necessary to add all that code to your app.

At Xtages we are building an all-in-one solution for CI/CD and hosting of apps, all with minimal configuration and no infrastructure to manage. We recently introduced a free plan (no credit card required) to make it easier to get started.