Cognito Authentication With React: A Small, Verifiable Setup
Authentication is not magic. It is configuration, tokens, and clear verification checks. This is the minimal AWS Cognito plus React setup I run when I need to understand the flow.
It avoids pre-built Amplify UI components so I can see exactly what is happening.
The key pieces this setup covers:
- User pool configuration with email sign-in and no client secret.
- App client creation so your React frontend can authenticate against the pool.
- Amplify wiring to connect the React app to Cognito using your User Pool ID and Client ID.
- Token verification to confirm the ID token (a JWT) is returned correctly after sign-in.
How do I set up AWS Cognito authentication with React?
To set up Cognito authentication with React, create a user pool in the AWS Console with email sign-in and no client secret, create a test user and app client, then install aws-amplify in your React app. Configure Amplify with your User Pool ID and Client ID in an aws-exports file, and wire it into your main entry point. Verify the config before adding UI.
Here is the end-to-end flow at a glance:
- Create a Cognito user pool with email sign-in and no client secret.
- Create a test user and copy the App Client ID.
- Scaffold a React app with Vite and install
aws-amplify. - Configure Amplify with your User Pool ID and Client ID.
- Verify the config loads without console errors.
- Add a sign-in form that calls
Amplify.signInand inspect the returned ID token.
Do I need a client secret for Cognito with a browser app?
No, you should uncheck the client secret option when creating a Cognito app client for a browser-based React app. Browser apps cannot keep secrets because everything in client-side JavaScript is visible to users. If you accidentally generate a client secret, create a new app client without one. This is one of the most common mistakes when configuring Cognito for frontend apps. See the Cognito app client documentation for details on client secret behavior.
What are the common mistakes when configuring Cognito with React?
The most common Cognito mistakes are forgetting to uncheck the client secret, using the wrong region in the User Pool ID, and skipping the test user to build sign-up first. Always create a user manually, verify that login works end to end, then add registration. Also confirm your Amplify config loads without console errors before building any sign-in UI on top of it.
A quick checklist to avoid these mistakes:
- Client secret is unchecked when creating the app client.
- Region in the User Pool ID matches the region in your Amplify config.
- A test user is created and login is verified before building sign-up.
Amplify.configureruns with no console errors before adding UI.
The actual authentication flow
The full flow has four stages, and it helps to understand each one before you write any UI:
- Signup. The user submits an email and password. Cognito creates the user in the pool with a
UNCONFIRMEDstatus. No tokens are issued yet. - Confirmation. Cognito sends a verification code to the user's email. The user submits the code, and Cognito marks the account
CONFIRMED. This is the step people forget when they build signup first and wonder why login fails. - Login. The user submits credentials. Cognito validates them and returns three tokens: an ID token (user identity claims), an access token (authorization for APIs), and a refresh token (used to get new tokens without re-entering the password).
- Token refresh. The ID and access tokens expire (default is 1 hour). The refresh token (default 30 days) is used silently to get new tokens. Amplify handles this automatically if you let it, but you need to understand it because it affects how you manage session state.
Here is what the key calls look like in Amplify v6:
import { signIn, signUp, confirmSignUp, fetchAuthSession } from 'aws-amplify/auth';await signUp({username: email,password,options: { userAttributes: { email } }});await confirmSignUp({ username: email, confirmationCode });const { isSignedIn, nextStep } = await signIn({ username: email, password });const session = await fetchAuthSession();const idToken = session.tokens?.idToken?.toString();
The idToken is a JWT you can decode at jwt.io to inspect the claims. That is how you verify the flow worked end to end.
The moving parts
- User pool: the directory of users. (Cognito user pools docs)
- App client: the public interface that lets your frontend talk to the pool. (App client docs)
- ID token: a JWT containing claims about the authenticated user. (ID token docs)
- Amplify Auth: the client library that manages sign-in and token storage. (Amplify Auth docs)
Cognito component comparison
| Component | Purpose | Where it lives | Who manages it |
|---|---|---|---|
| User pool | Stores user accounts and credentials | AWS region | You (via AWS Console or IaC) |
| App client | Identifies your frontend app to the pool | Inside the user pool | You |
| ID token | Carries user identity claims to your app | Returned to the browser after sign-in | Cognito issues it; your app consumes it |
| Amplify Auth | Handles sign-in calls and token storage | Client-side (npm package) | You (via npm) |
Step 1: create the user pool
In the AWS Console, go to Amazon Cognito and create a user pool:
- Select Email as the sign-in option.
- Set No MFA for the lab. Enable MFA later for real apps. (MFA docs)
- Leave sign-up defaults.
- Select Send email with Cognito for message delivery. (Email docs)
- Name the pool, for example
react-demo-pool. - Uncheck "Generate a client secret." Browser apps cannot keep secrets.
- Create the pool.
Save the User Pool ID. You will need it in Step 4.
Step 2: create a user and app client
Inside the pool:
- Go to the Users tab and create a user. Mark the email verified and set a known password.
- Go to the App integration tab and copy the Client ID.
You now have a User Pool ID, a Client ID, and a test user.
Step 3: create the React app
I use Vite to scaffold the project because it is fast and requires zero configuration:
npm create vite@latest cognito-demo -- --template reactcd cognito-demonpm install aws-amplify
Run npm run dev and confirm the default Vite screen loads.
Step 4: configure Amplify
Create src/aws-exports.js:
export const awsConfig = {Auth: {Cognito: {userPoolId: "YOUR_USER_POOL_ID",userPoolClientId: "YOUR_APP_CLIENT_ID",}}};
Then wire it into src/main.jsx:
import { Amplify } from 'aws-amplify';import { awsConfig } from './aws-exports';Amplify.configure(awsConfig);
At this point, the app can talk to Cognito. The next step is a sign-in form that calls Amplify.signIn (Amplify signIn docs). I usually verify the config first by checking the console for errors before adding UI.
To verify the config is correct:
- Open the browser console after
Amplify.configureruns. - Confirm there are no errors about missing region or invalid User Pool ID.
- Check that the Amplify version installed matches the v6 config shape shown above. (Amplify v6 migration guide)
Common mistakes I have made
- Forgetting to uncheck the client secret. Browser apps cannot use secrets. (Client secret docs)
- Using the wrong region in the User Pool ID.
- Skipping the test user and trying to build sign-up first. Create a user manually, verify login works, then add registration.
Pitfalls and security considerations
A few things that bite people once they get past the basic flow:
Token storage. Amplify v6 stores tokens in localStorage by default. That makes them accessible to any JavaScript running on your page, including third-party scripts. For most small apps this is an acceptable tradeoff, but if you handle sensitive data, consider using a custom storage adapter or moving to a backend-for-frontend pattern where tokens never touch the browser.
Refresh handling. Amplify refreshes tokens automatically, but only if the app is open and making calls. If a user leaves the app idle past the refresh token expiry, they get silently logged out. If your UI does not handle the signed-out state gracefully, users see a blank screen or stale data. Always check fetchAuthSession() on app load and redirect to login if it throws.
MFA setup. The lab setup uses No MFA. For a real app, enable TOTP-based MFA (not SMS). SMS MFA has SIM-swap risk and per-message costs. TOTP uses an authenticator app like Google Authenticator or 1Password. Configure it at the user pool level, then handle the MFA_SETUP and SMS_MFA / TOTP challenge steps in your signIn response.
OAuth flows and PKCE. If you use Cognito's hosted UI (the OAuth endpoint) instead of the Amplify SDK, use the Authorization Code flow with PKCE, never the Implicit flow. PKCE prevents authorization code interception in browser apps. The Amplify SDK handles this internally if you use signInWithRedirect, but if you build the OAuth URL yourself, you must generate and verify the PKCE challenge manually.
Token expiration. The ID token's exp claim is in seconds. If you pass it to a backend API, your backend must validate exp and reject expired tokens. Do not trust the token just because it exists in the request.
How Cognito compares to alternatives
Cognito is not the only option. Here is how it stacks up against the common alternatives for a React app:
- Auth0 — the most polished developer experience. Hosted login pages, social logins, and enterprise SSO work out of the box. The tradeoff is cost: the free tier covers 7,500 active users, but paid plans start at $35/month and scale with users. If you want to move fast and do not mind paying, Auth0 is the easiest.
- Firebase Auth — Google's offering. Free for most small apps (phone auth has per-use costs). Good if you are already in the Firebase ecosystem. The tradeoff is vendor lock-in to Google Cloud and a different token format that does not play as cleanly with AWS backends.
- Supabase Auth — built on PostgreSQL with row-level security. If you want your auth and database in one place, it is a strong choice. The free tier is generous. The tradeoff is a smaller ecosystem and less enterprise feature maturity than Cognito or Auth0.
- Cognito — the cheapest at scale if you are already on AWS. The free tier covers 50,000 monthly active users. The tradeoff is the developer experience: the console is clunky, the docs are scattered, and you do more wiring yourself. If you are building on AWS anyway, it is the natural choice.
For a small React app on AWS, Cognito is what I reach for. If I am not on AWS, I reach for Auth0 or Supabase depending on the stack.
Closing
Cognito is not complicated once you separate the pieces: the pool holds users, the client identifies your app, and Amplify handles the token exchange. Verify each piece before adding the next.
References
- AWS Cognito — developer guide
- Cognito user pools
- Cognito app clients
- Cognito MFA settings
- Cognito email verification
- Cognito ID tokens
- AWS Amplify — documentation
- Amplify Auth signIn
- Amplify v6 upgrade guide
- Amplify UI components
- React — official documentation
- Vite — getting started
- JWT introduction
