Add User Login with Amazon Cognito

Add real user authentication to a static website using Amazon Cognito.

Introduction

30 Second Summary

Every time you sign into a website, something invisible happens behind the scenes. The site checks your identity, hands you a digital "pass" called a token, and uses that pass to decide what you can see.

In this project, you will add real user authentication to a static website using Amazon Cognito. You will wire up sign-in, sign-up, and sign-out, then write JavaScript that reads an identity token and displays personalized content for each user.

What You'll Build

You will sign in to your own website, see a personalized welcome message pulled from a real identity token, and sign out cleanly while your page switches between a public view and a members-only view.

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

  • A Sign In button that redirects visitors to a Cognito-hosted login page where they can create an account or authenticate with existing credentials.
  • A personalized welcome message showing the logged-in user's email address, extracted from a real JWT token that your JavaScript parses.
  • A members-only content section that only appears after authentication. Logged-out visitors cannot see it, even by inspecting the page source.
  • Secret Mission: Build a profile card that displays all identity claims from the token and handles expired sessions gracefully.

Are there any prerequisites?

You need an AWS account with console access and basic familiarity with HTML and CSS. This project also assumes you have existing static site files (an index.html and a style.css) ready to upload.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This is your moment to commit to the project goal before any hands-on work begins.

Set Up Your S3 Bucket and CloudFront Distribution

Later in this project, you will connect your website to Amazon Cognito for user authentication. Cognito requires an HTTPS URL to redirect users back to after they sign in.

S3 website hosting only serves pages over HTTP. To get the HTTPS endpoint that Cognito needs, you will place a CloudFront distribution in front of your S3 bucket. In this step, you will create both resources and confirm your site loads securely.

In this step, get ready to:
  • Create an S3 bucket with static website hosting and upload your site files.
  • Create a CloudFront distribution to serve your site over HTTPS.
  • Verify your site loads correctly over HTTPS.
Create an S3 bucket and enable static website hosting

You need a place to store your website files that AWS can serve to visitors. An S3 bucket is that storage container.

  • Open your browser and navigate to the AWS Management Console.
  • In the search bar at the top, type S3 and select S3 from the results.
  • Click Create bucket.
  • In the Bucket name field, enter a unique name like my-auth-site-your-initials (for example, my-auth-site-jd).
  • Scroll down to Block Public Access settings for this bucket.
  • Uncheck Block all public access.
  • A warning will appear. Check the acknowledgement box to confirm you understand the risks.
  • Leave all other settings as defaults and click Create bucket at the bottom of the page.

You should see a green success banner and your new bucket listed on the S3 buckets page.

Why disable Block Public Access?

When you use the S3 static website hosting endpoint with CloudFront, the bucket must allow public read access. CloudFront fetches your files from the S3 website endpoint over HTTP, and that endpoint requires a public bucket policy.

For a learning project serving only static HTML and CSS, this is fine. In a production environment, you would use the S3 REST API endpoint with Origin Access Control (OAC) to keep the bucket private.

  • Click on your bucket name to open it.
  • Click the Upload button.
  • Click Add files and select your index.html and style.css files from your computer.
  • Click Upload at the bottom of the page.
  • Once you see a green success banner, click Close.

You should see index.html and style.css listed in your bucket's Objects tab.

  • Now add a bucket policy to allow public reads. Click the Permissions tab.
  • Scroll down to Bucket policy and click Edit.
  • Paste the following policy into the editor, replacing the bucket name with your own:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::[[BUCKET_NAME="my-auth-site-your-initials"]]/*"
        }
    ]
}

What does this policy do?

This bucket policy grants everyone ("Principal": "*") permission to read objects from your bucket. CloudFront needs this to fetch your files and serve them to visitors.

  • Click Save changes.

You should see a banner confirming the policy was saved. Now enable static website hosting.

  • Click the Properties tab.
  • Scroll all the way down to Static website hosting and click Edit.
  • Select Enable.
  • In the Index document field, enter index.html.
  • Click Save changes.

After saving, stay on the Properties tab. Scroll back down to Static website hosting. You now see an Endpoint URL. Copy this URL and save it somewhere. You will need it in the next substep.

Don't see the endpoint?

Make sure you selected Enable under Static website hosting and entered index.html as the index document. The file name is case-sensitive and must match your uploaded file exactly.

If you see an error when saving the bucket policy, check that your account-level Block Public Access settings are also disabled for this bucket.

help me troubleshoot my S3 static website hosting setup

Create a CloudFront distribution pointing to your S3 bucket

CloudFront is a content delivery network (CDN) that sits in front of your S3 bucket. It serves your site over HTTPS, which is what Cognito requires for its redirect URLs.

  • In the AWS console search bar at the top, type CloudFront and select CloudFront from the results.
  • Click Create distribution.
  • In the Origin domain field, paste your S3 website endpoint that you copied in the previous substep.

Do not select from the dropdown

When you click the Origin domain field, a dropdown will appear listing your S3 buckets. Do not select your bucket from this dropdown. The dropdown uses the S3 REST API endpoint, which will not work correctly with static website hosting.

Instead, paste the website endpoint you copied from the S3 Properties tab. It looks like my-auth-site-jd.s3-website-us-east-1.amazonaws.com (it contains s3-website in it).

  • Leave all other origin settings as their defaults.
  • Scroll down to Default cache behavior. Under Viewer protocol policy, select Redirect HTTP to HTTPS.
  • Scroll down to Settings. In the Default root object field, enter index.html.
  • Leave everything else as defaults and click Create distribution at the bottom of the page.

You are taken to your new distribution's details page. The Last modified field shows Deploying. This means CloudFront is propagating your distribution to edge servers worldwide.

How long does deployment take?

CloudFront deployment typically takes 5 to 15 minutes. You can continue reading while it deploys. Once the Last modified field changes from Deploying to a date and time, your distribution is ready.

  • While you wait, find the Distribution domain name on the details page. It looks like dXXXXXXXXXX.cloudfront.net.
  • Copy this domain name and save it. You will use this URL throughout the rest of the project.

Seeing an Access Denied error?

If you try to visit your CloudFront URL and see an XML error containing AccessDenied, check two things:

  • Confirm you pasted the S3 website endpoint (contains s3-website) as the origin, not the REST API endpoint.
  • Confirm your bucket policy allows public read access and that Block Public Access is disabled.

help me fix an AccessDenied error on my CloudFront distribution

Verify HTTPS access

Once your distribution finishes deploying, you can confirm everything is wired up correctly.

  • Wait until the Last modified field on your distribution's page shows a date and time (not Deploying).

Before you open the URL, what do you expect to see? Your static site from the files you uploaded, but now served over HTTPS.

  • Open a new browser tab and navigate to your CloudFront URL with https:// in front (for example, https://dXXXXXXXXXX.cloudfront.net).

You should see your static website loaded over HTTPS. Check the browser address bar for the lock icon confirming a secure connection.

What is HTTPS?

HTTPS encrypts the connection between your visitor's browser and the server. The lock icon in your browser bar confirms the connection is encrypted. Cognito will only redirect users back to HTTPS URLs, which is why this step was necessary before adding authentication.

Your static site is now live over HTTPS. Next up, you will create a Cognito User Pool that gives your site a real login system.

Create Your Cognito User Pool and Managed Login

Your site is live over HTTPS. Now you need a way for people to create accounts and sign in.

Building sign-up, sign-in, password resets, and email verification from scratch would take weeks of work. Amazon Cognito solves this with a User Pool (your user directory) and a managed login page (a ready-made sign-in form hosted by AWS). In this step, you will create both and verify the login page works in your browser.

In this step, get ready to:
  • Create an Amazon Cognito User Pool with email-based sign-in.
  • Configure the app client for the OAuth 2.0 implicit grant flow.
  • Test the managed login page in your browser.
Create the User Pool

A User Pool is a user directory managed entirely by AWS. It handles account creation, password policies, email verification, and more. You will create one now using the Cognito console.

  • In the AWS Management Console, search for Cognito in the top search bar and open the Amazon Cognito service.
  • Click Create user pool.
  • Under Define your application, choose Single-page application (SPA) as the application type.
  • Under Options for sign-in identifiers, select Email.
  • Under Required attributes for sign-up, keep the default (email is already selected).
  • Under Add a return URL, enter your CloudFront HTTPS URL (e.g., https://dXXXXXXXXXX.cloudfront.net).
  • Click Create your application.

Amazon Cognito creates both a User Pool and an app client with default settings for your SPA. You are taken to a setup page with code examples.

  • Scroll down and click Go to overview to see your new User Pool.

What is a User Pool?

A User Pool is a managed user directory. It stores usernames, passwords, and attributes (like email) for your app's users. It also handles sign-up verification, password resets, and account recovery without you writing any of that logic.

Why implicit grant over authorization code?

For a pure static site with no backend server, the implicit grant is the simplest approach. It delivers the identity token directly in the URL fragment after login, so your JavaScript can read it immediately.

The authorization code grant (with PKCE) is more secure for production SPAs. For this learning project, the implicit grant makes the flow visible. You see the token in the URL bar, understand what it is, and then parse it.

Configure the app client for implicit grant

The SPA template created an app client for you, but it defaults to the authorization code grant. You need to also enable the implicit grant so tokens appear directly in your URL after login.

  • In your User Pool, click the App clients menu on the left side.
  • Select your app client from the list.
  • Under App client information, click Edit.
  • Scroll down to the OAuth grant types section.
  • Ensure Implicit grant is checked (in addition to any default grant type already selected).
  • Under Allowed scopes, ensure openid, profile, and email are all selected.
  • Scroll to Allowed sign-out URLs and add your CloudFront URL (e.g., https://dXXXXXXXXXX.cloudfront.net).
  • Click Save changes.

What did you just configure?

You enabled the implicit grant, which tells Cognito to return identity tokens directly in the browser URL after login. You also added three scopes that control what information the token contains.

  • openid tells Cognito to return an ID token (required for any identity information).
  • profile includes the user's profile attributes.
  • email includes the user's email address in the token.
  • While still on the app client page, note your Client ID shown under App client information. Copy it somewhere safe. You will need it shortly.
  • Navigate to the Domain menu in the left sidebar of your User Pool.
  • Note your Cognito domain. It follows the format {prefix}.auth.{region}.amazoncognito.com. Copy this as well.

Don't see a domain listed?

The Cognito console auto-creates a domain when you create a User Pool. If you do not see one, click Create Cognito domain and enter a unique prefix (e.g., your name or project name). It can take up to 60 seconds for the domain to become active.

Help me set up my Cognito domain

Test the managed login page

You now have everything needed to construct the login URL. This URL is what your Sign In button will eventually point to. For now, you will open it manually to confirm the login page works.

  • Construct your login URL by combining your Cognito domain, client ID, and CloudFront URL into this format:
https://[[COGNITO_DOMAIN="your-prefix.auth.us-east-1.amazoncognito.com"]]/oauth2/authorize?response_type=token&client_id=[[COGNITO_CLIENT_ID="your-app-client-id"]]&redirect_uri=[[CLOUDFRONT_URL="https://dXXXXXXXXXX.cloudfront.net"]]&scope=openid+profile+email

What does this URL do?

  • /oauth2/authorize is the Cognito authorization endpoint that serves the login page.
  • response_type=token tells Cognito to use the implicit grant (return tokens directly in the URL).
  • client_id identifies your app client.
  • redirect_uri is where Cognito sends the user after login. It must exactly match your allowed callback URL.
  • scope=openid+profile+email requests the three scopes you enabled.

Before you open this URL, what do you think you will see? A blank page? An error? A login form?

  • Paste your constructed URL into your browser's address bar and press Enter.

You should see a real, working sign-in and sign-up form hosted by AWS. This is the Cognito managed login page. You did not write a single line of HTML for it.

Seeing an error instead of a login page?

  • Check that your redirect_uri exactly matches the callback URL you saved in your app client settings. Even a trailing slash difference will cause a mismatch error.
  • Confirm the client_id in your URL matches the Client ID shown on the App client information page.
  • If you just created the domain, wait 60 seconds and try again. New domains take a moment to become active.

Help me fix my Cognito login URL error

Your managed login page is live. Next up, you will wire a Sign In button on your site to this URL and experience the full authentication flow for the first time.

Wire Up the Login Button and Hit the Wall

Your Cognito User Pool is live, and the managed login page works. Now it is time to connect your actual website to it.

In this step you will add a Sign In button and the HTML structure for a logged-in view. You will sign up for a real account, authenticate, and get redirected back to your site. But here is the catch: the identity token will be sitting right there in your URL bar, and your page will completely ignore it. Nothing on screen changes. This is the moment you feel why JavaScript exists.

In this step, get ready to:
  • Add a Sign In button and logged-in/logged-out HTML structure to your site.
  • Upload the updated file to S3 and invalidate the CloudFront cache.
  • Sign up for a real user account and experience the designed obstacle.
Add the login button and page structure to your HTML

Your site needs two views: one for visitors who have not signed in yet, and one for authenticated users. You will replace your existing index.html content with a new structure that includes both views.

  • In VS Code, open your index.html file.
  • Select all existing content and replace it with the following HTML:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Authenticated Site</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>My Website</h1>
    </header>

    <main>
        <div id="logged-out">
            <h2>Welcome, visitor!</h2>
            <p>Sign in to access exclusive members-only content.</p>
            <a id="sign-in-btn" class="btn" href="#">Sign In</a>
        </div>

        <div id="logged-in" style="display: none;">
            <h2 id="welcome-msg">Welcome!</h2>
            <div id="members-content"></div>
            <button id="sign-out-btn" class="btn" onclick="signOut()">Sign Out</button>
        </div>
    </main>
</body>
</html>

What does this HTML do?

  • The logged-out div is the public view. Visitors see a welcome message and a Sign In button.
  • The logged-in div is hidden by default (display: none). It has a welcome message placeholder and a Sign Out button. JavaScript will toggle these divs later.
  • The Sign In button href is # for now. In the next step, JavaScript will set it to your Cognito login URL dynamically.
  • Save index.html.

File not saving?

Make sure you are editing the local copy of index.html (not a file inside an S3 download). Check for a white dot on the file tab in VS Code, which indicates unsaved changes.

help me check my index.html is saved correctly

Upload to S3 and invalidate the CloudFront cache

Your updated file is saved locally, but your live site still serves the old version from S3 via CloudFront. You need to upload the new file and tell CloudFront to stop serving its cached copy.

  • Open the S3 console in your browser.
  • Click on your bucket (my-auth-site-{your-initials}).
  • Click **Upload**, then **Add files**, and select your updated index.html from your computer.
  • Click **Upload** to confirm. This overwrites the existing file.

Now invalidate the CloudFront cache so visitors get the fresh version.

  • Open the CloudFront console.
  • Click on your distribution.
  • Select the **Invalidations** tab.
  • Click **Create invalidation**.
  • In the object paths field, enter:
/*
  • Click **Create invalidation** to submit.

What does invalidation do?

CloudFront caches your files at edge locations worldwide for fast delivery. When you update a file in S3, CloudFront keeps serving the old cached copy until it expires. An invalidation tells CloudFront to discard its cache immediately so the next request fetches the fresh version from S3.

  • Visit your CloudFront URL (https://dXXXXXXXXXX.cloudfront.net) in your browser.
  • Confirm you see the updated page with "Welcome, visitor!" and a Sign In button.

Still seeing the old page?

Invalidations can take 1-2 minutes to propagate. Wait a moment and do a hard refresh (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows). If the old page persists, check that your invalidation status shows "Completed" in the CloudFront console.

help me troubleshoot why my CloudFront distribution is still showing the old page

Sign up and experience the wall

The page structure is ready. The Sign In button's link will be set by JavaScript in the next step. For now, you will navigate directly to your Cognito login URL to test the full authentication flow and see what happens when you return.

  • In your browser address bar, enter your Cognito authorize URL. This is the URL you tested in Step 2 with this format:
https://[[COGNITO_DOMAIN="your-domain-prefix.auth.us-east-1.amazoncognito.com"]]/oauth2/authorize?response_type=token&client_id=[[CLIENT_ID="your-app-client-id"]]&redirect_uri=[[REDIRECT_URI="https://your-cloudfront-domain.cloudfront.net"]]&scope=openid+profile+email
  • On the Cognito login page, click **Sign up** to create a new account.
  • Enter your email address.
  • Create a password that meets the requirements (uppercase, lowercase, number, special character, minimum 8 characters).
  • Click **Sign up**.
  • Check your email inbox for a verification code from Amazon Cognito.
  • Enter the verification code on the confirmation page and click **Confirm**.
  • Sign in with your email and password.

Before you press Enter on that sign-in, pause. What do you think will happen on your site after Cognito authenticates you?

  • Complete the sign-in.

You land back on your CloudFront URL. Now look at your browser's address bar.

After the # symbol, you see a massive string starting with id_token=eyJ followed by hundreds of characters. That is your identity token. It contains your email, your user ID, and when the token expires.

Now look at the page itself. Nothing changed. The same "Welcome, visitor!" message stares back at you. Your page has zero idea that you just authenticated.

Why does nothing happen?

Cognito did its job. It authenticated you and sent your identity token back in the URL fragment (the part after #). But plain HTML cannot read the URL bar or react to it. Only JavaScript can extract that token, decode it, and change what the page displays.

This gap between "the data is right there" and "the page does nothing with it" is exactly what you will close in the next step.

✔️ Awesome, I've got everything!

Great. Confirm you have saved index.html locally and uploaded it to S3.

ⓧ I'd like to double check the full code

Here is the complete index.html you should have at this point:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My Authenticated Site</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header>
        <h1>My Website</h1>
    </header>

    <main>
        <div id="logged-out">
            <h2>Welcome, visitor!</h2>
            <p>Sign in to access exclusive members-only content.</p>
            <a id="sign-in-btn" class="btn" href="#">Sign In</a>
        </div>

        <div id="logged-in" style="display: none;">
            <h2 id="welcome-msg">Welcome!</h2>
            <div id="members-content"></div>
            <button id="sign-out-btn" class="btn" onclick="signOut()">Sign Out</button>
        </div>
    </main>
</body>
</html>

The token is in your URL. Your page is oblivious. In the next step, you will write your first JavaScript to crack open that token, read your identity, and transform the page into a personalized dashboard.

Decode the Token and Personalize Your Page

In the previous step, you signed in through Cognito and saw a long token string appear in your URL bar. Your page completely ignored it.

That token is a JWT (JSON Web Token). It contains your identity information encoded in Base64URL. In this step, you will write JavaScript to extract the token from the URL, decode it, and pull out your email address to display a personalized welcome message.

In this step, get ready to:
  • Write a JWT parser that decodes the identity token from your URL.
  • Build display functions that personalize the page with your email address.
  • Upload and verify the full login-to-personalization flow.
Write the token parser

Your JavaScript needs three things to work with Cognito: your domain, your app client ID, and your CloudFront redirect URL. These let it construct the login link and talk to Cognito's endpoints.

  • In VS Code, open index.html.
  • Find the closing </main> tag.
  • Add a <script> tag between </main> and </body>. All the JavaScript you write in this step goes inside this script tag.
  • Add the following configuration and login URL setup as the first code inside your script tag:
// === CONFIGURATION ===
// Replace these values with your own after completing Steps 1-2
var cognitoDomain = "your-domain-prefix.auth.us-east-1.amazoncognito.com";
var clientId = "your-app-client-id-here";
var redirectUri = "https://your-cloudfront-domain.cloudfront.net";

// === SET LOGIN LINK ===
var loginUrl = "https://" + cognitoDomain + "/oauth2/authorize?response_type=token&client_id=" + clientId + "&redirect_uri=" + encodeURIComponent(redirectUri) + "&scope=openid+profile+email";
document.getElementById("sign-in-btn").href = loginUrl;

What does this code do?

The three var lines store your Cognito domain, app client ID, and CloudFront URL. These are the values you noted in Steps 1 and 2.

The loginUrl line constructs the full OAuth 2.0 authorize URL. The last line assigns this URL to the Sign In button's href, so clicking it redirects to Cognito's login page.

  • Replace your-domain-prefix.auth.us-east-1.amazoncognito.com with your actual Cognito domain (from Step 2).
  • Replace your-app-client-id-here with your app client ID (from Step 2).
  • Replace your-cloudfront-domain.cloudfront.net with your CloudFront distribution domain (from Step 1).
  • Save index.html.
  • Open the file directly in your browser (double-click it, or use File > Open).
  • Right-click the Sign In button and select Inspect (or Inspect Element).
  • In the Elements panel, look at the href attribute of the <a> tag. It should now contain your full Cognito authorize URL instead of #.

Still seeing href="#"?

Make sure your <script> tag is placed after the </main> tag, not inside the <head>. The script needs the Sign In button to already exist in the page before it can set its href.

Also check the browser console (F12 > Console tab) for any red errors pointing to a typo.

Help me fix my Sign In button href

Now add the two token utility functions below your login URL code (still inside the same script tag):

// === JWT PARSER ===
function parseJwt(token) {
    try {
        var base64Url = token.split('.')[1];
        var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
        var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));
        return JSON.parse(jsonPayload);
    } catch (e) {
        return null;
    }
}

// === TOKEN EXTRACTION ===
function getTokenFromUrl() {
    var hash = window.location.hash.substring(1);
    var params = {};
    hash.split('&').forEach(function(part) {
        var pair = part.split('=');
        params[pair[0]] = decodeURIComponent(pair[1]);
    });
    return params.id_token || null;
}

What do these functions do?

A JWT has three parts separated by dots: header, payload, and signature. The parseJwt function takes the middle part (the payload), converts it from Base64URL to regular Base64, decodes it with atob(), and parses the resulting JSON. This gives you an object with claims like email, sub, and exp.

The getTokenFromUrl function reads everything after the # in the URL (where Cognito puts the token), splits it into key-value pairs, and returns the id_token value.

  • Save index.html and refresh your browser.
  • Open your browser's Developer Tools console (F12, then click the Console tab). Confirm there are no red JavaScript errors. This verifies your syntax is correct.

Seeing a syntax error in the console?

Check for missing closing braces or parentheses. The parseJwt function has nested calls that are easy to mistype. Compare your code carefully against the snippet above.

Help me fix my parseJwt syntax error

Display the user's identity

With the parser ready, you need functions that swap the page between logged-out and logged-in states. You also need a sign-out function and the main logic that ties everything together.

  • Below your getTokenFromUrl function (still inside the same script tag), add the sign-out and display functions:
// === SIGN OUT ===
function signOut() {
    sessionStorage.removeItem("id_token");
    var logoutUrl = "https://" + cognitoDomain + "/logout?client_id=" + clientId + "&logout_uri=" + encodeURIComponent(redirectUri);
    window.location.href = logoutUrl;
}

// === MAIN LOGIC ===
function showLoggedIn(payload) {
    document.getElementById("logged-out").style.display = "none";
    document.getElementById("logged-in").style.display = "block";
    document.getElementById("welcome-msg").textContent = "Welcome, " + payload.email + "!";

    var membersDiv = document.getElementById("members-content");
    membersDiv.innerHTML = "<h3>Members Only</h3><p>You are seeing this because you are authenticated. Your user ID is: " + payload.sub + "</p>";
}

function showLoggedOut() {
    document.getElementById("logged-out").style.display = "block";
    document.getElementById("logged-in").style.display = "none";
}

What do these functions do?

The signOut function clears the stored token from sessionStorage, then redirects to Cognito's logout endpoint. This ends the server-side session too.

The showLoggedIn function hides the public view, shows the authenticated view, sets the welcome message to include the user's email, and injects members-only content dynamically. The showLoggedOut function reverses this.

  • Below the showLoggedOut function, add the main logic that runs when the page loads:
// Check for token in URL (redirect from Cognito)
var token = getTokenFromUrl();
if (token) {
    sessionStorage.setItem("id_token", token);
    history.replaceState(null, "", window.location.pathname);
} else {
    token = sessionStorage.getItem("id_token");
}

if (token) {
    var payload = parseJwt(token);
    if (payload && payload.exp && (payload.exp * 1000 > Date.now())) {
        showLoggedIn(payload);
    } else {
        sessionStorage.removeItem("id_token");
        showLoggedOut();
    }
} else {
    showLoggedOut();
}

What does this main logic do?

First, it checks the URL hash for a token (which appears after Cognito redirects you back). If found, it stores the token in sessionStorage and cleans the hash from the URL bar with history.replaceState.

If no token is in the URL, it checks sessionStorage for a previously saved token (so refreshing the page does not lose your session).

Finally, it parses the token and checks the exp (expiration) claim. If the token is still valid, it shows the logged-in view. If expired or missing, it shows the public view.

  • After your main logic code, add the closing </script> tag. Make sure it sits just before </body>.
  • Save index.html.
Upload and verify the full login flow

Your JavaScript is complete. Time to upload it to S3 and test the entire authentication flow through CloudFront.

  • Return to the S3 console in your browser.
  • Click your bucket name to open it.
  • On the Objects tab, click Upload.
  • Click Add files and select your updated index.html from your computer.
  • Click Upload. This overwrites the existing file in S3.

Now invalidate the CloudFront cache so your visitors get the fresh file.

  • Open the CloudFront console.
  • Click your distribution.
  • Select the Invalidations tab.
  • Click Create invalidation.
  • Enter /* as the path.
  • Click Create invalidation.

Why invalidate?

CloudFront caches your files at edge locations around the world. When you upload a new version to S3, CloudFront still serves the old cached copy until it expires. Creating an invalidation for /* tells CloudFront to fetch fresh versions of all files from S3 on the next request.

Before you test, think about what you expect to happen. When you click Sign In and authenticate, what should the page show this time that it did not show before?

  • Open your CloudFront URL in your browser (format: https://dXXXXXXXXXX.cloudfront.net).
  • Click Sign In.
  • Sign in with the Cognito account you created in Step 3.
  • After authenticating, you are redirected back to your site.

This time the page transforms. You see "Welcome, " followed by your email address. The Sign In button is gone, replaced by the Members Only content and a Sign Out button.

Page still looks the same after sign-in?

  • Check that the CloudFront invalidation status shows Completed (not In Progress). Wait for it to finish before testing.
  • Open Developer Tools (F12) and check the Console tab for JavaScript errors.
  • Verify that the redirectUri in your script exactly matches the callback URL you set in your Cognito app client (including https:// and no trailing slash).

Help me debug why my page is not personalizing after login

✔️ Awesome, I've got everything!

Great. Double check you have saved index.html and that your CloudFront invalidation shows Completed.

ⓧ I'd like to double check the full code

Your index.html should now have four main sections inside the <script> tag. Scroll up and confirm each one is present, in order:

  • Configuration block (cognitoDomain, clientId, redirectUri) and the loginUrl assignment to sign-in-btn.
  • The parseJwt function and the getTokenFromUrl function.
  • The signOut, showLoggedIn, and showLoggedOut functions.
  • The main logic block that calls getTokenFromUrl(), stores the token, checks expiration, and calls showLoggedIn or showLoggedOut.

The HTML structure above the script tag should be unchanged from Step 3: the logged-out div with the Sign In button, and the logged-in div (hidden by default) with the welcome message, members-content div, and Sign Out button.

Your token parser is live and your page personalizes on login. Next up, you will gate the members-only content so it only exists in the page when a valid token is present, and wire up the Sign Out button to fully clear the session.

Add Protected Content and Sign Out

Your authentication logic is working. Users sign in through Cognito, get redirected back with a token, and your JavaScript parses it to show a personalized welcome message.

But there are two things left to finalize. The members-only section and sign-out button need proper visual styling so users can clearly see the state change. You also need to verify the complete authentication cycle works end-to-end, including that signing out truly clears the session.

In this step, get ready to:
  • Organize your JavaScript with section comments for maintainability.
  • Style the members-only content area and sign-out button.
  • Test the full authentication cycle from sign in to sign out.
Organize your script and verify content gating

Your members-only content is already securely gated. The <div id="members-content"></div> in your HTML is empty. The showLoggedIn() function injects content only after successfully parsing a valid token.

Anyone viewing your page source without authenticating sees an empty div. This is the correct pattern for client-side content gating.

Why is this secure enough for a static site?

Since you have no server to protect data, the goal is making content invisible to casual visitors. The JavaScript injection means the members-only HTML never exists in the page source until a valid JWT is parsed.

For truly sensitive data, you would use a server that validates the token before returning protected content. For a static site, client-side gating is the standard approach.

  • In VS Code, open index.html.
  • Find the opening <script> tag inside your file.
  • Add section comments above each logical group to organize the code. Place the following comment on the line directly above var cognitoDomain:
// === CONFIGURATION ===
  • Add this comment on the line directly above var loginUrl:
// === SET LOGIN LINK ===
  • Add this comment on the line directly above function parseJwt:
// === JWT PARSER ===
  • Add this comment on the line directly above function getTokenFromUrl:
// === TOKEN EXTRACTION ===
  • Add this comment on the line directly above function signOut:
// === SIGN OUT ===
  • Add this comment on the line directly above function showLoggedIn:
// === MAIN LOGIC ===
  • Add this comment on the line directly above var token = getTokenFromUrl();:
// Check for token in URL (redirect from Cognito)
  • Inside the showLoggedIn function, add a blank line between the welcome-msg textContent line and the var membersDiv line.
  • Save index.html.

What do these comments do?

Section comments make your script scannable at a glance. When you return to this code later, you can jump directly to the configuration, parser, or sign-out logic without reading through everything.

Style the authentication elements

Your page needs CSS that visually distinguishes the authenticated state. The members-only area should stand out, and the sign-out button should be clearly different from the sign-in button.

  • In VS Code, open style.css.
  • Select all the existing content and delete it.
  • Paste the following reset and layout styles at the top of the empty file:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    line-height: 1.6;
    color: #333;
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
}

header {
    margin-bottom: 2rem;
    padding-bottom: 1rem;
    border-bottom: 2px solid #eee;
}

h1 {
    color: #1a1a2e;
}

What does this code do?

The * rule resets all default browser margins and padding for consistent spacing. The body rule centers your page content with a maximum width of 800 pixels.

  • Below what you just pasted, add the typography and button styles:
h2 {
    margin-bottom: 1rem;
    color: #16213e;
}

h3 {
    margin-bottom: 0.5rem;
    color: #0f3460;
}

p {
    margin-bottom: 1rem;
}

.btn {
    display: inline-block;
    padding: 0.75rem 1.5rem;
    background-color: #0f3460;
    color: white;
    text-decoration: none;
    border-radius: 4px;
    border: none;
    cursor: pointer;
    font-size: 1rem;
}

.btn:hover {
    background-color: #16213e;
}

What does this code do?

The .btn class styles both your Sign In link and Sign Out button with the same base appearance. The :hover state darkens the background when a user hovers over the button.

  • Save style.css and refresh your browser (if you have a local preview open).

You should see your page with clean typography, a centered layout, and a styled Sign In button.

  • Now add the authentication-specific styles below what you just pasted:
#members-content {
    margin: 1.5rem 0;
    padding: 1.5rem;
    background-color: #f0f8ff;
    border-left: 4px solid #0f3460;
    border-radius: 4px;
}

#sign-out-btn {
    margin-top: 1rem;
    background-color: #e94560;
}

#sign-out-btn:hover {
    background-color: #c23152;
}

What does this code do?

The #members-content styles give your protected section a light blue background and a bold left border, making it visually distinct from the public page.

The #sign-out-btn overrides the default button color with red, signaling that this action ends the session.

  • Save style.css.

Styles not looking right?

Make sure there are no extra curly braces or missing semicolons. Each property line must end with a semicolon, and each rule block must have matching opening and closing braces.

help me debug my CSS styles for the authentication page

✔️ Awesome, I've got everything!

Great. Make sure you have saved both index.html and style.css.

ⓧ I'd like to double check the full code

Your style.css should contain these three sections in order:

  • Reset and layout styles (*, body, header, h1 selectors).
  • Typography and button styles (h2, h3, p, .btn, .btn:hover selectors).
  • Authentication-specific styles (#members-content, #sign-out-btn, #sign-out-btn:hover selectors).

Scroll up and confirm each code block is present in your file, in the order shown above.

For index.html, confirm all seven section comments are in place (// === CONFIGURATION === through // Check for token in URL (redirect from Cognito)) and the blank line exists inside showLoggedIn between the welcome message and members div lines.

Upload and test the full authentication cycle

Both files are finalized locally. Now you need to upload them to S3 and clear the CloudFront cache so your live site serves the latest version.

  • Return to the S3 console in your browser.
  • Open your my-auth-site-{initials} bucket.
  • Click Upload.
  • Drag in both index.html and style.css from your local folder.
  • Click Upload to overwrite the existing files.

S3 now has your latest code. But CloudFront may still be serving the old cached version.

  • Switch to the CloudFront console.
  • Select your distribution.
  • Click the Invalidations tab.
  • Click Create invalidation.
  • Enter /* in the object path field.
  • Click Create invalidation to submit.

What does the invalidation do?

CloudFront caches your files at edge locations worldwide. The /* invalidation tells every edge location to discard its cached copies and fetch fresh versions from S3 on the next request.

  • Wait about 30 seconds for the invalidation to complete (the status changes from In Progress to Completed).

Now it is time to test the entire authentication cycle. Before you open your site, predict what you expect to see at each stage: the public view first, then personalized content after sign in, then back to public after sign out.

  • Open your CloudFront URL in a new browser tab (format: https://dXXXXXXXXXX.cloudfront.net).

You should see the public landing page with the Welcome, visitor! heading, the description text, and a styled Sign In button.

  • Click Sign In.
  • On the Cognito login page, sign in with the account you created earlier.

After authenticating, you land back on your site. The page now shows your personalized welcome message with your email address. Below it, the members-only section appears with a light blue background and bold left border. A red Sign Out button sits below the content.

  • Click Sign Out.

You are redirected to Cognito's logout endpoint, which clears the server-side session. Then you land back on your site showing the public view with the Sign In button. The members-only content is gone.

  • Refresh the page to confirm the session is truly cleared.

After refreshing, the page remains on the public view. The sign-out successfully cleared sessionStorage and the Cognito session cookie.

Still seeing the logged-in view after sign out?

Check that your CloudFront URL is listed in the Allowed sign-out URLs in your Cognito app client settings. The logout_uri parameter must exactly match one of the allowed URLs (including the https:// prefix).

Also confirm your redirectUri variable in the script matches your actual CloudFront domain exactly.

help me debug why sign out is not working on my Cognito authenticated site

Your authentication system is complete. Users can sign up, sign in, see protected content, and sign out cleanly. Next up, you'll tackle the Secret Mission and extend this foundation with a profile card that displays all identity claims from the token.

Secret mission

Build a Profile Card with All Identity Claims

Your JWT token contains far more than just an email address. It holds a user ID, issuer, audience, timestamps, and more. In this secret mission, you will build a profile card that displays every claim from the decoded token with human-readable labels, and add expiration handling so your page gracefully recovers when a session times out.

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 only AWS free tier services, so there are no ongoing costs at this usage level.

Resources you used:

  • Amazon Cognito User Pool (free tier covers 10,000 MAUs indefinitely).
  • S3 Bucket (free tier covers 5GB storage for the first 12 months).
  • CloudFront Distribution (free tier covers 1TB data transfer/month for the first 12 months).

Keep everything running

No action needed. Choose this if you are still actively building on your authenticated site or plan to extend the Secret Mission further.

  • The Cognito free tier (10,000 monthly active users) does not expire after the 12-month AWS free tier period. It is free indefinitely.
  • S3 and CloudFront free tier allowances apply for the first 12 months of your AWS account. After that, costs for this project would be fractions of a cent per month given the minimal traffic.

Pause - I'll come back to this later

These AWS services cannot be paused or stopped. However, at this usage level they cost nothing within free tier limits.

  • Your Cognito User Pool, S3 bucket, and CloudFront distribution will remain idle but functional.
  • No charges will accrue as long as you stay within free tier limits (no significant traffic to your site).

Delete - I don't want to use this again

Remove all project resources and start fresh. Delete resources in this order to avoid dependency issues.

Delete the CloudFront distribution first:

  • Open the CloudFront console at https://console.aws.amazon.com/cloudfront/v4/home.
  • Select the checkbox next to your distribution.
  • Click Disable, then click Yes, Disable to confirm.
  • Wait until the Last modified column shows a new timestamp (this may take a few minutes as the change propagates to edge locations).
  • Select the checkbox again, click Delete, then click Delete to confirm.

Empty and delete the S3 bucket:

  • Open the S3 console and find your my-auth-site- bucket.
  • Select the bucket, click Empty, type permanently delete to confirm, and click Empty.
  • Return to the bucket list, select the bucket again, click Delete, type the bucket name to confirm, and click Delete bucket.

Delete the Cognito User Pool:

  • Open the Amazon Cognito console at https://console.aws.amazon.com/cognito/home.
  • Choose User Pools in the left navigation.
  • Select your user pool from the list.
  • Click Delete. If deletion protection is active, deactivate it in the same prompt by checking the box.
  • Type delete to confirm, then click Delete.

That's a Wrap!

That's a Wrap!

Nice work! You just built a real user authentication system on a static website powered by Amazon Cognito and plain JavaScript.

You've learned how to:

  • Created an Amazon Cognito User Pool with a managed login page that handles sign-up, sign-in, and email verification without writing any backend code.
  • Written your first JavaScript to parse a JWT token, decode its Base64URL payload, and extract real user identity claims.
  • Built a personalized page using the OAuth 2.0 implicit grant flow that shows different content for logged-in and logged-out users, with full sign-out via Cognito's logout endpoint.
  • Secret Mission: Built a profile card displaying all JWT claims with human-readable labels and handled token expiration gracefully.

Ready to quiz yourself?