AWS Cognito: User Authentication and Authorization (2026)
AWS Cognito is a managed identity service that handles user sign-up, sign-in, and access control for web and mobile apps. It removes the need to build and maintain your own authentication infrastructure — no password hashing, session management, or OAuth server to run. In 2026, Cognito supports social identity providers (Google, Apple, GitHub), enterprise federation via SAML and OIDC, advanced security features like compromised credential detection, and seamless integration with API Gateway, ALB, and AppSync.
User Pools vs Identity Pools
Cognito has two distinct components that serve different purposes. Mixing them up is the most common Cognito confusion.
| Feature | User Pools | Identity Pools (Federated Identities) |
|---|---|---|
| Purpose | Authentication — who is this user? | Authorization — what AWS resources can they access? |
| Output | JWTs (ID token, access token) | Temporary AWS credentials (STS) |
| User directory | Yes — stores usernames, passwords, attributes | No — maps external identities to IAM roles |
| Social login | Yes (hosted UI) | Yes (maps to IAM role) |
| Guest access | No | Yes (unauthenticated role) |
| Use case | App sign-in, token-based API auth | Direct S3/DynamoDB access from mobile client |
| MFA support | Yes | Inherits from identity provider |
| Lambda triggers | Yes (11 trigger types) | No |
Sign-Up and Sign-In Flow
Cognito supports two UI modes: the hosted UI (a fully managed sign-in page at your-domain.auth.region.amazoncognito.com) and custom UI where you call Cognito APIs directly.
# Create a User Pool
aws cognito-idp create-user-pool \
--pool-name "MyAppUsers" \
--policies '{"PasswordPolicy":{"MinimumLength":8,"RequireUppercase":true,"RequireNumbers":true,"RequireSymbols":false}}' \
--auto-verified-attributes email \
--username-attributes email \
--mfa-configuration "OPTIONAL" \
--account-recovery-setting '{"RecoveryMechanisms":[{"Priority":1,"Name":"verified_email"}]}'
# Create an App Client (no secret for SPAs/mobile)
aws cognito-idp create-user-pool-client \
--user-pool-id us-east-1_abc123 \
--client-name "WebApp" \
--no-generate-secret \
--explicit-auth-flows ALLOW_USER_SRP_AUTH ALLOW_REFRESH_TOKEN_AUTH \
--supported-identity-providers COGNITO Google
# Associate a domain for the hosted UI
aws cognito-idp create-user-pool-domain \
--domain "myapp-auth" \
--user-pool-id us-east-1_abc123
For SRP (Secure Remote Password) authentication from a browser, use the AWS Amplify JS library or cognito-identity-js. These handle the SRP challenge/response protocol automatically. Never use the ADMIN_USER_PASSWORD_AUTH flow from client-side code — it requires passing the password to the server and should only be used in server-side admin scripts.
JWT Tokens: ID, Access, and Refresh
On successful authentication, Cognito returns three tokens:
- ID Token: Contains user identity claims (sub, email, phone_number, custom attributes, Cognito groups). Use this to identify the user in your backend.
- Access Token: Contains scopes and group memberships. Use this to authorize calls to your API Gateway or user pool operations. Does not contain user attributes by default.
- Refresh Token: Long-lived (default 30 days). Used to get new ID and Access tokens without re-authentication.
import boto3
import hmac
import hashlib
import base64
def calculate_secret_hash(username: str, client_id: str, client_secret: str) -> str:
"""Required when app client has a secret."""
message = username + client_id
dig = hmac.new(client_secret.encode('utf-8'), msg=message.encode('utf-8'), digestmod=hashlib.sha256).digest()
return base64.b64encode(dig).decode()
cognito = boto3.client('cognito-idp', region_name='us-east-1')
def sign_in(username: str, password: str) -> dict:
response = cognito.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={'USERNAME': username, 'PASSWORD': password},
ClientId='your-app-client-id'
)
auth_result = response['AuthenticationResult']
return {
'id_token': auth_result['IdToken'],
'access_token': auth_result['AccessToken'],
'refresh_token': auth_result['RefreshToken'],
'expires_in': auth_result['ExpiresIn'] # seconds, typically 3600
}
def refresh_tokens(refresh_token: str) -> dict:
response = cognito.initiate_auth(
AuthFlow='REFRESH_TOKEN_AUTH',
AuthParameters={'REFRESH_TOKEN': refresh_token},
ClientId='your-app-client-id'
)
return response['AuthenticationResult']
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json. Never trust token claims without signature verification. Use the python-jose or jwcrypto library for this.MFA Setup: TOTP and SMS
Cognito supports three MFA modes: OFF, OPTIONAL (user chooses), and REQUIRED (enforced for all users). TOTP (Google Authenticator, Authy) is preferred over SMS because it has no per-message cost and works offline.
def setup_totp_mfa(access_token: str) -> str:
"""Step 1: Associate a software token (returns secret code for QR)."""
response = cognito.associate_software_token(AccessToken=access_token)
secret_code = response['SecretCode']
# Show this as a QR code: otpauth://totp/MyApp:user@example.com?secret=SECRET&issuer=MyApp
return secret_code
def verify_totp_setup(access_token: str, totp_code: str, device_name: str) -> bool:
"""Step 2: Verify the TOTP code to activate MFA."""
try:
cognito.verify_software_token(
AccessToken=access_token,
UserCode=totp_code,
FriendlyDeviceName=device_name
)
# Step 3: Set TOTP as preferred MFA method
cognito.set_user_mfa_preference(
AccessToken=access_token,
SoftwareTokenMfaSettings={'Enabled': True, 'PreferredMfa': True}
)
return True
except cognito.exceptions.EnableSoftwareTokenMFAException:
return False
Lambda Triggers
Cognito has 11 Lambda trigger points that let you customize every stage of the auth flow without replacing Cognito itself.
| Trigger | When It Fires | Common Use |
|---|---|---|
| Pre Sign-Up | Before user is created | Block signups from certain domains |
| Post Confirmation | After email/phone verified | Create user record in DynamoDB |
| Pre Authentication | Before password check | Block banned users |
| Post Authentication | After successful sign-in | Log audit events |
| Pre Token Generation | Before JWT is issued | Add custom claims to tokens |
| Custom Message | Before email/SMS is sent | Brand your confirmation emails |
| User Migration | When unknown user tries to sign in | Migrate users from legacy system |
| Define/Create/Verify Auth Challenge | Custom auth flow | Passwordless OTP auth |
def pre_token_generation_handler(event, context):
"""Add custom claims to the ID token and suppress default claims."""
user_id = event['userName']
# Fetch user permissions from DynamoDB
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('UserPermissions')
result = table.get_item(Key={'userId': user_id})
permissions = result.get('Item', {}).get('permissions', [])
event['response']['claimsOverrideDetails'] = {
'claimsToAddOrOverride': {
'permissions': ','.join(permissions),
'plan': result.get('Item', {}).get('plan', 'free')
},
'claimsToSuppress': ['phone_number_verified']
}
return event
Social Identity Providers
Adding Google sign-in to a Cognito User Pool takes about 10 minutes. The flow: browser → Cognito hosted UI → Google OAuth → Cognito creates/updates user → returns JWT.
# Register Google as an identity provider
aws cognito-idp create-identity-provider \
--user-pool-id us-east-1_abc123 \
--provider-name Google \
--provider-type Google \
--provider-details '{
"client_id": "your-google-oauth-client-id.apps.googleusercontent.com",
"client_secret": "your-google-oauth-client-secret",
"authorize_scopes": "profile email openid"
}' \
--attribute-mapping '{
"email": "email",
"name": "name",
"picture": "picture"
}'
# Update app client to support Google
aws cognito-idp update-user-pool-client \
--user-pool-id us-east-1_abc123 \
--client-id your-app-client-id \
--supported-identity-providers COGNITO Google \
--callback-urls '["https://myapp.com/callback"]' \
--logout-urls '["https://myapp.com/logout"]' \
--allowed-o-auth-flows code \
--allowed-o-auth-scopes openid email profile
Cognito + API Gateway Authorizer
A Cognito User Pool Authorizer validates the JWT access token on every API Gateway request without writing any Lambda code.
# Create a Cognito authorizer on your REST API
aws apigateway create-authorizer \
--rest-api-id abc123def \
--name "CognitoAuthorizer" \
--type COGNITO_USER_POOLS \
--provider-arns "arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_abc123" \
--identity-source "method.request.header.Authorization"
# Attach authorizer to a resource method
aws apigateway update-method \
--rest-api-id abc123def \
--resource-id resourceId123 \
--http-method GET \
--patch-operations '[
{"op":"replace","path":"/authorizationType","value":"COGNITO_USER_POOLS"},
{"op":"replace","path":"/authorizerId","value":"authorizer123"}
]'
For HTTP APIs (not REST APIs), use a JWT authorizer instead — it's simpler to configure and adds ~1ms less latency than the REST API Cognito authorizer.
Cognito + ALB Authentication
ALB natively integrates with Cognito to protect any backend service (ECS, EC2, on-prem) without changing application code. The ALB handles the OAuth dance and injects user identity headers.
# Add Cognito auth action to an ALB listener rule
aws elbv2 create-rule \
--listener-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:listener/app/my-alb/xxx/yyy \
--conditions '[{"Field":"path-pattern","Values":["/api/*"]}]' \
--priority 10 \
--actions '[
{
"Type": "authenticate-cognito",
"Order": 1,
"AuthenticateCognitoConfig": {
"UserPoolArn": "arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_abc123",
"UserPoolClientId": "your-client-id",
"UserPoolDomain": "myapp-auth",
"OnUnauthenticatedRequest": "authenticate"
}
},
{
"Type": "forward",
"Order": 2,
"TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/my-targets/abc"
}
]'
Admin Operations with Python boto3
import boto3
cognito = boto3.client('cognito-idp', region_name='us-east-1')
USER_POOL_ID = 'us-east-1_abc123'
def admin_create_user(email: str, temp_password: str, given_name: str) -> str:
response = cognito.admin_create_user(
UserPoolId=USER_POOL_ID,
Username=email,
TemporaryPassword=temp_password,
UserAttributes=[
{'Name': 'email', 'Value': email},
{'Name': 'email_verified', 'Value': 'true'},
{'Name': 'given_name', 'Value': given_name}
],
MessageAction='SUPPRESS' # Don't send the welcome email
)
return response['User']['Username']
def admin_add_to_group(username: str, group_name: str):
cognito.admin_add_user_to_group(
UserPoolId=USER_POOL_ID, Username=username, GroupName=group_name)
def admin_disable_user(username: str):
cognito.admin_disable_user(UserPoolId=USER_POOL_ID, Username=username)
def list_users_in_group(group_name: str) -> list:
paginator = cognito.get_paginator('list_users_in_group')
users = []
for page in paginator.paginate(UserPoolId=USER_POOL_ID, GroupName=group_name):
users.extend(page['Users'])
return users
def admin_reset_password(username: str):
"""Force user to set a new password on next sign-in."""
cognito.admin_reset_user_password(UserPoolId=USER_POOL_ID, Username=username)
FAQ
Q: What's the free tier for Cognito?
The first 50,000 Monthly Active Users (MAUs) for User Pools are free. After that, pricing starts at $0.0055 per MAU for the first 100K. SAML/OIDC federated users cost $0.015 per MAU (different tier). There's no charge for Identity Pools themselves — you only pay for STS temporary credentials, which are free.
Q: How do I handle token expiration in a SPA?
Store the refresh token in an HTTP-only cookie (not localStorage) to prevent XSS theft. When the access token expires (default 1 hour), call REFRESH_TOKEN_AUTH to get a new access token silently. The Amplify JS library handles this automatically with its Auth.currentSession() method. Set short access token lifetimes (15–60 minutes) and longer refresh token lifetimes (7–30 days).
Q: Can I migrate users from an existing auth system without forcing password resets?
Yes, using the User Migration Lambda trigger. When a user tries to sign in and isn't found in Cognito, the trigger fires with the username and password. Your Lambda validates them against your old system and returns the user attributes to Cognito, which creates the user transparently. The user never knows the migration happened.
Q: Does Cognito support fine-grained authorization (RBAC)?
Cognito handles coarse-grained authorization via groups (add a user to "admin" group → get "cognito:groups" claim in JWT). For fine-grained RBAC (resource-level permissions), add custom claims via the Pre Token Generation Lambda trigger. Cognito doesn't natively do attribute-based access control — combine it with Cedar policy engine or implement ABAC in your backend.
Q: What's the difference between Cognito hosted UI and a custom sign-in page?
The hosted UI is a fully managed OAuth 2.0 sign-in page that supports social login and works out of the box — you just redirect to it. A custom UI calls Cognito APIs directly, giving you full design control but requiring you to implement the SRP authentication protocol using the Amplify JS library or cognito-identity-js. Use the hosted UI when getting to production fast matters; use custom UI when branding and UX control are priorities.