DEV Community

Cover image for How to use Amazon Cognito with React/TypeScript and Terraform
Masayoshi Haruta for AWS Community Builders

Posted on • Edited on

How to use Amazon Cognito with React/TypeScript and Terraform

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 2.6.0
Terraform CLI 1.1.0
react 18.2.0
typescript 4.6.2
react-router-dom 6.3.0
chakra-ui/react 2.2.4
aws-amplify 4.3.27

Sample Codes

Here are sample codes. 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 in README.md.

How to Set up

  1. Create Amazon Cognito
  2. Develop React App
  3. In conclusion

1. Create Amazon Cognito

⚠️ The steps require AWS Credential information. Please make sure your credential info has been set up.

Create Cognito

Create a Cognito User pool and its client app. I am using Terraform, so here is the documentation.

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



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
}


Enter fullscreen mode Exit fullscreen mode

Create user

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" 


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

※The User Pool Id can be confirmed from Management Console as below.
Cognito UserPoolId

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!

1. Install Library

Create a Project.



npx create-react-app app --template typescript


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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

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


Enter fullscreen mode Exit fullscreen mode

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

app/src/config/auth.ts



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",
};


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

The client ID can be viewed from the following page
Cognito App ID

If you have forgotten your UserPool ID, please refer to 2. Create user.

Now, Integrated App with Cognito has finished!

Next, prepare useAuth hooks that summarize the authentication process, context, and state.

app/src/hooks/useAuth.tsx



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,
    };
};


Enter fullscreen mode Exit fullscreen mode

app/src/components/PrivateRoute.tsx



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;



Enter fullscreen mode Exit fullscreen mode

Then, create the pages; top page, login page, and login success page.

app/src/pages/SignIn.tsx



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>
    );
}


Enter fullscreen mode Exit fullscreen mode

app/src/pages/Success.tsx



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>
    );
}


Enter fullscreen mode Exit fullscreen mode

The top page is contained with App.tsx.

app/src/App.tsx



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;


Enter fullscreen mode Exit fullscreen mode

Finally, I set the index.tsx including some providers.

app/src/index.tsx



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>
);


Enter fullscreen mode Exit fullscreen mode

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

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!

Top comments (1)

Collapse
 
therealgabryx profile image
Gabriele 'Gabryx' Concli • Edited

Great article! Not as many good Cognito login examples around for react apps, so very much thanks for sharing! Are you planning on writing the follow-up article you mentioned in the end? It could be very useful.