How to use Amazon Cognito with React/TypeScript and Terraform
Tags: aws, react, typescript, terraform
Published: 2022-07-17 09:40:00

## Introduction
I recently created a login page in React/TypeScript that was surprisingly easy to implement using Amazon Cognito, so I wanted to share it with you. As a sample app, the demo is intended to be really simple, so I think this tutorial can be done in 15~30 minutes.

I would be very happy if Cognito could be used as a secure and easy to use AWS service for modern front-end development and so on.

### Prerequisite
* Amazon Cognito is created with AWS CLI and Terraform.
* Demo App is developed in React/TypeScript, and Chakra UI

Details will be as follows, please set up if necessary.

| name | version |
|:-----|:-----|
|[AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)|2.6.0|
|[Terraform CLI](https://learn.hashicorp.com/tutorials/terraform/install-cli)|1.1.0|
|[react](https://github.com/facebook/react)|18.2.0| 
|[typescript](https://github.com/microsoft/TypeScript)|4.6.2| 
|[react-router-dom](https://github.com/remix-run/react-router)|6.3.0| 
|[chakra-ui/react](https://github.com/chakra-ui/chakra-ui)|2.2.4| 
|[aws-amplify](https://github.com/aws-amplify/amplify-js)|4.3.27| 

## Sample Codes
Here are [sample codes.](https://github.com/paprika-mah/DemoCognitoWithReact) I also wrote an example in the blog, but it would be too long to write everything, so I have abbreviated some of the information. 

If you want to see the full codes, and run the demo, please refer to this GitHub repository.
Also, If you would like to try it out first, please refer to [Quick Setup](https://github.com/paprika-mah/Demo-Cognito-React-Userpool-Login#quick-setup) in README.md.



## How to Set up
1. Create Amazon Cognito
  * [Create Cognito](#chap1)
  * [Create user](#chap2)
1. Develop React App
  * [Install libraries](#chap3)
  * [Develop login UI](#chap4)
1. [In conclusion](#chap5)

---
## 1. Create Amazon Cognito
⚠️ The steps require [AWS Credential information](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html
). Please make sure your credential info has been set up. 


### Create Cognito <a name="chap1"></a>
Create a Cognito User pool and its client app. I am using Terraform, so here is the documentation.
* [Resource: aws_cognito_user_pool](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user_pool)
* [Resource: aws_cognito_user_pool_client](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user_pool_client)

In this case, the setup is simple because the user pool is used for login. The Terraform codes have only a few lines(※The below is full codes, not snippets). I think Cognito is so easy to set up and help developer reduce the burden of developing.

#### infra/main.tf
```json
resource "aws_cognito_user_pool" "pool" {
  name = "congnito-sample-user-pool"
}

resource "aws_cognito_user_pool_client" "client" {
  name          = "cognito-sample-user-pool-app-client"
  user_pool_id  = aws_cognito_user_pool.pool.id
}
```


### Create user<a name="chap2"></a>
Next, create a simple User for testing. Please refer to the following AWS CLI command. 
⚠️ Please don't forget to **TYPE YOUR USERPOOL ID** before running these commands.

#### Create a user
```
aws cognito-idp admin-create-user  --user-pool-id "{Please type your userpool id}"  --username "test-user-paprika" 
```

#### Setting a password
```
aws cognito-idp admin-set-user-password --user-pool-id "{Please type your userpool id}" --username "test-user-paprika" --password 'Password1234#' --permanent
```

※The User Pool Id can be confirmed from Management Console as below.
![Cognito UserPoolId](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/41mn37vgu8kfedx9etf2.png)

Also, confirm that the user information is displayed as shown above. If the Confirmation Status is set to "CONFIRMED", the password has been registered. Please make sure that the Status is set to "Enabled" just to be sure.


Then, completes the setup! Let's implement an application to use it.

--------------------------
## 2. Develop React App
Again note that only the important parts of the code are listed here as snippets.
If you want to see the all codes, please see [the GitHub Repository!](https://github.com/paprika-mah/DemoCognitoWithReact)

### 1. Install Library<a name="chap2"></a>
Create a Project.
```
npx create-react-app app --template typescript
```

After changing the directory, (running `cd app`), install the below libraries.
```
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion @chakra-ui/icons
npm install react-router-dom
npm install --save-dev @types/react-router-dom
npm install aws-amplify
```

Then, unnecessary files created by create-react-app, such as logo.svg, are not used, so it may be a good idea to delete them if you want.

### 2. Develop Login UI<a name="chap2"></a>
Then, let's start coding! The following is the directory structure, so I will mainly create files under `src`.

```
.
├── .env
├── .gitignore
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.tsx
│   ├── components
│   │   └── PrivateRoute.tsx
│   ├── config
│   │   └── auth.ts
│   ├── hooks
│   │   └── useAuth.tsx
│   ├── index.tsx
│   └── pages
│       ├── SignIn.tsx
│       └── Success.tsx
└── tsconfig.json
```

First, I will create a config file to use Cognito.

#### app/src/config/auth.ts
```typescript
export const AwsConfigAuth = {
    region: process.env.REACT_APP_AUTH_REGION,
    userPoolId: process.env.REACT_APP_AUTH_USER_POOL_ID,
    userPoolWebClientId: process.env.REACT_APP_AUTH_USER_POOL_WEB_CLIENT_ID,
    cookieStorage: {
        domain: process.env.REACT_APP_AUTH_COOKIE_STORAGE_DOMAIN,
        path: "/",
        expires: 365,
        sameSite: "strict",
        secure: true,
    },
    authenticationFlowType: "USER_SRP_AUTH",
};
```

To switch environment variables, add a `.env.local` file as below.  
⚠️ Please don't forget to type YOUR COGNITO USERPOOL INFORMATION.

#### app/.env.local
```
REACT_APP_AUTH_REGION={Please type aws region you want to use}
REACT_APP_AUTH_USER_POOL_ID={Please type your user id}
REACT_APP_AUTH_USER_POOL_WEB_CLIENT_ID={Please type your client id}
REACT_APP_AUTH_COOKIE_STORAGE_DOMAIN=localhost
```
The client ID can be viewed from the following page
![Cognito App ID](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ds3q1xfm6v7x5dsy958e.png)

If you have forgotten your UserPool ID, please refer to [2. Create user.](#chap2)

Now, Integrated App with Cognito has finished!  
Next, prepare useAuth hooks that summarize the authentication process, context, and state. 

#### app/src/hooks/useAuth.tsx
```typescript
import Amplify, { Auth } from "aws-amplify";
import React, { createContext, useContext, useEffect, useState } from "react";
import { AwsConfigAuth } from "../config/auth";

Amplify.configure({ Auth: AwsConfigAuth });

interface UseAuth {
    isLoading: boolean;
    isAuthenticated: boolean;
    username: string;
    signIn: (username: string, password: string) => Promise<Result>;
    signOut: () => void;
}

interface Result {
    success: boolean;
    message: string;
}

type Props = {
    children?: React.ReactNode;
};

const authContext = createContext({} as UseAuth);

export const ProvideAuth: React.FC<Props> = ({ children }) => {
    const auth = useProvideAuth();
    return <authContext.Provider value={auth}>{children}</authContext.Provider>;
};

export const useAuth = () => {
    return useContext(authContext);
};

const useProvideAuth = (): UseAuth => {
    const [isLoading, setIsLoading] = useState(true);
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [username, setUsername] = useState("");

    useEffect(() => {
        Auth.currentAuthenticatedUser()
            .then((result) => {
                setUsername(result.username);
                setIsAuthenticated(true);
                setIsLoading(false);
            })
            .catch(() => {
                setUsername("");
                setIsAuthenticated(false);
                setIsLoading(false);
            });
    }, []);

    const signIn = async (username: string, password: string) => {
        try {
            const result = await Auth.signIn(username, password);
            setUsername(result.username);
            setIsAuthenticated(true);
            return { success: true, message: "" };
        } catch (error) {
            return {
                success: false,
                message: "LOGIN FAIL",
            };
        }
    };

    const signOut = async () => {
        try {
            await Auth.signOut();
            setUsername("");
            setIsAuthenticated(false);
            return { success: true, message: "" };
        } catch (error) {
            return {
                success: false,
                message: "LOGOUT FAIL",
            };
        }
    };

    return {
        isLoading,
        isAuthenticated,
        username,
        signIn,
        signOut,
    };
};
```

#### app/src/components/PrivateRoute.tsx
```typescript
import { Navigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";

type Props = {
    children?: React.ReactNode;
};

const PrivateRoute: React.FC<Props> = ({ children }) => {
    const { isAuthenticated } = useAuth();
    return isAuthenticated ? <>{children}</> : <Navigate to="/signin" />;
};

export default PrivateRoute;

```

Then, create the pages; top page, login page, and login success page.
#### app/src/pages/SignIn.tsx
```typescript
export function SignIn() {
    const auth = useAuth();
    const navigate = useNavigate();
    const [username, setUsername] = useState("");
    const [password, setPassword] = useState("");

    const executeSignIn = async (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const result = await auth.signIn(username, password);
        if (result.success) {
            navigate({ pathname: "/success" });
        } else {
            alert(result.message);
        }
    };

    return (
        <Flex justify={"center"}>
            <VStack h={500} justify="center">
                <form noValidate onSubmit={executeSignIn}>
                    <Box>
                        <FormLabel htmlFor="username">User Name</FormLabel>
                        <Spacer height="10px" />
                        <Input
                            type="text"
                            placeholder="UserID"
                            value={username}
                            onChange={(e) => setUsername(e.target.value)}
                            size="lg"
                        />
                    </Box>
                    <Spacer height="20px" />
                    <FormLabel htmlFor="password">Password</FormLabel>
                    <Input
                        type="password"
                        placeholder="password"
                        value={password}
                        onChange={(e) => setPassword(e.target.value)}
                        size="lg"
                    />
                    <Spacer height="35px" />
                    <Stack align="center">
                        <Button type="submit" colorScheme="teal" size="lg">
                            Login
                        </Button>
                    </Stack>
                </form>
            </VStack>
        </Flex>
    );
}
```

#### app/src/pages/Success.tsx
```typescript
export function SuccessPage() {
    const auth = useAuth();

    if (auth.isLoading) {
        return <Box />;
    }

    return (
        <PrivateRoute>
            <VStack h={500} justify="center" spacing={8}>
                <Text fontSize="5xl">Welcome {auth.username}!!</Text>
                <Text fontSize="4xl">Login Succeed🎉</Text>
                <Button
                    colorScheme="teal"
                    size="lg"
                    onClick={() => auth.signOut()}
                >
                    Log out
                </Button>
            </VStack>
        </PrivateRoute>
    );
}
```

The top page is contained with App.tsx.  
#### app/src/App.tsx
```typescript
function App() {
    const auth = useAuth();

    if (auth.isLoading) {
        return <Box />;
    }

    const TopPage = () => (
        <Flex justify={"center"}>
            <VStack h={500} justify="center" spacing={8}>
                <Text fontSize="5xl">Cognito Test</Text>
                <Text fontSize={"3xl"}>
                    {auth.isAuthenticated
                        ? "STATUS: LOGIN"
                        : "STATUS: NOT LOGIN"}
                </Text>
                <Link to="/signin">
                    <Text fontSize={"2xl"}>
                        Go to LoginPage(Click Here){" "}
                        <ExternalLinkIcon mx="4px" />
                    </Text>
                </Link>
            </VStack>
        </Flex>
    );

    return (
        <BrowserRouter>
            <Routes>
                <Route index element={<TopPage />} />
                <Route path="signin" element={<SignIn />} />
                <Route path="success" element={<SuccessPage />}></Route>
                <Route path="*" element={<p>Page Not Found</p>} />
            </Routes>
        </BrowserRouter>
    );
}

export default App;
```

Finally, I set the index.tsx including some providers.
#### app/src/index.tsx
```typescript
import App from "./App";
import { ProvideAuth } from "./hooks/useAuth";

import * as React from "react";
import ReactDOM from "react-dom/client";
import { ChakraProvider } from "@chakra-ui/react";

const root = ReactDOM.createRoot(
    document.getElementById("root") as HTMLElement
);
root.render(
    <React.StrictMode>
        <ChakraProvider>
            <ProvideAuth>
                <App />
            </ProvideAuth>
        </ChakraProvider>
    </React.StrictMode>
);
```
---
### In conclusion
Congrats🎉 You've finished developing the Login Page with React and Cognito! Please go to the login page and touch the login demo! 
![Top Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8g6lr2l0pg6eknxobj50.png)

**It is amazing how easy it was to create a demo application.**
Actually, this blog is focused on simplicity, and Cognito, in particular, requires a lot more configuration when considered for production deployment. You need to prepare a new user registration page, and you need to monitor quotas, and so on.

Also there are many good features, such as using SSO with SAML to make it more convenient, or implementing a login implementation with more secure authentication methods than what we have now.

If there is a response, I would like to write a follow-up on these points!
Thank you for reading!