Cognito Authentication With React: A Small, Verifiable Setup
Authentication is not magic. It is configuration, tokens, and clear verification checks.
This post documents a minimal Cognito plus React setup. It avoids abstractions like the Amplify UI components to show exactly how the authentication flow works.
You will create a user pool, wire a frontend, and confirm the login process.
This is a lab guide. You should run these steps locally to verify the results.
The promise
By the end of this guide, you will:
- Create a Cognito user pool with email sign in.
- Configure a React application to communicate with AWS.
- Verify that a user can log in and that an ID token is stored in the browser.
What this is
High level: Amazon Cognito is a hosted identity service. It stores user accounts and issues JSON Web Tokens (JWTs) when users sign in.
Low level: You will create a "User Pool" to store accounts and an "App Client" to allow your React application to make public API calls to that pool.
Key terms:
- User pool: The directory of users.
- App client: The interface that allows your frontend to interact with the pool.
- ID token: A token containing claims about the identity of the authenticated user.
- Amplify Auth: The client library that manages the OAuth handshake and token storage.
What you need
- An active AWS account.
- Node.js version 18 or later.
- A terminal and code editor.
Start to Finish
Step 1: Create the user pool
Goal: Create a database for your users that allows sign in via email address.
Actions:
- Log in to the AWS Console.
- Navigate to Amazon Cognito.
- Select User pools and click Create user pool.
- Step 1: Configure sign-in experience
- Select Email under "Cognito user pool sign-in options".
- Click Next.
- Step 2: Configure security requirements
- For "MFA enforcement", select No MFA. (This simplifies the lab; enable MFA for production).
- Leave "User account recovery" as default.
- Click Next.
- Step 3: Configure sign-up experience
- Leave defaults.
- Click Next.
- Step 4: Configure message delivery
- Select Send email with Cognito.
- Click Next.
- Step 5: Integrate your app
- Enter a "User pool name" (e.g.,
react-demo-pool). - Important: Uncheck "Generate a client secret". Browser-based applications cannot secure a client secret.
- Click Next.
- Enter a "User pool name" (e.g.,
- Step 6: Review and create
- Review settings and click Create user pool.
Why: The user pool is the source of truth for identities. Configuring email sign-in reduces friction. Disabling the client secret is a strict requirement for frontend JavaScript applications.
Verify:
- Click on the name of your new user pool to open the details page.
- Copy the User Pool ID (e.g.,
us-east-1_XyZ123). Save this for Step 4. - Navigate to the Users tab.
- You should see an empty list and a button labeled "Create user". This confirms the pool is active.
If it fails:
- Symptom: Error creating pool regarding permissions.
- Fix: Ensure your AWS IAM user has
AmazonCognitoPowerUseror Administrator permissions.
Step 2: Create a user and app client
Goal: Create a test user for login and retrieve the Client ID required for the React app.
Actions:
- Create a User:
- Inside your User Pool, go to the Users tab.
- Click Create user.
- Enter an email address you control.
- Select "Send an email invitation" or "Mark email address as verified". For this lab, select Mark email address as verified and set a known password.
- Click Create user.
- Get Client ID:
- Go to the App integration tab.
- Scroll down to the App clients and analytics list.
- Click on the app client name created during setup (or create a new one if missed).
- Copy the Client ID (e.g.,
5m4n3o2p1q0r9s8t7u6v).
Why: You need a pre-existing user to test the login flow immediately without building a registration form. The Client ID is the public identifier your React app will use to introduce itself to AWS.
Verify:
- You have a User Pool ID string.
- You have a Client ID string.
- You have a valid username (email) and password ready to use.
If it fails:
- Symptom: You cannot find the Client ID.
- Fix: Navigate to the "App integration" tab of your User Pool dashboard.
Step 3: Create the React app
Goal: Initialize a clean React application and install the AWS Amplify library.
Actions:
- Open your terminal.
- Run the following commands:
npm create vite@latest cognito-demo -- --template reactcd cognito-demonpm install aws-amplify
- Open the project in your code editor.
Why:
Vite provides a minimal, fast build tool for React. The aws-amplify package contains the logic needed to sign requests and manage tokens. We avoid the amplify-cli to keep the project structure transparent.
Verify:
- Run
npm run dev. - Open the localhost URL provided.
- Confirm the default Vite splash screen loads.
If it fails:
- Symptom: Command not found.
- Fix: Ensure Node.js is installed by running
node -v.
Step 4: Configure Amplify
Goal: Connect the React application to your specific Cognito resources.
Actions:
- Create a new file
src/aws-exports.js. - Paste the following code, replacing the placeholders with your actual IDs from Step 1 and Step 2:
export const awsConfig = {Auth: {Cognito: {userPoolId: "YOUR_USER_POOL_ID", // e.g., us-east-1_xxxxxxuserPoolClientId: "YOUR_APP_CLIENT_ID", // e.g., 5xxxxxxxxx}}};
- Open
src/main.jsx. - Add the configuration logic before the React render:
import React from 'react';import ReactDOM from 'react-dom/client';import App from './App.jsx';import './index.css';import { Amplify } from 'aws-amplify';import { awsConfig } from './aws-exports';Amplify.configure(awsConfig);ReactDOM.createRoot(document.getElementById('root')).render(<React.StrictMode><App /></React.StrictMode>,);
Why: Amplify must be initialized with your AWS coordinates before it can function. Storing config in a separate file mimics production best practices.
Verify:
- Run
npm run dev. - Open the browser console (F12).
- Ensure there are no red errors regarding
AmplifyorAuth.
If it fails:
- Symptom:
TypeError: Cannot read properties of undefined. - Fix: Double check the object structure in
aws-exports.js. It must match{ Auth: { Cognito: { ... } } }.
Step 5: Add a basic sign in form
Goal: Implement the login logic using the Amplify Auth library.
Actions:
- Open
src/App.jsx. - Replace the entire file content with:
import { useState } from "react";import { signIn, signOut, getCurrentUser } from "aws-amplify/auth";export default function App() {const [email, setEmail] = useState("");const [password, setPassword] = useState("");const [status, setStatus] = useState("signed out");async function handleSignIn(e) {e.preventDefault();try {const { isSignedIn, nextStep } = await signIn({ username: email, password });if (isSignedIn) {const user = await getCurrentUser();setStatus(`signed in as ${user.username}`);} else {// Handle next steps like NEW_PASSWORD_REQUIREDsetStatus(`Next step: ${nextStep.signInStep}`);}} catch (error) {console.error('error signing in', error);setStatus(`Error: ${error.message}`);}}async function handleSignOut() {try {await signOut();setStatus("signed out");} catch (error) {console.error('error signing out', error);}}return (<div style={{ padding: 24, fontFamily: "system-ui" }}><h1>Cognito Verification</h1><p>Status: <strong>{status}</strong></p><form onSubmit={handleSignIn} style={{ display: 'flex', flexDirection: 'column', gap: 10, maxWidth: 300 }}><inputvalue={email}onChange={(e) => setEmail(e.target.value)}placeholder="Email"type="email"/><inputvalue={password}onChange={(e) => setPassword(e.target.value)}placeholder="Password"type="password"/><button type="submit">Sign in</button></form><br /><button onClick={handleSignOut}>Sign out</button></div>);}
Why:
This component demonstrates the raw signIn API call. It handles the happy path (successful login) and basic error states.
Verify:
- Enter the email and password of the test user created in Step 2.
- Click Sign in.
- Expected: The status text updates to
signed in as <user_id>.
If it fails:
- Symptom:
NewPasswordRequiredException. - Fix: If you created the user in the console, you may need to change the password on first login. The console user creation flow often sets the user to a "Force change password" state. For this lab, handle the error or create a user with a permanent password via the CLI.
Verify it worked
To confirm this is a real authentication session:
- Check the UI: The status should read "signed in".
- Check the Storage:
- Open Chrome DevTools.
- Go to the Application tab.
- Expand Local Storage.
- Look for keys starting with
CognitoIdentityServiceProvider. - You will see an
idTokenand anaccessToken.
- Check Persistence: Refresh the page. The tokens remain in Local Storage, meaning the user is still technically authenticated (though the simple React state in this demo will reset to "signed out" on refresh because we did not add a
useEffectto checkgetCurrentUseron load).
Common mistakes
-
Symptom:
NotAuthorizedException: Incorrect username or password.Cause: You are using the wrong region or the user is not in "Confirmed" status. Fix: Check the User Pool region and ensure the user status in the console is "Confirmed". -
Symptom:
Network Erroror CORS issues. Cause: TheuserPoolIdin your config does not match the region the app is running in, or the ID is typoed. Fix: Copy the ID directly from the AWS Console. -
Symptom:
Unable to verify secret hash for client. Cause: You enabled a "Client Secret" when creating the App Client. Fix: Delete the App Client and create a new one. Ensure "Generate client secret" is unchecked.
Cheat sheet
- User Pool: Create with default email settings.
- App Client: Create a public client, no secret.
- Project:
npm create vite@latest. - Deps:
npm install aws-amplify. - Config:
Amplify.configurewithuserPoolIdanduserPoolClientId. - Code:
signIn({ username, password }).
Next steps
This setup is the foundation. To make it production ready:
- Add a
useEffecthook to check for an existing session on app load. - Implement the "Forgot Password" flow.
- Add React Router to protect specific routes based on auth status.
- Move credentials to
.envfiles (though Client IDs are public, it is good practice).