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
- Create Amazon Cognito
- Develop React App
- 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
}
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"
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.
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
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
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
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
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,
};
};
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;
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>
);
}
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>
);
}
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;
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>
);
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!
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)
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.