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.

FeatureUser PoolsIdentity Pools (Federated Identities)
PurposeAuthentication — who is this user?Authorization — what AWS resources can they access?
OutputJWTs (ID token, access token)Temporary AWS credentials (STS)
User directoryYes — stores usernames, passwords, attributesNo — maps external identities to IAM roles
Social loginYes (hosted UI)Yes (maps to IAM role)
Guest accessNoYes (unauthenticated role)
Use caseApp sign-in, token-based API authDirect S3/DynamoDB access from mobile client
MFA supportYesInherits from identity provider
Lambda triggersYes (11 trigger types)No
Pro Tip: For most web and mobile apps, you only need a User Pool. Use an Identity Pool only when your mobile client needs direct AWS SDK calls (e.g., uploading directly to S3 or reading from DynamoDB without going through your API).

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']
Note: Validate JWTs on your backend by verifying the signature against Cognito's JWKS endpoint: 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.

TriggerWhen It FiresCommon Use
Pre Sign-UpBefore user is createdBlock signups from certain domains
Post ConfirmationAfter email/phone verifiedCreate user record in DynamoDB
Pre AuthenticationBefore password checkBlock banned users
Post AuthenticationAfter successful sign-inLog audit events
Pre Token GenerationBefore JWT is issuedAdd custom claims to tokens
Custom MessageBefore email/SMS is sentBrand your confirmation emails
User MigrationWhen unknown user tries to sign inMigrate users from legacy system
Define/Create/Verify Auth ChallengeCustom auth flowPasswordless 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.