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.
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 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) 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 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 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 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 | |
| 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:
- Create a SAML 2.0 application in the Okta admin console.
- Set the Single Sign-On URL to
https://<cognito-domain>.auth.<region>.amazoncognito.com/saml2/idpresponse. - Set the Audience URI (SP Entity ID) to
urn:amazon:cognito:sp:<user-pool-id>. - Configure attribute statements to map Okta user attributes (email, firstName, lastName) to SAML assertion attributes.
- Download the Okta metadata XML.
- In Cognito, create a SAML identity provider and upload the metadata XML.
- Map SAML attributes to Cognito user attributes.
- Add Okta as a supported identity provider on the app client.
For OIDC integration with Okta:
- Create an OIDC application in the Okta admin console.
- Set the sign-in redirect URI to
https://<cognito-domain>.auth.<region>.amazoncognito.com/oauth2/idpresponse. - Note the client ID and client secret.
- In Cognito, create an OIDC identity provider with the Okta issuer URL (
https://<okta-domain>.okta.com). - Provide the client ID, client secret, and requested scopes (
openid email profile). - 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:
- Register an application in the Entra ID portal.
- Configure the redirect URI:
https://<cognito-domain>.auth.<region>.amazoncognito.com/oauth2/idpresponse. - Create a client secret in Certificates & secrets.
- Note the Application (client) ID and Directory (tenant) ID.
- In Cognito, create an OIDC identity provider with the issuer URL:
https://login.microsoftonline.com/<tenant-id>/v2.0. - Provide the client ID, client secret, and scopes (
openid email profile). - 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 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
AdminUserGlobalSignOutto 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:
- Decode the JWT header to get the key ID (
kid). - 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. - Find the matching public key by
kidin the JWKS. - Verify the signature using the RSA public key.
- Check the expiration (
expclaim). Reject expired tokens. - Verify the issuer (
issclaim). It must match your User Pool URL. - Verify the audience (
audclaim for ID tokens) or client ID (client_idclaim for access tokens). - Verify the token use (
token_useclaim). Useidfor ID tokens andaccessfor 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
RevokeTokenwhen the user signs out, orAdminUserGlobalSignOutto 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:groupsclaim 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 |
| 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:
- User signs in and calls
AssociateSoftwareTokenwith their access token. - Cognito returns a secret key (Base32-encoded).
- The application displays the secret as a QR code (using the
otpauth://URI format). - The user scans the QR code with their authenticator app.
- The user enters the current TOTP code displayed in the app.
- The application calls
VerifySoftwareTokenwith the code. - Cognito verifies the code and enables TOTP MFA for the user.
- The application calls
SetUserMFAPreferenceto 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:
- Store the refresh token in an HttpOnly, Secure, SameSite=Strict cookie set by your backend.
- Store the access token and ID token in memory (JavaScript variable).
- 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.
- 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:
- Keep Cognito lean. Store only authentication-essential attributes in the User Pool. Use
subas the foreign key to your application database for everything else. Cognito is an identity provider, not a user profile database. - 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.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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.
- 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_usernameoremaildepending on configuration. Test each IdP integration with real accounts and verify the mapped attributes in Cognito. - 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] 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.

