Skip to main content

AWS Cognito User Authentication: An Architecture Deep-Dive

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.

ComponentPurposeToken OutputWhen to Use
User PoolUser directory, authentication, token issuanceID token, access token, refresh token (JWTs)Every application that authenticates users
Identity PoolToken exchange for AWS credentialsTemporary AWS access key, secret key, session tokenClient-side access to AWS services (S3, DynamoDB, IoT)
Hosted UIPre-built authentication pagesAuthorization code or tokens via redirectRapid prototyping, social/enterprise federation without custom UI
Custom UI + SDKYour own authentication pages using AWS SDKsSame tokens as User PoolFull 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.

DecisionReversibleImpact
Pool creation (region, name)No: cannot move a User Pool between regionsUsers 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 enabledEnable only the standard attributes you actually need
Custom attributesPartial: can add new ones, cannot remove or rename existing onesPlan custom attributes carefully; unused attributes persist forever
Username configuration (email, phone, or username)No: set at pool creation, immutableDetermines sign-in identifier; email-as-username is the most common pattern
Case sensitivityNo: set at pool creationCase-insensitive usernames prevent duplicate accounts (e.g., "User@email.com" vs "user@email.com")
MFA configurationPartial: can change between off/optional/required, but cannot disable if set to required via certain pathsStart with "optional" to give yourself flexibility
Password policyYes: can change at any timeOnly applies to new passwords; existing passwords are unaffected
Deletion protectionYes: can enable/disableEnable 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:

ParameterDefaultRangeRecommendation
Minimum length86-9912 characters minimum for production
Require uppercaseYesYes/NoYes
Require lowercaseYesYes/NoYes
Require numbersYesYes/NoYes
Require symbolsYesYes/NoOptional (length matters more than complexity)
Temporary password validity7 days1-365 days7 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.

SettingPurposeRecommendation
Client secretEnables confidential client flow (server-side)Enable for server-side apps; disable for SPAs and mobile apps
Auth flowsWhich authentication APIs the client can callEnable only the flows you use; ALLOW_USER_SRP_AUTH for password-based, ALLOW_REFRESH_TOKEN_AUTH for token refresh
Token expirationID 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 scopesopenid, email, profile, phone, custom scopesopenid email profile covers most use cases
Callback URLsAllowed redirect URIs after authenticationWhitelist 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.

sequenceDiagram
    participant C as Client
    participant CG as Cognito
    participant PV as Pre Sign-Up
Lambda participant CM as Custom Message
Lambda participant PS as Post Confirmation
Lambda C->>CG: SignUp(email, password, attributes) CG->>PV: Pre Sign-Up trigger PV-->>CG: Allow / Deny / Auto-confirm CG-->>C: SignUp response (UserSub) CG->>CM: Custom Message trigger (verification code) CM-->>CG: Customized email template CG-->>C: Verification code sent via email C->>CG: ConfirmSignUp(email, code) CG->>PS: Post Confirmation trigger PS-->>CG: Side effects (create DB record, send welcome email) CG-->>C: Confirmation success
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.

sequenceDiagram
    participant C as Client
    participant CG as Cognito
    participant PL as Pre Authentication
Lambda participant PA as Post Authentication
Lambda C->>CG: InitiateAuth(SRP_AUTH, email) CG->>PL: Pre Authentication trigger PL-->>CG: Allow / Deny CG-->>C: SRP challenge (PASSWORD_VERIFIER) C->>CG: RespondToAuthChallenge(SRP proof) alt MFA Enabled CG-->>C: MFA challenge (SMS_MFA or SOFTWARE_TOKEN_MFA) C->>CG: RespondToAuthChallenge(MFA code) end CG->>PA: Post Authentication trigger PA-->>CG: Side effects (log sign-in, update last login) CG-->>C: Authentication result (ID token, access token, refresh token)
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.

sequenceDiagram
    participant C as Client
    participant CG as Cognito
    participant CM as Custom Message
Lambda C->>CG: ForgotPassword(email) CG->>CM: Custom Message trigger (reset code) CM-->>CG: Customized email template CG-->>C: CodeDeliveryDetails (email destination) Note over C: User receives code via email C->>CG: ConfirmForgotPassword(email, code, new_password) CG-->>C: Password reset success
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.

sequenceDiagram
    participant C as Client
    participant HU as Cognito
Hosted UI participant SP as Social Provider
(Google/Facebook/Apple) participant CG as Cognito participant PV as Pre Sign-Up
Lambda C->>HU: Redirect to /authorize?identity_provider=Google HU->>SP: Redirect to provider's login SP-->>HU: Authorization code HU->>SP: Exchange code for tokens SP-->>HU: Provider tokens (ID token, access token) HU->>CG: Map provider attributes to Cognito attributes alt New User CG->>PV: Pre Sign-Up trigger PV-->>CG: Allow / Auto-confirm / Auto-link end CG-->>HU: Cognito authorization code HU-->>C: Redirect to callback URL with code C->>CG: Exchange code for Cognito tokens CG-->>C: ID token, access token, refresh token
Social federation flow via Hosted UI

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

ProviderDeveloper ConsoleCognito ConfigurationAttribute Mapping Notes
GoogleGoogle Cloud Console: create OAuth 2.0 credentials, configure consent screenIdentity provider type: Google; provide client ID and secret; scopes: openid email profilesub maps to Cognito username; email and name map directly
FacebookMeta for Developers: create app, configure Facebook Login productIdentity provider type: Facebook; provide app ID and secret; scopes: public_profile,emailFacebook uses numeric IDs; email requires explicit permission
AppleApple Developer: register Services ID, configure Sign in with AppleIdentity provider type: Sign in with Apple; provide Services ID, Team ID, Key ID, and private keyApple 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:

AspectSAML 2.0OIDC
Protocol maturityEstablished standard since 2005; ubiquitous in enterpriseNewer, simpler; built on OAuth 2.0
Token formatXML-based SAML assertionsJWT tokens
Setup complexityHigher: metadata XML, assertion consumer service URL, certificate managementLower: issuer URL, client ID, client secret
Attribute mappingSAML attribute statements mapped to Cognito attributesOIDC claims mapped to Cognito attributes
Provider supportUniversal: every enterprise IdP supports SAMLGrowing: most modern IdPs support OIDC
RecommendationUse when the IdP only supports SAML or when the enterprise mandates SAMLPrefer 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.

sequenceDiagram
    participant U as User
    participant App as Application
    participant CG as Cognito
Hosted UI participant IdP as Enterprise IdP
(Okta / Entra ID) U->>App: Click "Sign in with SSO" App->>CG: Redirect to /authorize?identity_provider=OktaSAML CG->>IdP: SAML AuthnRequest IdP-->>U: Corporate login page U->>IdP: Enter corporate credentials + MFA IdP-->>CG: SAML Response (signed assertion) CG->>CG: Validate assertion signature,
map attributes, create/update user CG-->>App: Redirect with authorization code App->>CG: Exchange code for tokens CG-->>App: ID token, access token, refresh token
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

TokenPurposeContainsDefault ExpirationConfigurable Range
ID tokenIdentity assertion: who the user isUser attributes (email, name, custom attributes), Cognito groups, authentication time1 hour5 minutes to 1 day
Access tokenAuthorization: what the user can doScopes, Cognito groups, client ID, username1 hour5 minutes to 1 day
Refresh tokenObtain new ID and access tokens without re-authenticationOpaque token (not a JWT)30 days1 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:

ClaimDescriptionExample
subUser's unique identifier (UUID)a1b2c3d4-5678-90ab-cdef-example11111
issIssuer: the User Pool URLhttps://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123
audAudience: the app client ID1example23456789
token_useToken typeid
auth_timeAuthentication timestamp (epoch)1708700000
expExpiration timestamp (epoch)1708703600
iatIssued at timestamp (epoch)1708700000
emailUser's email addressuser@example.com
email_verifiedWhether the email is verifiedtrue
cognito:groupsGroups the user belongs to["admin", "editors"]
custom:*Custom attributescustom: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.

MethodSecurity LevelUser ExperiencePhishing ResistanceAvailability
SMSModerate: vulnerable to SIM swapping and SS7 interceptionFamiliar; no app requiredLow: codes can be phishedGenerally available; requires SNS configuration
TOTP (authenticator app)High: codes generated locally, not transmittedRequires setup with authenticator app (Google Authenticator, Authy, 1Password)Moderate: codes can be phished but are time-limitedGenerally available
EmailModerate: dependent on email account securityFamiliar; code sent to emailLow: codes can be phishedAvailable (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 LevelTrigger ExamplesConfigurable Actions
LowKnown device, familiar IP, normal timeAllow (no additional challenge)
MediumNew device, unfamiliar IP, unusual timeOptional MFA, notify user
HighImpossible travel, known malicious IP, credential stuffing patternBlock, 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:

ApproachSetup EffortCustomizationBundle Size ImpactBest For
AWS AmplifyLow: npm install aws-amplify; pre-built UI components and hooksModerate: themed components, custom form fields, limited layout controlLarge: ~80-150 KB gzipped (Amplify auth module)Rapid prototyping; teams that want managed UI components
Hosted UI redirectVery low: redirect to Cognito domain, handle callbackLow: CSS customization only, fixed layout and flowMinimal: no client-side auth library requiredEnterprise 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 manuallyFull: complete control over UI, flow, and error handlingSmall: ~15-30 KB gzippedProduction 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 MethodXSS VulnerabilityCSRF VulnerabilityRecommendation
localStorageHigh: any XSS attack can read tokensNone: JavaScript-only accessAvoid for refresh tokens; acceptable for short-lived access tokens if CSP is strict
sessionStorageHigh: same as localStorage but scoped to tabNoneMarginally better than localStorage (cleared on tab close)
HttpOnly cookieNone: JavaScript cannot accessModerate: mitigated with SameSite=StrictBest for refresh tokens; requires a backend endpoint to set the cookie
In-memory (JavaScript variable)Moderate: accessible via XSS but not persistedNoneBest 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.

TriggerWhen It FiresCommon Use CasesCan Block Flow
Pre Sign-UpBefore creating a new userValidate email domain, enforce invitation codes, auto-confirm users, auto-link federated accountsYes: return error to deny sign-up
Pre AuthenticationBefore password verificationCustom rate limiting, IP-based blocking, account lockout logicYes: return error to deny sign-in
Post AuthenticationAfter successful authenticationLog sign-in events, update last login timestamp, trigger analyticsNo
Post ConfirmationAfter user confirms their accountCreate application database record, send welcome email, assign default groupNo
Pre Token GenerationBefore issuing tokensAdd custom claims, modify groups, suppress attributesNo (can modify tokens)
Custom MessageBefore sending email/SMSCustom verification email templates, branded password reset messagesNo (can modify message)
Define Auth ChallengeDuring custom authentication flowImplement CAPTCHA, knowledge-based auth, or custom MFAControls flow
Create Auth ChallengeDuring custom authentication flowGenerate challenge parameters (CAPTCHA image, question)Controls flow
Verify Auth ChallengeDuring custom authentication flowValidate challenge responseControls flow
User MigrationWhen user signs in but does not exist in poolMigrate users from legacy auth system on first sign-inYes: 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:

LayerManaged ByLifetimeStorage
Cognito sessionCognito (refresh token)Configurable (1 hour to 10 years)Cognito service (server-side)
Application sessionYour backendTypically 15-60 minutes (access token expiration)In-memory or session store (Redis, DynamoDB)
Browser sessionCookies or in-memory tokensUntil tab close (sessionStorage) or explicit expiration (cookies)Browser
Hosted UI sessionCognito Hosted UI cookie1 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

ControlImplementationImpact
Enable deletion protectionUser Pool settingsPrevents accidental pool deletion
Configure account recoverySet recovery to verified email only (not phone)Reduces attack surface for account takeover
Enable advanced securityUser Pool advanced security settingsAdds risk-based adaptive auth, compromised credential detection
Block sign-in after failed attemptsAdvanced security automatic risk responsePrevents brute-force attacks
Use case-insensitive usernamesUser Pool creation setting (immutable)Prevents duplicate accounts
Prevent user existence errorsApp client setting: PreventUserExistenceErrors=ENABLEDPrevents account enumeration attacks
Enforce MFAMFA configuration: required or optionalReduces account takeover risk
Restrict callback URLsApp client OAuth configurationPrevents open redirect attacks
Set minimum token expirationApp client token expiration settingsLimits exposure window for stolen tokens
Enable token revocationApp client settingAllows global sign-out to invalidate refresh tokens
Restrict auth flowsApp client: enable only required auth flowsReduces attack surface
Configure WAF on CognitoAttach WAF web ACL to User PoolRate 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.

TierPrice per MAUNotes
First 10,000 MAUFreeFree 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:

FeatureCostNotes
Advanced Security$0.050 per MAURisk-based adaptive auth, compromised credentials detection
SMS MFASNS 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 MAUApplies only to users who authenticate via SAML or OIDC

Cost at Scale

ScaleMAUBase CostWith Advanced SecurityWith SAML (50% of users)Total
Startup5,000Free$250/moN/A$0 - $250/mo
Growth50,000$220/mo$2,500/mo$375/mo$220 - $3,095/mo
Scale-up500,000$2,300/mo$25,000/mo$3,750/mo$2,300 - $31,050/mo
Enterprise2,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

DimensionCognitoAuth0Firebase AuthKeycloak
Hosting modelManaged (AWS)Managed (Okta)Managed (Google)Self-hosted (open source)
Free tier10,000 MAU7,500 MAU50,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/moCustom pricing (typically $5,000-15,000/mo)~$0 (email/password); phone auth adds costInfrastructure cost (~$500-2,000/mo)
Social federationGoogle, Facebook, Apple, Amazon, OIDC30+ social providersGoogle, Facebook, Apple, Twitter, GitHub, and moreAny OIDC/SAML provider
Enterprise SSO (SAML)Yes (+$0.015/MAU)Yes (included in Enterprise plans)No native SAML supportYes (included)
CustomizationLimited: Hosted UI CSS, Lambda triggersExtensive: Actions, Forms, BrandingModerate: UI SDK customizationFull: open source, modify anything
Multi-tenancyManual: separate pools or group-based isolationNative: Organizations featureManual: Firebase projects or custom claimsNative: realms
AWS integrationDeep: IAM, API Gateway, ALB, AppSync, Identity PoolsVia OIDC/SAML standard protocolsGoogle Cloud nativeVia OIDC/SAML standard protocols
Lock-in riskModerate: standard OIDC/JWT but migration requires re-registrationModerate: proprietary extensions but standard protocolsModerate: Firebase-specific SDKsLow: open source, standard protocols
Operational burdenNone (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 ModeCauseMitigation
1Token validation rejects valid tokensJWKS cache is stale after key rotationCache JWKS with TTL of 1 hour maximum; implement fallback that re-fetches JWKS on signature verification failure
2Duplicate user accounts for same personSocial login and email/password create separate accountsImplement account linking in Pre Sign-Up trigger; match on verified email
3Rate limiting on authentication endpointsCognito 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
4Lambda trigger timeout causes auth failureTrigger Lambda exceeds 5-second Cognito timeoutKeep trigger functions lightweight; avoid synchronous external API calls; use async patterns for heavy operations
5Custom attribute cannot be removedCustom attributes are append-only by designPlan schema carefully before launch; store volatile attributes in your application database
6Hosted UI session persists after sign-outApplication clears its own session but does not call Cognito's /logout endpointRedirect to https://<domain>/logout?client_id=<id>&logout_uri=<uri> during sign-out
7Federation attribute mapping mismatchIdP returns attributes with unexpected names or formatsTest attribute mapping with each IdP; log the raw SAML assertion or OIDC claims during integration testing
8Email delivery failuresCognito's default email sender hits the 50/day limitConfigure Amazon SES as the email sender for production workloads
9Refresh token revocation does not invalidate access tokensAccess tokens are stateless JWTs; revocation only affects refresh tokensSet short access token expiration (15-60 minutes); implement a token deny-list for immediate revocation requirements
10User Pool cannot be moved between regionsUser Pools are regional resources with no cross-region replicationChoose 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.
flowchart TD
    A[Authentication
Requirement] --> B{Users are
internal employees?} B -->|Yes| C{Corporate IdP
available?} C -->|Yes| D[Cognito + SAML/OIDC
Federation with Okta or Entra ID] C -->|No| E[Cognito User Pool
with email/password + MFA] B -->|No| F{Need social
login?} F -->|Yes| G{Custom UI
required?} G -->|Yes| H[Cognito User Pool +
Direct SDK + Social Providers] G -->|No| I[Cognito User Pool +
Hosted UI + Social Providers] F -->|No| J{Client needs
direct AWS access?} J -->|Yes| K[Cognito User Pool +
Identity Pool] J -->|No| L[Cognito User Pool
with 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.