Build a Login System with Amazon Cognito

Set up secure user authentication with Amazon Cognito and React.

Introduction

30 Second Summary

Every app you use requires you to log in. Behind that simple sign-in form is a complex system handling passwords, email verification, and security tokens.

In this project, you will build a secure authentication system using Amazon Cognito and a React web app. You'll configure user sign-up, sign-in, and email verification, then display the identity tokens that prove a user is who they claim to be.

What You'll Build

A secure login system powered by Amazon Cognito that handles user registration, email verification, and sign-in through a React single-page application.

By the end of this project, you'll have:

  • An Amazon Cognito user pool with managed login configured for sign-up, sign-in, and password reset.
  • A React app integrated with AWS Amplify that handles the full authentication flow using the OAuth 2.0 authorization code grant.
  • A dashboard that displays decoded JWT token claims showing how identity information travels between Cognito and your app.
  • Secret Mission: Add multi-factor authentication (MFA) with TOTP so users need an authenticator app to sign in.

Are there any prerequisites?

You need an AWS account (free tier eligible). No prior AWS experience is required. The project walks you through every console step.

Create a Cognito User Pool

Your development environment is ready. Now it is time to create the backbone of your authentication system.

A user pool is a user directory in Amazon Cognito. It stores user accounts and handles sign-up, sign-in, and password recovery. In this step, you will create a user pool and configure it to accept email-based sign-in with automatic email verification.

In this step, get ready to:
  • Create a Cognito user pool with email sign-in and self-registration enabled.
  • Confirm the Essentials tier is selected for managed login support.
  • Set up a Cognito domain prefix for your managed login pages.
Navigate to Amazon Cognito and start creating a user pool

Every authentication system starts with a place to store users. Amazon Cognito provides this through user pools, which handle the full lifecycle of user accounts.

  • Return to the AWS Management Console in your browser.
  • In the search bar at the top, type Cognito and select Amazon Cognito from the results.
  • In the left navigation pane, click User Pools.
  • Click Create user pool.

What is a user pool?

A user pool is a user directory that manages sign-up, sign-in, and account recovery for your application. It stores usernames, passwords, and user attributes like email addresses. Think of it as a managed database specifically built for authentication.

Configure sign-in options and security settings

The user pool creation wizard walks you through several configuration pages. You will set email as the sign-in option, enable self-registration, and require email verification.

  • On the Configure sign-in experience page, under Cognito user pool sign-in options, select Email.
  • Leave User name requirements at the default (case insensitive).
  • Click Next.
  • On the Configure security requirements page, leave the password policy at the Cognito defaults.
  • Under Multi-factor authentication, select No MFA.
  • Click Next.
  • On the Configure sign-up experience page, confirm that Enable self-registration is checked.
  • Under Cognito-assisted verification and confirmation, confirm that Allow Cognito to automatically send messages to verify and confirm is selected.
  • Under Attributes to verify, select Send email message, verify email address.
  • Under Required attributes, confirm that email is listed.
  • Click Next.

Why require email verification?

Email verification proves that a user actually owns the email address they signed up with. Without it, anyone could sign up with someone else's email. Cognito sends a verification code to the email address, and the user must enter it before their account is confirmed.

Select the Essentials tier and configure the domain

The Essentials tier is the default for new user pools. It includes managed login pages that handle sign-up, sign-in, and password reset without you writing any UI code. Managed login requires a domain to serve these pages.

  • On the Configure message delivery page, select Send email with Amazon Cognito (this is the free default option for development).
  • Click Next.
  • On the Integrate your app page, enter a User pool name (e.g., cognito-login-pool).
  • Under Domain, select Use a Cognito domain.
  • Enter a unique domain prefix in the Cognito domain field (e.g., my-login-app). This must be globally unique across all AWS accounts.
  • Under Feature plan, confirm that Essentials is selected (it is the default).
  • Skip the Initial app client section for now. You will configure the app client in the next step with specific settings.
  • Click Next.
  • On the Review and create page, review your settings and click Create user pool.

What is the Essentials tier?

Amazon Cognito offers three pricing tiers: Lite, Essentials, and Plus. The Essentials tier is the default for new user pools. It includes managed login pages, passwordless sign-in options, and token customization.

The free tier covers 10,000 monthly active users at no cost. For this project, you will stay well within that limit.

Why does managed login need a domain?

Managed login pages are hosted web pages that Cognito serves on your behalf. They need a URL to be reachable on the internet. The Cognito domain prefix you chose creates a URL like my-login-app.auth.us-east-1.amazoncognito.com where your sign-in pages live.

After creation completes, you will be taken to the user pool details page.

  • On the user pool details page, locate and note down the User Pool ID (it looks like us-east-1_AbCdEfGhI). You will need this in a later step.

Domain prefix already taken?

Cognito domain prefixes must be globally unique. If you see an error that your prefix is already in use, try adding your initials or a random number to the prefix (e.g., my-login-app-jd42).

Having trouble setting up your user pool? Help me troubleshoot my Cognito user pool creation.

Your user pool is live and ready to store users. Next up, you will configure an app client and verify that the managed login pages are working.

Configure the App Client and Managed Login

Your user pool is set up and ready to authenticate users. But for your React app to communicate with Cognito, it needs an app client. An app client is like a registered ID card that tells Cognito which application is making authentication requests.

In this step, you will create a public app client, configure it with the OAuth 2.0 authorization code flow, and verify that the managed login pages are working. You will also collect the configuration values your React app needs later.

In this step, get ready to:
  • Create a public app client with the authorization code grant flow.
  • Verify the managed login page is live and working.
  • Note down the User Pool ID, App Client ID, and Cognito domain.
Create the public app client

An app client tells Amazon Cognito which application is allowed to initiate sign-in requests. Since your React app runs entirely in the browser, it needs a public client (one without a secret key).

  • In the AWS Management Console, make sure you are on the Amazon Cognito service page. If you navigated away, search for Cognito in the top search bar and select Amazon Cognito.
  • Click User Pools in the left sidebar.
  • Select your user pool from the list.
  • In your user pool, select the App clients menu under Applications in the left navigation.
  • Click Create app client.
  • For Application type, select Single-page application. This creates a public client without a client secret, which is what browser-based apps require.
  • For the app name, enter cognito-login.
  • For the Return URL (also called the callback URL), enter http://localhost:5173/.
  • Click Create app client.

What is a public app client?

A public client runs in a browser or on a mobile device where you cannot safely store a secret key. Cognito identifies your app by its client ID alone.

The authorization code grant flow means your app receives a short-lived code after login, then exchanges it for tokens. This is more secure than receiving tokens directly in the URL.

Why the trailing slash matters

The callback URL must exactly match what your app sends. http://localhost:5173/ (with trailing slash) and http://localhost:5173 (without) are treated as different URLs by Cognito. Use the version with the trailing slash to match Vite's default.

After creating the app client, Cognito takes you to the app client details page.

  • On the app client details page, scroll down to the Login pages tab.
  • Confirm that you see http://localhost:5173/ listed under Allowed callback URLs.

Don't see Login pages tab?

Make sure you are viewing the specific app client you just created, not the user pool overview. Select App clients from the left sidebar, then click on your app client name to open its details.

help me find the Login pages tab for my Cognito app client

Verify the managed login page

Cognito automatically generates a hosted sign-in page for your app client. This managed login page handles sign-up, sign-in, and password reset without you writing any UI code for those flows.

  • On the app client details page, select the Login pages tab.
  • Click the View login pages button.
  • A new browser tab opens showing Cognito's managed login page with a sign-in form.

You should see a branded sign-in page with fields for email and password, plus a link to create an account. This confirms that your user pool, domain, and app client are all connected correctly.

What is managed login?

Managed login provides self-contained web pages for sign-up, sign-in, MFA, and password reset. Cognito hosts these pages on the domain prefix you configured in the previous step.

In this project, you will use the Amplify Authenticator component in your React app instead of redirecting to these pages directly. But verifying them now proves your configuration is correct.

Seeing an error on the login page?

If the page does not load or shows an error, check that your Cognito domain prefix is set up correctly. Return to your user pool, click Domain in the left sidebar, and confirm a domain is active.

help me troubleshoot why my Cognito managed login page is not loading

Note down your configuration values

Your React app will need three values from Cognito to connect. Collect them now so they are ready for the next step.

  • In the left sidebar, click User pool overview (or the user pool name at the top). Copy the User pool ID (it looks like us-east-1_XXXXXXXXX) and paste it into a text file or note.
  • Click App clients under Applications in the left sidebar. Click on your app client name to open its details.
  • Copy the Client ID and paste it into your notes.
  • Click Domain in the left sidebar under Branding. Copy the full domain (it looks like my-login-app.auth.us-east-1.amazoncognito.com) and paste it into your notes.

Keep these values handy

You will use all three values (User Pool ID, Client ID, and domain) in Step 5 when you create the Amplify configuration file. Save them somewhere easy to access.

Your app client is configured and the managed login pages are live. Next up, you will scaffold the React app that connects to this Cognito setup.

Scaffold the React App with Vite

Your Cognito user pool is configured and your app client is ready to accept login requests. Now it's time to build the front-end application that will connect to it.

In this step, you'll scaffold a React app using Vite, install the AWS Amplify libraries that handle authentication, and confirm everything runs locally before wiring in Cognito.

In this step, get ready to:
  • Create a React project using Vite.
  • Install the AWS Amplify authentication packages.
  • Start the development server and confirm the app loads.
Create the React project with Vite

Vite is a modern build tool that scaffolds a ready-to-run React project in seconds. It also includes a built-in development server with hot reloading so you can see changes instantly.

  • Switch back to your terminal and make sure you are on the Desktop by running:
cd ~/Desktop
  • Scaffold the React project by running this command:
npm create vite@latest cognito-login -- --template react

What does this command do?

  • npm create vite@latest downloads and runs the Vite project scaffolder.
  • cognito-login is the name of the folder it creates for your project.
  • --template react tells Vite to set up a JavaScript React project instead of asking you to choose.

You may see an install prompt

If this is your first time running the command, npm will ask to install the create-vite package. Enter y and press Enter to proceed.

  • Move into the new project folder and install its dependencies by running:
cd cognito-login
npm install
  • Confirm the project files exist by running:
ls

You should see files like package.json, vite.config.js, and a src/ folder in the output.

Don't see these files?

  • Make sure you ran cd cognito-login before ls. If you are still in ~/Desktop, you will see the folder itself rather than its contents.
  • If the scaffold command failed, check that Node.js is installed by running node -v. You need a working Node.js installation for npm create to work.

Help me troubleshoot my Vite scaffold

Install the Amplify packages

AWS Amplify provides pre-built UI components and helper functions that connect your React app to Cognito. You need two packages: the core library and the React UI components.

  • Install both Amplify packages by running:
npm install aws-amplify @aws-amplify/ui-react

What are these packages?

  • aws-amplify is the core library that handles communication with AWS services like Cognito.
  • @aws-amplify/ui-react provides ready-made React components for sign-up, sign-in, and other authentication flows.
  • Verify the packages were added by running:
cat package.json

You should see aws-amplify and @aws-amplify/ui-react listed under dependencies in the output.

Install command failed?

  • Make sure you are inside the cognito-login folder (not its parent). Run pwd to check your current directory.
  • If you see a network error, check your internet connection and try running the install command again.

Help me fix my npm install error

Start the development server

Vite includes a development server that serves your React app locally. Starting it now confirms that the scaffold and package installation both succeeded.

  • Start the dev server by running:
npm run dev
  • Open your browser and navigate to http://localhost:5173.

You should see the default Vite + React welcome page with the Vite and React logos and a counter button.

Why localhost:5173?

Vite's default development port is 5173. This is the same URL you configured as the callback URL in your Cognito app client. When Cognito finishes authenticating a user, it redirects back to this address.

Page not loading in the browser?

  • Check that the terminal still shows the dev server running. If you see an error, read the message carefully for missing dependencies.
  • Make sure you are visiting http://localhost:5173 (not https). The dev server does not use HTTPS by default.
  • If port 5173 is already in use, Vite will pick the next available port and display it in the terminal output.

Help me get my Vite dev server working

Your React app is running locally and the Amplify libraries are ready to go. Next up, you'll wire Amplify to your Cognito user pool so the app can actually authenticate users.

Integrate Amplify with Cognito

Your React project is running and the Amplify libraries are installed. Now it's time to connect your app to the Cognito user pool you created earlier.

In this step, you'll create a configuration file that tells Amplify where your Cognito resources live. Then you'll wire up Amplify in the app's entry point and add a pre-built sign-in UI.

In this step, get ready to:
  • Create a configuration file with your Cognito user pool settings.
  • Wire up Amplify in the app's entry point.
  • Add the Authenticator component for sign-in and sign-up.
Create the Amplify configuration file

Amplify needs to know which Cognito user pool to talk to. You'll store these details in a dedicated configuration file so the rest of your app can import them.

  • In VS Code, create a new file called src/amplify-config.js inside your cognito-login project folder.
  • Paste the following configuration into the file:
const amplifyConfig = {
  Auth: {
    Cognito: {
      // Your Cognito User Pool ID from the AWS Console
      userPoolId: '[[USER_POOL_ID="your-user-pool-id-here"]]',
      // Your App Client ID from the app client settings
      userPoolClientId: '[[APP_CLIENT_ID="your-app-client-id-here"]]',
      loginWith: {
        oauth: {
          // Your Cognito domain (e.g., my-login-app.auth.us-east-1.amazoncognito.com)
          domain: '[[COGNITO_DOMAIN="your-domain-prefix.auth.us-east-1.amazoncognito.com"]]',
          scopes: ['openid', 'email', 'profile'],
          redirectSignIn: ['http://localhost:5173/'],
          redirectSignOut: ['http://localhost:5173/'],
          responseType: 'code',
        },
      },
    },
  },
};

export default amplifyConfig;

What does this code do?

This configuration object tells Amplify exactly how to reach your Cognito user pool.

  • userPoolId identifies which user pool to authenticate against.
  • userPoolClientId identifies your app client within that pool.
  • domain points to the managed login pages you set up earlier.
  • responseType: 'code' tells Cognito to use the OAuth 2.0 authorization code flow, the recommended pattern for web apps.
  • redirectSignIn and redirectSignOut must exactly match the callback and sign-out URLs you configured in your app client.

Where to find your values

In the AWS Console, go to Amazon Cognito and select your user pool. The User Pool ID is displayed at the top of the overview page. The App Client ID is under App clients. The domain is under App clients > your client > Login pages tab.

  • Save src/amplify-config.js.
  • You won't see a change in the browser yet. The next chunk wires this config into the app's entry point.

Not sure which values to paste?

Double-check that your userPoolId follows the format us-east-1_XXXXXXXXX (region + underscore + alphanumeric string). If you're pasting the domain, include only the hostname without https:// at the start.

help me find my Cognito User Pool ID and App Client ID in the AWS Console

✔️ Awesome, I've got everything!

Great. Double check you've saved src/amplify-config.js before moving on.

ⓧ I'd like to double check the full code

const amplifyConfig = {
  Auth: {
    Cognito: {
      userPoolId: 'your-user-pool-id-here',
      userPoolClientId: 'your-app-client-id-here',
      loginWith: {
        oauth: {
          domain: 'your-domain-prefix.auth.us-east-1.amazoncognito.com',
          scopes: ['openid', 'email', 'profile'],
          redirectSignIn: ['http://localhost:5173/'],
          redirectSignOut: ['http://localhost:5173/'],
          responseType: 'code',
        },
      },
    },
  },
};

export default amplifyConfig;
Configure Amplify in the app's entry point

Your app's entry point is src/main.jsx. This is where React mounts to the DOM. You need to call Amplify.configure() here so the auth library is initialized before any components render.

  • Open src/main.jsx in VS Code.
  • Select all the existing content and replace it with the following:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Amplify } from 'aws-amplify';
import amplifyConfig from './amplify-config.js';
import App from './App.jsx';
import './index.css';

// Initialize Amplify with your Cognito configuration
Amplify.configure(amplifyConfig);

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

What does this code do?

  • import { Amplify } from 'aws-amplify' brings in the core Amplify library.
  • import amplifyConfig from './amplify-config.js' pulls in the configuration you just created.
  • Amplify.configure(amplifyConfig) initializes Amplify before any components mount. This ensures the auth system is ready by the time your app renders.
  • Save src/main.jsx.
  • You won't see a visible change in the browser yet. Amplify is now configured behind the scenes, but the App component still shows the default Vite template.

Seeing a console error after saving?

Check that the import path './amplify-config.js' matches the filename exactly. JavaScript imports are case-sensitive. Also confirm that aws-amplify was installed in the previous step by checking your package.json for the dependency.

help me debug an import error in main.jsx after adding Amplify.configure

✔️ Awesome, I've got everything!

Great. Double check you've saved src/main.jsx before moving on.

ⓧ I'd like to double check the full code

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Amplify } from 'aws-amplify';
import amplifyConfig from './amplify-config.js';
import App from './App.jsx';
import './index.css';

Amplify.configure(amplifyConfig);

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
Replace the App component with the Authenticator

The Amplify Authenticator component provides a complete sign-in and sign-up UI out of the box. It handles form validation, error messages, and the entire authentication flow. You just wrap your app content inside it.

  • Open src/App.jsx in VS Code.
  • Select all the existing content and replace it with the following:
import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import Dashboard from './Dashboard.jsx';

// Wrap the app in Authenticator to require sign-in
function App() {
  return (
    <Authenticator>
      {({ signOut, user }) => (
        <Dashboard signOut={signOut} user={user} />
      )}
    </Authenticator>
  );
}

export default App;

What does this code do?

  • Authenticator is a pre-built component from Amplify UI that renders a sign-in form when the user is not authenticated.
  • The render prop ({ signOut, user }) only executes after the user has signed in. It gives you a signOut function and the authenticated user object.
  • The CSS import @aws-amplify/ui-react/styles.css applies the default styling to the sign-in form.
  • Dashboard is a component you'll build in the next step. For now, you need to create a placeholder so the app doesn't crash.

The import references Dashboard.jsx which doesn't exist yet. You need a placeholder so the app can load.

  • Create a new file called src/Dashboard.jsx and paste in this temporary placeholder:
// Placeholder component - you'll build this out in the next step
function Dashboard({ signOut, user }) {
  return (
    <div style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
      <h1>Welcome, {user.username}!</h1>
      <button onClick={signOut} style={{ marginBottom: '2rem', padding: '0.5rem 1rem' }}>
        Sign Out
      </button>
    </div>
  );
}

export default Dashboard;

Why create a placeholder?

JavaScript imports are resolved when a file loads. If Dashboard.jsx doesn't exist, the entire app fails with a module-not-found error. This placeholder exports a minimal component so the Authenticator can render without crashing.

You'll replace this with the full dashboard in the next step.

  • Save both src/App.jsx and src/Dashboard.jsx.
  • Switch to your browser. You should see the Amplify Authenticator sign-in form with fields for email and password, plus a Create Account tab.

Don't see the sign-in form?

If the browser shows a blank page or error, open the browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and check for errors.

  • A "module not found" error means one of your import paths is wrong. Check that ./Dashboard.jsx and ./amplify-config.js match your filenames exactly.
  • A "UserPoolId is required" error means your amplify-config.js still has the placeholder string. Replace it with your actual User Pool ID.
  • If the page is loading but no form appears, confirm you imported the styles with import '@aws-amplify/ui-react/styles.css' in App.jsx.

help me debug why the Amplify Authenticator sign-in form is not showing

✔️ Awesome, I've got everything!

Great. Double check you've saved both src/App.jsx and src/Dashboard.jsx.

ⓧ I'd like to double check the full code

import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import Dashboard from './Dashboard.jsx';

function App() {
  return (
    <Authenticator>
      {({ signOut, user }) => (
        <Dashboard signOut={signOut} user={user} />
      )}
    </Authenticator>
  );
}

export default App;

Your app now shows a fully functional sign-in form powered by Cognito. Next up, you'll build the dashboard that users see after they authenticate.

Build the Authenticated Dashboard

Your app currently shows the Amplify Authenticator sign-in form. Once a user signs in, the Authenticator renders what's inside it. Right now, that's a Dashboard component that doesn't exist yet.

In this step, you'll create the Dashboard component that users see after signing in. It will greet them by name, let them sign out, and display the JWT token claims that Amazon Cognito issued.

In this step, get ready to:
  • Create a Dashboard component with a welcome message, sign-out button, and JWT token display.
  • Update the app styling for a clean authentication layout.
  • Test the full sign-up, email verification, and sign-in flow.
Create the dashboard component

The Dashboard component is what authenticated users see after a successful sign-in. It receives the current user and a sign-out function from the Authenticator wrapper, fetches the authentication session, and displays the JWT token claims in a table.

  • In VS Code, create a new file called src/Dashboard.jsx inside your cognito-login project.
  • Add the following code to create the Dashboard component:
import { useState, useEffect } from 'react';
import { fetchAuthSession } from 'aws-amplify/auth';

function Dashboard({ signOut, user }) {
  const [tokenClaims, setTokenClaims] = useState(null);

  useEffect(() => {
    async function getToken() {
      try {
        const session = await fetchAuthSession();
        const idToken = session.tokens?.idToken;
        if (idToken) {
          setTokenClaims(idToken.payload);
        }
      } catch (error) {
        console.error('Error fetching session:', error);
      }
    }
    getToken();
  }, []);

  return (
    <div style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
      <h1>Welcome, {user.username}!</h1>
      <button onClick={signOut} style={{ marginBottom: '2rem', padding: '0.5rem 1rem' }}>
        Sign Out
      </button>

      <h2>Your JWT Token Claims</h2>
      {tokenClaims ? (
        <table style={{ borderCollapse: 'collapse', width: '100%' }}>
          <thead>
            <tr>
              <th style={{ border: '1px solid #ddd', padding: '8px', textAlign: 'left' }}>Claim</th>
              <th style={{ border: '1px solid #ddd', padding: '8px', textAlign: 'left' }}>Value</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Subject (sub)</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>{tokenClaims.sub}</td>
            </tr>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Email</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>{tokenClaims.email}</td>
            </tr>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Email Verified</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>{String(tokenClaims.email_verified)}</td>
            </tr>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Token Expiration</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                {new Date(tokenClaims.exp * 1000).toLocaleString()}
              </td>
            </tr>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Issuer</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>{tokenClaims.iss}</td>
            </tr>
          </tbody>
        </table>
      ) : (
        <p>Loading token claims...</p>
      )}
    </div>
  );
}

export default Dashboard;

What does this code do?

  • The component receives two props from the Authenticator: signOut (a function to log the user out) and user (an object containing the authenticated user's details).
  • The fetchAuthSession() function retrieves the current authentication session from Amplify, which contains the JWT tokens Cognito issued.
  • The useEffect hook calls this function once when the component loads, storing the ID token's payload (claims) in state.
  • The table renders five key claims: sub (the user's unique ID), email, email_verified, exp (token expiration), and iss (the issuer URL pointing back to your Cognito user pool).
  • Save src/Dashboard.jsx.
  • Check your browser at http://localhost:5173. You should still see the Authenticator sign-in form. The Dashboard only appears after a successful sign-in.

Seeing a compile error in the browser?

Make sure the file is named exactly Dashboard.jsx (capital D) inside the src/ folder. The import in App.jsx expects this exact path and casing.

Also check that the import path is exactly 'aws-amplify/auth' (not 'aws-amplify' or '@aws-amplify/auth'). Amplify v6 uses subpath exports for its modules.

Help me debug my Dashboard.jsx compile error

Next, replace the default Vite styles with a clean layout for your authentication app.

  • Open src/index.css in VS Code.
  • Replace the entire contents of the file with the following CSS:
:root {
  font-family: system-ui, -apple-system, sans-serif;
  line-height: 1.5;
  color: #213547;
  background-color: #ffffff;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

#root {
  width: 100%;
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

What does this CSS do?

  • The :root block sets a clean system font, comfortable line height, and neutral colors.
  • The body styles center the app content vertically and horizontally on the page.
  • The #root block constrains the app width to 800px and adds padding, keeping the layout readable.
  • Save src/index.css.
  • Your browser should refresh automatically. The sign-in form now appears centered on a clean white background.

Sign-in form not centered?

Make sure you replaced ALL the content in index.css, not just appended to it. Leftover styles from the Vite template can conflict with the new layout.

Help me fix my CSS layout

✔️ Awesome, I've got everything!

Great. Make sure you have saved both src/Dashboard.jsx and src/index.css.

ⓧ I'd like to double check the full code

Here is the complete src/Dashboard.jsx:

import { useState, useEffect } from 'react';
import { fetchAuthSession } from 'aws-amplify/auth';

function Dashboard({ signOut, user }) {
  const [tokenClaims, setTokenClaims] = useState(null);

  useEffect(() => {
    async function getToken() {
      try {
        const session = await fetchAuthSession();
        const idToken = session.tokens?.idToken;
        if (idToken) {
          setTokenClaims(idToken.payload);
        }
      } catch (error) {
        console.error('Error fetching session:', error);
      }
    }
    getToken();
  }, []);

  return (
    <div style={{ padding: '2rem', fontFamily: 'system-ui, sans-serif' }}>
      <h1>Welcome, {user.username}!</h1>
      <button onClick={signOut} style={{ marginBottom: '2rem', padding: '0.5rem 1rem' }}>
        Sign Out
      </button>

      <h2>Your JWT Token Claims</h2>
      {tokenClaims ? (
        <table style={{ borderCollapse: 'collapse', width: '100%' }}>
          <thead>
            <tr>
              <th style={{ border: '1px solid #ddd', padding: '8px', textAlign: 'left' }}>Claim</th>
              <th style={{ border: '1px solid #ddd', padding: '8px', textAlign: 'left' }}>Value</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Subject (sub)</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>{tokenClaims.sub}</td>
            </tr>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Email</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>{tokenClaims.email}</td>
            </tr>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Email Verified</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>{String(tokenClaims.email_verified)}</td>
            </tr>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Token Expiration</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>
                {new Date(tokenClaims.exp * 1000).toLocaleString()}
              </td>
            </tr>
            <tr>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>Issuer</td>
              <td style={{ border: '1px solid #ddd', padding: '8px' }}>{tokenClaims.iss}</td>
            </tr>
          </tbody>
        </table>
      ) : (
        <p>Loading token claims...</p>
      )}
    </div>
  );
}

export default Dashboard;

And the complete src/index.css:

:root {
  font-family: system-ui, -apple-system, sans-serif;
  line-height: 1.5;
  color: #213547;
  background-color: #ffffff;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

#root {
  width: 100%;
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}
Understand the JWT token claims

Your Dashboard is already set up to fetch and display JWT claims. Each time a user signs in, Cognito issues a JSON Web Token containing encoded information about the user's identity.

What is a JWT?

A JSON Web Token is a compact, URL-safe string that contains encoded claims about the user. It has three parts separated by dots: a header, a payload, and a signature.

The payload is what you see in the table. Cognito signs the token with a private key so your backend can verify it's authentic without calling Cognito again. This is what makes token-based authentication scalable.

What do the five claims mean?

  • sub is a universally unique identifier for this user. It never changes, even if the user updates their email.
  • email is the email address the user signed up with.
  • email_verified confirms whether the user completed email verification.
  • exp is the expiration timestamp. After this time, the token is invalid and the user must re-authenticate.
  • iss is the issuer URL pointing back to your specific Cognito user pool. Your backend uses this to verify which pool issued the token.

These claims are how your application knows WHO the user is and WHAT they're allowed to do. Every API request can include this token for server-side verification.

Test the complete sign-up and sign-in flow

Everything is wired up. Time to test the full authentication flow from sign-up to viewing your JWT claims.

  • Switch to your browser at http://localhost:5173.
  • On the Authenticator sign-in form, click the Create Account tab.
  • Enter a real email address you have access to.
  • Create a password that meets the requirements (at least 8 characters, with uppercase, lowercase, numbers, and special characters).
  • Click Create Account.

Why do I need a real email?

Your user pool requires email verification. Cognito sends a verification code to the email address you provide. Without a real email, you cannot complete the sign-up process.

  • Check your email inbox for a verification code from Cognito.
  • Enter the verification code in the confirmation form and click Confirm.
  • After verification, sign in with your email and password.

After a successful sign-in, the Authenticator disappears and your Dashboard renders. You should see a welcome heading with your username, a Sign Out button, and a table showing five JWT claims.

  • Verify the table shows your sub (a unique UUID), your email address, email_verified as true, a future expiration date, and an issuer URL containing your user pool ID.
  • Click the Sign Out button. The Authenticator sign-in form reappears.
  • Sign in again with the same credentials. The Dashboard loads immediately this time (no verification needed).

Dashboard shows "Loading token claims..." and never updates?

Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for error messages. A common cause is a mismatch between the User Pool ID or App Client ID in src/amplify-config.js and what exists in the AWS Console.

Also confirm that the oauth.domain value in your config matches the Cognito domain prefix you set up (including the full .auth.us-east-1.amazoncognito.com suffix, using your actual region).

Help me debug my token claims not loading

Your authentication system is fully functional. You've built a React app that handles user registration, email verification, sign-in, and token display. Next up, take on the Secret Mission to add multi-factor authentication for an extra layer of security.

Secret mission

Add TOTP Multi-Factor Authentication

Your login system works, but a username and password alone can be compromised. In this secret mission, you will add TOTP multi-factor authentication so users need both their password and a time-based code from an authenticator app to sign in.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses Amazon Cognito's free tier, which covers 10,000 monthly active users at no cost. Your user pool has no idle charges.

Resources you used:

  • Amazon Cognito user pool (including app client, domain, and user accounts).
  • Local cognito-login project folder on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your login system.

  • Your Cognito user pool costs nothing while idle. You are only charged if you exceed 10,000 monthly active users on the Essentials tier.
  • Your local project files stay on disk and take minimal space.
  • The Vite dev server only runs when you start it manually with npm run dev.

Pause - I'll come back to this later

Shut down any running processes to free up memory. Your files and cloud resources stay intact for when you return.

  • If your Vite dev server is still running in the terminal, press Ctrl+C to stop it.
  • Your Cognito user pool has no idle cost. No further action needed in AWS.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your user pool and all registered user accounts.

Delete the Cognito user pool:

  • Open the AWS Management Console and navigate to Amazon Cognito.
  • Select User Pools from the left sidebar.
  • Select your user pool from the list.
  • Click Delete.
  • The console will prompt you to deactivate deletion protection (enabled by default for console-created pools). Confirm the deactivation and type the pool name to proceed with deletion.

Delete the local project files:

  • Remove the project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/cognito-login

Windows

rmdir /s /q %USERPROFILE%\Desktop\cognito-login
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see cognito-login in the output.

Nice Work!

Nice Work!

You just built a secure authentication system from scratch using Amazon Cognito. Your app handles user registration, email verification, and sign-in using industry-standard OAuth 2.0 and JWT patterns.

Here's what you learned:

  • Created an Amazon Cognito user pool with managed login pages for sign-up, sign-in, and password reset.
  • Configured the OAuth 2.0 authorization code flow with a public app client and integrated it into a React app using the Amplify Authenticator component.
  • Retrieved and decoded JWT token claims to understand how identity information flows between Cognito and your application.
  • Added TOTP multi-factor authentication so users need an authenticator app to complete sign-in.

Ready to quiz yourself?