Skip to main content

AWS Cognito User Authentication: An Architecture Deep-Dive

AWS Architecture Authentication Cognito Security

About the author: I'm Charles Sieg, a cloud architect and platform engineer who builds apps, services, and infrastructure for Fortune 1000 clients through Vantalect. If your organization is rethinking its software strategy in the age of AI-assisted engineering, let's talk.

User authentication looks simple from the outside. A sign-up form, a login page, maybe a "Forgot Password" link. Behind that surface sits a sprawling system of token management, federation protocols, MFA enrollment, session lifecycle, Lambda triggers, and security hardening decisions that are expensive to reverse once users are in the system. I have built authentication layers on AWS Cognito for applications ranging from internal tools with fifty users to consumer platforms with hundreds of thousands, and the lessons from those projects inform every recommendation in this article.

This is an architecture reference for building production authentication on AWS with Cognito. It covers email/password flows, social federation with Google, Facebook, and Apple, enterprise integration with Okta and Entra ID, the full token lifecycle, and the Python and React implementation patterns that hold up under real-world conditions.

The AWS Identity Landscape

AWS provides multiple identity services, each targeting a different problem. Cognito sits in the middle of this landscape, and understanding where it fits prevents architectural mistakes early on.

Cognito User Pools manage user directories and authentication flows. A User Pool stores user accounts (email, password, attributes), handles sign-up, sign-in, MFA, password recovery, and issues JWT tokens. It is an identity provider (IdP) that speaks OIDC and can federate with external IdPs.

Cognito Identity Pools (formerly Federated Identities) exchange tokens from any identity provider (including User Pools) for temporary AWS credentials. Identity Pools do not authenticate users. They assume an IAM role and hand back short-lived access keys. Use Identity Pools when your application needs to call AWS services directly from the client (S3 uploads, DynamoDB reads, IoT connections).

The Hosted UI is a pre-built, Cognito-managed authentication web interface that handles sign-up, sign-in, MFA, password recovery, and social/enterprise federation flows. It runs on a Cognito domain and redirects back to your application with authorization codes or tokens. The Hosted UI is functional but limited in customization.

Component Purpose Token Output When to Use
User Pool User directory, authentication, token issuance ID token, access token, refresh token (JWTs) Every application that authenticates users
Identity Pool Token exchange for AWS credentials Temporary AWS access key, secret key, session token Client-side access to AWS services (S3, DynamoDB, IoT)
Hosted UI Pre-built authentication pages Authorization code or tokens via redirect Rapid prototyping, social/enterprise federation without custom UI
Custom UI + SDK Your own authentication pages using AWS SDKs Same tokens as User Pool Full control over the authentication experience

Most applications need a User Pool. Some also need an Identity Pool. The decision to use the Hosted UI versus a custom UI depends on how much control you need over the user experience and whether you need social or enterprise federation (the Hosted UI handles the OAuth redirect dance automatically).

Cognito User Pool Architecture

Schema Design

A User Pool's attribute schema is defined at creation and largely immutable afterward. Adding new custom attributes is possible. Removing them, renaming them, or changing their data type is not. This makes schema design one of the highest-stakes decisions in your Cognito deployment.

Standard attributes map to OIDC claims and include email, phone_number, name, given_name, family_name, address, birthdate, gender, locale, preferred_username, and others. Custom attributes use the prefix custom: and support String, Number, DateTime, and Boolean types.

Decision Reversible Impact
Pool creation (region, name) No: cannot move a User Pool between regions Users are tied to the pool's region; pick the region closest to your primary user base
Attribute schema (standard attributes) No: cannot remove standard attributes once enabled Enable only the standard attributes you actually need
Custom attributes Partial: can add new ones, cannot remove or rename existing ones Plan custom attributes carefully; unused attributes persist forever
Username configuration (email, phone, or username) No: set at pool creation, immutable Determines sign-in identifier; email-as-username is the most common pattern
Case sensitivity No: set at pool creation Case-insensitive usernames prevent duplicate accounts (e.g., "User@email.com" vs "user@email.com")
MFA configuration Partial: can change between off/optional/required, but cannot disable if set to required via certain paths Start with "optional" to give yourself flexibility
Password policy Yes: can change at any time Only applies to new passwords; existing passwords are unaffected
Deletion protection Yes: can enable/disable Enable in production to prevent accidental pool deletion

My recommendation: enable only email and name as standard attributes. Store everything else in your application database with the Cognito sub (user ID) as the foreign key. Cognito's attribute storage is not a general-purpose database. It has a 50-custom-attribute limit, no query capability beyond lookup by username or sub, and no way to remove attributes once created. Keep Cognito lean: authentication and identity only.

Password Policy

Cognito enforces configurable password policies:

Parameter Default Range Recommendation
Minimum length 8 6-99 12 characters minimum for production
Require uppercase Yes Yes/No Yes
Require lowercase Yes Yes/No Yes
Require numbers Yes Yes/No Yes
Require symbols Yes Yes/No Optional (length matters more than complexity)
Temporary password validity 7 days 1-365 days 7 days

Password policies apply to new passwords only. Changing the policy does not invalidate existing passwords. If you tighten the policy from 8 to 12 characters, users with 8-character passwords continue to sign in successfully until they change their password.

App Clients

An app client represents an application that can interact with the User Pool. Each app client has its own configuration for authentication flows, token expiration, OAuth scopes, and callback URLs. A single User Pool can have multiple app clients (web app, mobile app, admin portal) with different settings.

Setting Purpose Recommendation
Client secret Enables confidential client flow (server-side) Enable for server-side apps; disable for SPAs and mobile apps
Auth flows Which authentication APIs the client can call Enable only the flows you use; ALLOW_USER_SRP_AUTH for password-based, ALLOW_REFRESH_TOKEN_AUTH for token refresh
Token expiration ID token (5 min to 1 day), access token (5 min to 1 day), refresh token (1 hour to 10 years) ID/access: 1 hour; refresh: 30 days for most applications
OAuth scopes openid, email, profile, phone, custom scopes openid email profile covers most use cases
Callback URLs Allowed redirect URIs after authentication Whitelist exact URLs; no wildcards in production

Authentication Flows

Email/Password Sign-Up

The sign-up flow creates a new user account, sends a verification code, and confirms the user's email address. Lambda triggers fire at specific points, enabling custom validation and side effects.

SignUp(email, password, attributes) Pre Sign-Up trigger Allow / Deny / Auto-confirm SignUp response (UserSub) Custom Message trigger (verification code) Customized email template Verification code sent via email ConfirmSignUp(email, code) Post Confirmation trigger Side effects (create DB record, send welcome email) Confirmation success Client Cognito Pre Sign-UpLambda Custom MessageLambda Post ConfirmationLambda
Email/password sign-up flow with Lambda triggers

The Pre Sign-Up trigger is your first line of defense. Use it to validate email domains (block disposable email providers), enforce custom registration rules (invitation-only sign-up), or auto-confirm users from trusted sources. Returning a denial from the Pre Sign-Up trigger prevents the account from being created entirely.

The Custom Message trigger lets you replace the default verification email with your own branded template. Cognito passes the verification code to your Lambda function, and you return the email body (HTML or plain text) that Cognito sends. This is the only way to customize the verification email beyond Cognito's basic template editor.

Sign-In with MFA

The sign-in flow authenticates the user and optionally challenges for MFA. Cognito uses the Secure Remote Password (SRP) protocol by default, which verifies the password without transmitting it over the network.

alt [MFA Enabled] InitiateAuth(SRP_AUTH, email) Pre Authentication trigger Allow / Deny SRP challenge (PASSWORD_VERIFIER) RespondToAuthChallenge(SRP proof) MFA challenge (SMS_MFA or SOFTWARE_TOKEN_MFA) RespondToAuthChallenge(MFA code) Post Authentication trigger Side effects (log sign-in, update last login) Authentication result (ID token, access token, refresh token) Client Cognito Pre AuthenticationLambda Post AuthenticationLambda
Sign-in flow with optional MFA challenge

The Pre Authentication trigger runs before password verification. Use it for custom rate limiting, account lockout logic, or IP-based access control that goes beyond Cognito's built-in advanced security features.

Token expiration starts from the moment of issuance. If your ID and access tokens expire in 1 hour, the client must use the refresh token to obtain new tokens before the hour elapses. The refresh token itself has a separate, longer expiration (configurable up to 10 years).

Forgot Password

The forgot password flow resets the user's password via a verification code sent to their confirmed email or phone number.

ForgotPassword(email) Custom Message trigger (reset code) Customized email template CodeDeliveryDetails (email destination) ConfirmForgotPassword(email, code, new_password) Password reset success User receives code via email Client Cognito Custom MessageLambda
Forgot password and reset flow

Cognito does not reveal whether an email address exists in the User Pool during the forgot password flow (when configured with the recommended "prevent user existence errors" setting). This prevents account enumeration attacks where an attacker probes email addresses to discover which ones have accounts.

Social Federation (Google, Facebook, Apple)

Social federation delegates authentication to an external identity provider. The user authenticates with Google, Facebook, or Apple, and Cognito creates or links a local user account with the federated identity.

alt [New User] Redirect to /authorize?identity_provider=Google Redirect to provider's login Authorization code Exchange code for tokens Provider tokens (ID token, access token) Map provider attributes to Cognito attributes Pre Sign-Up trigger Allow / Auto-confirm / Auto-link Cognito authorization code Redirect to callback URL with code Exchange code for Cognito tokens ID token, access token, refresh token Client CognitoHosted UI Social Provider(Google/Facebook/Apple) Cognito Pre Sign-UpLambda
Social federation flow via Hosted UI

Each social provider requires specific configuration in both the provider's developer console and in Cognito:

Provider Developer Console Cognito Configuration Attribute Mapping Notes
Google Google Cloud Console: create OAuth 2.0 credentials, configure consent screen Identity provider type: Google; provide client ID and secret; scopes: openid email profile sub maps to Cognito username; email and name map directly
Facebook Meta for Developers: create app, configure Facebook Login product Identity provider type: Facebook; provide app ID and secret; scopes: public_profile,email Facebook uses numeric IDs; email requires explicit permission
Apple Apple Developer: register Services ID, configure Sign in with Apple Identity provider type: Sign in with Apple; provide Services ID, Team ID, Key ID, and private key Apple may not return email after first sign-in; handle accordingly

The Pre Sign-Up trigger handles a critical decision for federated users: whether to auto-link a federated identity with an existing Cognito account that shares the same email address. Without auto-linking, a user who signs up with email/password and later signs in with Google gets two separate accounts. The Pre Sign-Up trigger can detect this overlap and link the federated identity to the existing account.

Enterprise Federation

Enterprise federation connects Cognito to corporate identity providers like Okta or Microsoft Entra ID (formerly Azure AD). Employees authenticate with their corporate credentials, and Cognito issues application-specific tokens.

SAML vs. OIDC Federation

Cognito supports both SAML 2.0 and OIDC federation with enterprise identity providers:

Aspect SAML 2.0 OIDC
Protocol maturity Established standard since 2005; ubiquitous in enterprise Newer, simpler; built on OAuth 2.0
Token format XML-based SAML assertions JWT tokens
Setup complexity Higher: metadata XML, assertion consumer service URL, certificate management Lower: issuer URL, client ID, client secret
Attribute mapping SAML attribute statements mapped to Cognito attributes OIDC claims mapped to Cognito attributes
Provider support Universal: every enterprise IdP supports SAML Growing: most modern IdPs support OIDC
Recommendation Use when the IdP only supports SAML or when the enterprise mandates SAML Prefer OIDC when both options are available; simpler setup and maintenance

Okta Integration

Okta is the most common enterprise IdP I encounter in production Cognito deployments. The setup involves creating an application in Okta and configuring Cognito to trust Okta as a SAML or OIDC provider.

For SAML integration with Okta:

  1. Create a SAML 2.0 application in the Okta admin console.
  2. Set the Single Sign-On URL to https://<cognito-domain>.auth.<region>.amazoncognito.com/saml2/idpresponse.
  3. Set the Audience URI (SP Entity ID) to urn:amazon:cognito:sp:<user-pool-id>.
  4. Configure attribute statements to map Okta user attributes (email, firstName, lastName) to SAML assertion attributes.
  5. Download the Okta metadata XML.
  6. In Cognito, create a SAML identity provider and upload the metadata XML.
  7. Map SAML attributes to Cognito user attributes.
  8. Add Okta as a supported identity provider on the app client.

For OIDC integration with Okta:

  1. Create an OIDC application in the Okta admin console.
  2. Set the sign-in redirect URI to https://<cognito-domain>.auth.<region>.amazoncognito.com/oauth2/idpresponse.
  3. Note the client ID and client secret.
  4. In Cognito, create an OIDC identity provider with the Okta issuer URL (https://<okta-domain>.okta.com).
  5. Provide the client ID, client secret, and requested scopes (openid email profile).
  6. Map OIDC claims to Cognito attributes.

Entra ID Integration

Microsoft Entra ID (Azure AD) integration follows the same patterns as Okta with Entra-specific configuration:

  1. Register an application in the Entra ID portal.
  2. Configure the redirect URI: https://<cognito-domain>.auth.<region>.amazoncognito.com/oauth2/idpresponse.
  3. Create a client secret in Certificates & secrets.
  4. Note the Application (client) ID and Directory (tenant) ID.
  5. In Cognito, create an OIDC identity provider with the issuer URL: https://login.microsoftonline.com/<tenant-id>/v2.0.
  6. Provide the client ID, client secret, and scopes (openid email profile).
  7. Map Entra ID claims to Cognito attributes.

Entra ID returns the oid claim as the unique user identifier and preferred_username as the user's email in many configurations. Verify the claim names in your tenant's token configuration; they vary based on the Entra ID edition and application registration settings.

Click "Sign in with SSO" Redirect to /authorize?identity_provider=OktaSAML SAML AuthnRequest Corporate login page Enter corporate credentials + MFA SAML Response (signed assertion) Validate assertion signature,map attributes, create/update user Redirect with authorization code Exchange code for tokens ID token, access token, refresh token User Application CognitoHosted UI Enterprise IdP(Okta / Entra ID)
Enterprise SAML federation with Okta or Entra ID

SCIM Provisioning

Cognito does not support SCIM (System for Cross-domain Identity Management) natively. SCIM automates user provisioning and deprovisioning: when an employee is added or removed in the corporate IdP, the change propagates to downstream applications automatically.

Without SCIM, Cognito relies on just-in-time (JIT) provisioning. A user account is created in Cognito the first time the user authenticates via federation. Deprovisioning is the gap: disabling a user in Okta prevents new sign-ins (because authentication goes through Okta first), but the Cognito user record persists, and any existing refresh tokens remain valid until they expire.

For applications that require prompt deprovisioning, implement one of these patterns:

  • Short token expiration. Set access and ID token expiration to 15-30 minutes and refresh token expiration to 1-8 hours. When the refresh token expires, the user must re-authenticate through the IdP, which will reject them if their account is disabled.
  • Token revocation webhook. Build a webhook endpoint that the IdP calls when a user is deprovisioned. The webhook calls AdminUserGlobalSignOut to invalidate all of the user's tokens immediately.
  • Periodic sync. Run a scheduled Lambda function that queries the IdP's user directory (via SCIM or the IdP's API) and disables or deletes Cognito users that no longer exist in the IdP.

Token Management and JWT

Cognito issues three tokens upon successful authentication. Understanding each token's purpose, structure, and lifecycle is essential for building secure applications.

Token Types

Token Purpose Contains Default Expiration Configurable Range
ID token Identity assertion: who the user is User attributes (email, name, custom attributes), Cognito groups, authentication time 1 hour 5 minutes to 1 day
Access token Authorization: what the user can do Scopes, Cognito groups, client ID, username 1 hour 5 minutes to 1 day
Refresh token Obtain new ID and access tokens without re-authentication Opaque token (not a JWT) 30 days 1 hour to 10 years

The ID token and access token are JWTs signed with RS256 using the User Pool's RSA key pair. The refresh token is an opaque string that Cognito stores and validates server-side.

JWT Structure

A Cognito ID token contains these claims:

Claim Description Example
sub User's unique identifier (UUID) a1b2c3d4-5678-90ab-cdef-example11111
iss Issuer: the User Pool URL https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123
aud Audience: the app client ID 1example23456789
token_use Token type id
auth_time Authentication timestamp (epoch) 1708700000
exp Expiration timestamp (epoch) 1708703600
iat Issued at timestamp (epoch) 1708700000
email User's email address user@example.com
email_verified Whether the email is verified true
cognito:groups Groups the user belongs to ["admin", "editors"]
custom:* Custom attributes custom:tenant_id: "acme-corp"

JWT Verification

Every API request carrying a Cognito JWT must be verified before trusting its claims. The verification process:

  1. Decode the JWT header to get the key ID (kid).
  2. Fetch the JWKS (JSON Web Key Set) from https://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/jwks.json. Cache this; the keys change infrequently.
  3. Find the matching public key by kid in the JWKS.
  4. Verify the signature using the RSA public key.
  5. Check the expiration (exp claim). Reject expired tokens.
  6. Verify the issuer (iss claim). It must match your User Pool URL.
  7. Verify the audience (aud claim for ID tokens) or client ID (client_id claim for access tokens).
  8. Verify the token use (token_use claim). Use id for ID tokens and access for access tokens.

Skipping any of these steps creates vulnerabilities. I have seen applications that verify the signature but skip the audience check, which means any valid Cognito token from any app client (or even a different User Pool with the same signing key rotation) passes verification.

Refresh Token Strategy

The refresh token is the most security-sensitive token. It can obtain new ID and access tokens without user interaction, and it has the longest lifetime. If compromised, an attacker has persistent access until the refresh token expires or is explicitly revoked.

Best practices for refresh token management:

  • Store refresh tokens server-side whenever possible. For SPAs, store in an HttpOnly, Secure, SameSite=Strict cookie. Never store refresh tokens in localStorage or sessionStorage.
  • Set appropriate expiration. 30 days is reasonable for consumer applications. 1-8 hours for high-security applications. For enterprise applications with SSO, shorter refresh token expiration forces periodic re-authentication through the corporate IdP.
  • Implement token rotation. Cognito supports refresh token rotation: each time a refresh token is used, a new refresh token is issued and the old one is invalidated. This limits the window of exposure if a refresh token is intercepted.
  • Support explicit revocation. Call RevokeToken when the user signs out, or AdminUserGlobalSignOut to revoke all tokens for a user. Revocation invalidates the refresh token immediately; ID and access tokens remain valid until they expire (they are stateless JWTs and cannot be revoked individually).

Custom Claims via Pre Token Generation

The Pre Token Generation Lambda trigger fires before Cognito issues tokens, allowing you to add, modify, or suppress claims in the ID and access tokens.

Common use cases:

  • Add application-specific claims. Inject tenant ID, subscription tier, feature flags, or role information from your application database into the token. This avoids a secondary lookup on every API request.
  • Remove sensitive attributes. Suppress claims that should not appear in the token (e.g., internal user metadata).
  • Override group claims. Modify the cognito:groups claim based on external authorization logic.

The V2 trigger event (TokenGeneration_V2) supports modifying access token claims and scopes in addition to ID token claims. The V1 trigger only supports ID token customization.

MFA Patterns

Multi-factor authentication adds a second verification step beyond the password. Cognito supports three MFA methods, each with different security properties and user experience trade-offs.

Method Security Level User Experience Phishing Resistance Availability
SMS Moderate: vulnerable to SIM swapping and SS7 interception Familiar; no app required Low: codes can be phished Generally available; requires SNS configuration
TOTP (authenticator app) High: codes generated locally, not transmitted Requires setup with authenticator app (Google Authenticator, Authy, 1Password) Moderate: codes can be phished but are time-limited Generally available
Email Moderate: dependent on email account security Familiar; code sent to email Low: codes can be phished Available (added in 2024)

TOTP Setup

TOTP (Time-based One-Time Password) is the recommended MFA method for most applications. The setup flow:

  1. User signs in and calls AssociateSoftwareToken with their access token.
  2. Cognito returns a secret key (Base32-encoded).
  3. The application displays the secret as a QR code (using the otpauth:// URI format).
  4. The user scans the QR code with their authenticator app.
  5. The user enters the current TOTP code displayed in the app.
  6. The application calls VerifySoftwareToken with the code.
  7. Cognito verifies the code and enables TOTP MFA for the user.
  8. The application calls SetUserMFAPreference to set TOTP as the preferred MFA method.

Adaptive Authentication

Cognito Advanced Security Features provide risk-based adaptive authentication. Cognito analyzes contextual signals (IP address, device fingerprint, location, login history) and assigns a risk level to each authentication attempt:

Risk Level Trigger Examples Configurable Actions
Low Known device, familiar IP, normal time Allow (no additional challenge)
Medium New device, unfamiliar IP, unusual time Optional MFA, notify user
High Impossible travel, known malicious IP, credential stuffing pattern Block, require MFA, notify user

Advanced Security costs $0.050 per MAU (monthly active user) beyond the free tier. For applications with security-sensitive user bases (financial services, healthcare), the cost is justified. For low-risk applications, standard MFA enforcement may suffice.

React Frontend Integration

Approach Comparison

Three approaches exist for integrating Cognito authentication into a React application:

Approach Setup Effort Customization Bundle Size Impact Best For
AWS Amplify Low: npm install aws-amplify; pre-built UI components and hooks Moderate: themed components, custom form fields, limited layout control Large: ~80-150 KB gzipped (Amplify auth module) Rapid prototyping; teams that want managed UI components
Hosted UI redirect Very low: redirect to Cognito domain, handle callback Low: CSS customization only, fixed layout and flow Minimal: no client-side auth library required Enterprise SSO where the login page is secondary to the IdP experience
Direct SDK (amazon-cognito-identity-js or @aws-sdk/client-cognito-identity-provider) High: implement every flow manually Full: complete control over UI, flow, and error handling Small: ~15-30 KB gzipped Production applications requiring full control over the authentication experience

My recommendation for production applications: use the direct SDK approach. The Amplify UI components are excellent for prototyping, but production applications inevitably need customization that fights against Amplify's opinions (custom error handling, branded loading states, non-standard flows like invitation-based sign-up). Starting with the direct SDK avoids a migration later.

Token Storage Security

Where and how you store tokens in the browser has significant security implications:

Storage Method XSS Vulnerability CSRF Vulnerability Recommendation
localStorage High: any XSS attack can read tokens None: JavaScript-only access Avoid for refresh tokens; acceptable for short-lived access tokens if CSP is strict
sessionStorage High: same as localStorage but scoped to tab None Marginally better than localStorage (cleared on tab close)
HttpOnly cookie None: JavaScript cannot access Moderate: mitigated with SameSite=Strict Best for refresh tokens; requires a backend endpoint to set the cookie
In-memory (JavaScript variable) Moderate: accessible via XSS but not persisted None Best for access tokens in SPAs; tokens are lost on page refresh

The secure pattern for SPAs:

  1. Store the refresh token in an HttpOnly, Secure, SameSite=Strict cookie set by your backend.
  2. Store the access token and ID token in memory (JavaScript variable).
  3. On page load, call your backend's token refresh endpoint, which reads the refresh token from the cookie and returns new access and ID tokens.
  4. The access token in memory is used for API calls and refreshed automatically before expiration.

This pattern prevents XSS attacks from stealing the refresh token (HttpOnly makes it inaccessible to JavaScript) and prevents CSRF attacks from using the cookie (SameSite=Strict blocks cross-origin requests).

Python Backend Integration

boto3 Operations

The boto3 Cognito Identity Provider client provides two categories of operations: user-facing operations (called with the user's tokens) and admin operations (called with AWS credentials).

Key admin operations:

import boto3

cognito = boto3.client("cognito-idp", region_name="us-east-1")

# Create a user (admin-initiated sign-up)
cognito.admin_create_user(
    UserPoolId="us-east-1_ABC123",
    Username="user@example.com",
    UserAttributes=[
        {"Name": "email", "Value": "user@example.com"},
        {"Name": "email_verified", "Value": "true"},
    ],
    DesiredDeliveryMediums=["EMAIL"],
)

# Disable a user (preserve account but prevent sign-in)
cognito.admin_disable_user(
    UserPoolId="us-east-1_ABC123",
    Username="user@example.com",
)

# Add user to a group
cognito.admin_add_user_to_group(
    UserPoolId="us-east-1_ABC123",
    Username="user@example.com",
    GroupName="admin",
)

# Sign out user globally (revoke all tokens)
cognito.admin_user_global_sign_out(
    UserPoolId="us-east-1_ABC123",
    Username="user@example.com",
)

JWT Verification Middleware

Server-side JWT verification in Python using python-jose and requests:

import json
import time
from functools import wraps

import requests
from jose import jwt, JWTError

REGION = "us-east-1"
USER_POOL_ID = "us-east-1_ABC123"
APP_CLIENT_ID = "1example23456789"
ISSUER = f"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}"
JWKS_URL = f"{ISSUER}/.well-known/jwks.json"

# Cache JWKS keys
_jwks_cache = None
_jwks_cache_time = 0
JWKS_CACHE_DURATION = 3600  # 1 hour


def get_jwks():
    global _jwks_cache, _jwks_cache_time
    if _jwks_cache and (time.time() - _jwks_cache_time) < JWKS_CACHE_DURATION:
        return _jwks_cache
    response = requests.get(JWKS_URL)
    _jwks_cache = response.json()["keys"]
    _jwks_cache_time = time.time()
    return _jwks_cache


def verify_token(token, token_use="access"):
    jwks = get_jwks()
    headers = jwt.get_unverified_headers(token)
    kid = headers["kid"]

    key = next((k for k in jwks if k["kid"] == kid), None)
    if not key:
        raise JWTError("Public key not found in JWKS")

    claims = jwt.decode(
        token,
        key,
        algorithms=["RS256"],
        audience=APP_CLIENT_ID if token_use == "id" else None,
        issuer=ISSUER,
    )

    if claims.get("token_use") != token_use:
        raise JWTError(f"Invalid token_use: expected {token_use}")

    return claims

Lambda Triggers Reference

Cognito Lambda triggers execute custom logic at specific points in the authentication lifecycle. Each trigger receives a specific event structure and can modify the flow.

Trigger When It Fires Common Use Cases Can Block Flow
Pre Sign-Up Before creating a new user Validate email domain, enforce invitation codes, auto-confirm users, auto-link federated accounts Yes: return error to deny sign-up
Pre Authentication Before password verification Custom rate limiting, IP-based blocking, account lockout logic Yes: return error to deny sign-in
Post Authentication After successful authentication Log sign-in events, update last login timestamp, trigger analytics No
Post Confirmation After user confirms their account Create application database record, send welcome email, assign default group No
Pre Token Generation Before issuing tokens Add custom claims, modify groups, suppress attributes No (can modify tokens)
Custom Message Before sending email/SMS Custom verification email templates, branded password reset messages No (can modify message)
Define Auth Challenge During custom authentication flow Implement CAPTCHA, knowledge-based auth, or custom MFA Controls flow
Create Auth Challenge During custom authentication flow Generate challenge parameters (CAPTCHA image, question) Controls flow
Verify Auth Challenge During custom authentication flow Validate challenge response Controls flow
User Migration When user signs in but does not exist in pool Migrate users from legacy auth system on first sign-in Yes: return user data to create account

The User Migration trigger deserves special attention. It enables zero-downtime migration from a legacy authentication system. When a user who does not exist in Cognito attempts to sign in, the trigger fires with the username and password. Your Lambda function validates the credentials against the legacy system and, if valid, returns the user's attributes. Cognito creates the user account and completes the sign-in. Subsequent sign-ins go directly through Cognito. Over time, your entire user base migrates without any user-facing disruption.

Session Management and Security

Session Architecture

A production authentication system manages multiple session layers:

Layer Managed By Lifetime Storage
Cognito session Cognito (refresh token) Configurable (1 hour to 10 years) Cognito service (server-side)
Application session Your backend Typically 15-60 minutes (access token expiration) In-memory or session store (Redis, DynamoDB)
Browser session Cookies or in-memory tokens Until tab close (sessionStorage) or explicit expiration (cookies) Browser
Hosted UI session Cognito Hosted UI cookie 1 hour (not configurable) Browser cookie on Cognito domain

The Hosted UI session creates a subtle behavior that catches teams off guard. After a user authenticates through the Hosted UI, Cognito sets a session cookie on its domain. If the user navigates back to the Hosted UI within the session lifetime, Cognito skips the login form and immediately redirects with new tokens. This is convenient for SSO but can conflict with explicit sign-out expectations. Calling the /logout endpoint on the Cognito domain clears this session cookie.

Security Hardening Checklist

Control Implementation Impact
Enable deletion protection User Pool settings Prevents accidental pool deletion
Configure account recovery Set recovery to verified email only (not phone) Reduces attack surface for account takeover
Enable advanced security User Pool advanced security settings Adds risk-based adaptive auth, compromised credential detection
Block sign-in after failed attempts Advanced security automatic risk response Prevents brute-force attacks
Use case-insensitive usernames User Pool creation setting (immutable) Prevents duplicate accounts
Prevent user existence errors App client setting: PreventUserExistenceErrors=ENABLED Prevents account enumeration attacks
Enforce MFA MFA configuration: required or optional Reduces account takeover risk
Restrict callback URLs App client OAuth configuration Prevents open redirect attacks
Set minimum token expiration App client token expiration settings Limits exposure window for stolen tokens
Enable token revocation App client setting Allows global sign-out to invalidate refresh tokens
Restrict auth flows App client: enable only required auth flows Reduces attack surface
Configure WAF on Cognito Attach WAF web ACL to User Pool Rate limiting, IP blocking, bot protection on auth endpoints

WAF integration with Cognito (added in 2022) is significant. Cognito's authentication endpoints (/oauth2/token, the hosted UI, the API) are public by default. Without WAF, they are exposed to credential stuffing, brute-force attacks, and bots. Attach a WAF web ACL with rate-based rules, AWS Managed Rules for known bad inputs, and bot control rules.

Cost Model

Pricing Tiers

Cognito pricing is based on monthly active users (MAU). A user is counted as active if they perform any authentication operation (sign-up, sign-in, token refresh, password change) during the calendar month.

Tier Price per MAU Notes
First 10,000 MAU Free Free tier applies to User Pool only
10,001 to 100,000 $0.0055
100,001 to 1,000,000 $0.0046
1,000,001 to 10,000,000 $0.00325
Over 10,000,000 $0.0025

Additional costs:

Feature Cost Notes
Advanced Security $0.050 per MAU Risk-based adaptive auth, compromised credentials detection
SMS MFA SNS pricing ($0.00645+ per message in the US) Varies significantly by destination country
Email delivery (beyond 50/day) SES pricing ($0.10 per 1,000 emails) Free tier covers 50 emails per day
SAML/OIDC federation $0.015 per MAU Applies only to users who authenticate via SAML or OIDC

Cost at Scale

Scale MAU Base Cost With Advanced Security With SAML (50% of users) Total
Startup 5,000 Free $250/mo N/A $0 - $250/mo
Growth 50,000 $220/mo $2,500/mo $375/mo $220 - $3,095/mo
Scale-up 500,000 $2,300/mo $25,000/mo $3,750/mo $2,300 - $31,050/mo
Enterprise 2,000,000 $7,575/mo $100,000/mo $15,000/mo $7,575 - $122,575/mo

The free tier of 10,000 MAU is generous for small applications and development environments. Advanced Security pricing scales linearly and becomes a significant cost at scale. Evaluate whether the risk-based features justify the cost for your security requirements; many applications achieve adequate security with standard MFA enforcement and WAF rules at a fraction of the cost.

SAML federation pricing ($0.015 per MAU) applies per federated user. For enterprise applications where a large percentage of users authenticate via SAML, this cost adds up. The pricing is the same regardless of how many SAML IdPs you configure.

Cognito vs. Alternatives

Dimension Cognito Auth0 Firebase Auth Keycloak
Hosting model Managed (AWS) Managed (Okta) Managed (Google) Self-hosted (open source)
Free tier 10,000 MAU 7,500 MAU 50,000 MAU (phone auth: 10K verifications) Unlimited (you pay for infrastructure)
Cost at 100K MAU ~$495/mo ~$228/mo (B2C Essentials) ~$0 (within free tier for email/password) Infrastructure cost only (~$100-500/mo for hosting)
Cost at 1M MAU ~$4,850/mo Custom pricing (typically $5,000-15,000/mo) ~$0 (email/password); phone auth adds cost Infrastructure cost (~$500-2,000/mo)
Social federation Google, Facebook, Apple, Amazon, OIDC 30+ social providers Google, Facebook, Apple, Twitter, GitHub, and more Any OIDC/SAML provider
Enterprise SSO (SAML) Yes (+$0.015/MAU) Yes (included in Enterprise plans) No native SAML support Yes (included)
Customization Limited: Hosted UI CSS, Lambda triggers Extensive: Actions, Forms, Branding Moderate: UI SDK customization Full: open source, modify anything
Multi-tenancy Manual: separate pools or group-based isolation Native: Organizations feature Manual: Firebase projects or custom claims Native: realms
AWS integration Deep: IAM, API Gateway, ALB, AppSync, Identity Pools Via OIDC/SAML standard protocols Google Cloud native Via OIDC/SAML standard protocols
Lock-in risk Moderate: standard OIDC/JWT but migration requires re-registration Moderate: proprietary extensions but standard protocols Moderate: Firebase-specific SDKs Low: open source, standard protocols
Operational burden None (managed) None (managed) None (managed) High: upgrades, scaling, monitoring, backups

When Cognito is the right choice: your application runs on AWS, you need deep AWS service integration (API Gateway authorizers, ALB authentication, Identity Pools for direct AWS access), and your customization needs can be met with Lambda triggers.

When to consider alternatives: you need extensive social provider coverage (Auth0), your users are primarily on Google Cloud (Firebase Auth), you need full control and cannot accept vendor lock-in (Keycloak), or you need native multi-tenant support with per-organization branding (Auth0 Organizations).

Common Failure Modes

Production Cognito deployments exhibit recurring failure patterns. Knowing these in advance saves hours of debugging.

# Failure Mode Cause Mitigation
1 Token validation rejects valid tokens JWKS cache is stale after key rotation Cache JWKS with TTL of 1 hour maximum; implement fallback that re-fetches JWKS on signature verification failure
2 Duplicate user accounts for same person Social login and email/password create separate accounts Implement account linking in Pre Sign-Up trigger; match on verified email
3 Rate limiting on authentication endpoints Cognito has per-User Pool rate limits (default varies by operation) Implement client-side exponential backoff; request limit increases via AWS Support; distribute load across multiple app clients
4 Lambda trigger timeout causes auth failure Trigger Lambda exceeds 5-second Cognito timeout Keep trigger functions lightweight; avoid synchronous external API calls; use async patterns for heavy operations
5 Custom attribute cannot be removed Custom attributes are append-only by design Plan schema carefully before launch; store volatile attributes in your application database
6 Hosted UI session persists after sign-out Application clears its own session but does not call Cognito's /logout endpoint Redirect to https://<domain>/logout?client_id=<id>&logout_uri=<uri> during sign-out
7 Federation attribute mapping mismatch IdP returns attributes with unexpected names or formats Test attribute mapping with each IdP; log the raw SAML assertion or OIDC claims during integration testing
8 Email delivery failures Cognito's default email sender hits the 50/day limit Configure Amazon SES as the email sender for production workloads
9 Refresh token revocation does not invalidate access tokens Access tokens are stateless JWTs; revocation only affects refresh tokens Set short access token expiration (15-60 minutes); implement a token deny-list for immediate revocation requirements
10 User Pool cannot be moved between regions User Pools are regional resources with no cross-region replication Choose the region closest to your primary user base at creation; for global applications, consider multiple User Pools with a federation layer

Key Architectural Patterns

After building authentication systems on Cognito for applications of various sizes and complexity levels, these are the patterns I follow consistently:

  1. Keep Cognito lean. Store only authentication-essential attributes in the User Pool. Use sub as the foreign key to your application database for everything else. Cognito is an identity provider, not a user profile database.
  2. Use email as the username. Configure the User Pool with email as the sign-in alias. It is the most natural identifier for users, eliminates the "forgot username" flow, and simplifies social federation account linking.
  3. Start with MFA optional, enforce via policy. Create the User Pool with MFA set to "optional" rather than "required." This gives you the flexibility to enforce MFA for specific user groups (admins, enterprise users) while keeping the sign-up friction low for consumer users. Changing from "optional" to "required" is straightforward; the reverse is more constrained.
  4. Configure SES for email delivery from day one. The default Cognito email sender is limited to 50 emails per day and uses a generic FROM address. Set up Amazon SES with your domain, verify it, and configure Cognito to send through SES. The cost is negligible ($0.10 per 1,000 emails), and your verification and password reset emails will actually reach your users' inboxes.
  5. Implement the Pre Sign-Up trigger for account linking. When a user authenticates via social federation (Google, Facebook) with the same email as an existing email/password account, the Pre Sign-Up trigger should detect the overlap and link the accounts. Without this, users accumulate duplicate accounts.
  6. Set short access token expiration. Access and ID tokens should expire in 15-60 minutes. The refresh token handles seamless re-authentication. Short-lived access tokens limit the damage window if a token is compromised and reduce the impact of the revocation gap (where revoked refresh tokens cannot invalidate already-issued access tokens).
  7. Attach WAF to the User Pool. Authentication endpoints are prime targets for credential stuffing and brute-force attacks. A WAF web ACL with rate-based rules, bot control, and AWS Managed Rules provides defense that Cognito's built-in protections do not fully cover.
  8. Use the User Migration trigger for zero-downtime migration. Migrating from a legacy auth system does not require a big-bang cutover. The User Migration trigger validates credentials against the old system on first sign-in and creates the Cognito account transparently. Users never know they were migrated.
  9. Test federation attribute mapping thoroughly. Every IdP returns attributes with slightly different names, formats, and optional fields. Apple may not return the email after the first sign-in. Entra ID may use preferred_username or email depending on configuration. Test each IdP integration with real accounts and verify the mapped attributes in Cognito.
  10. Plan for the User Pool's immutable decisions. Region, username configuration, case sensitivity, and attribute schema cannot be changed after creation. Document these decisions and their rationale. Recreating a User Pool means migrating all users, which is operationally expensive and disruptive.
Yes Yes No No Yes Yes No No Yes No AuthenticationRequirement Users areinternal employees? Corporate IdPavailable? Cognito + SAML/OIDCFederation with Okta or Entra ID Cognito User Poolwith email/password + MFA Need sociallogin? Custom UIrequired? Cognito User Pool +Direct SDK + Social Providers Cognito User Pool +Hosted UI + Social Providers Client needsdirect AWS access? Cognito User Pool +Identity Pool Cognito User Poolwith email/password + MFA
Authentication architecture decision tree

Additional Resources

  • AWS Cognito Developer Guide: comprehensive reference covering User Pools, Identity Pools, and the Hosted UI
  • Cognito User Pool Lambda triggers: event structure, configuration, and code examples for all ten trigger types
  • JWT verification in different languages: AWS documentation on validating Cognito JWTs in Python, Node.js, Java, and Go
  • Cognito pricing: current MAU pricing tiers, Advanced Security costs, and federation surcharges
  • SAML federation with Cognito: step-by-step configuration guides for Okta, Entra ID, and generic SAML providers
  • Cognito WAF integration: attaching WAF web ACLs to User Pools for authentication endpoint protection

See also: Amazon API Gateway: An Architecture Deep-Dive for Cognito authorizer integration with API Gateway, AWS Elastic Load Balancing: An Architecture Deep-Dive for ALB authentication with Cognito, and Amazon CloudFront: An Architecture Deep-Dive for WAF integration at the edge.

Let's Build Something!

I help teams ship cloud infrastructure that actually works at scale. Whether you're modernizing a legacy platform, designing a multi-region architecture from scratch, or figuring out how AI fits into your engineering workflow, I've seen your problem before. Let me help.

Currently taking on select consulting engagements through Vantalect.