AWS Cognito Testing Guide: User Pools, Identity Pools, JWT Validation, and Amplify

AWS Cognito Testing Guide: User Pools, Identity Pools, JWT Validation, and Amplify

AWS Cognito provides user authentication, authorization, and identity management for AWS-hosted applications. Testing Cognito integrations is critical — a misconfigured user pool can lock users out, a broken Lambda trigger can block all signups, and a wrong JWT audience claim can silently reject valid users.

This guide covers testing Cognito user pools, identity pools, Lambda triggers, JWT validation, and Amplify-based auth flows.

Testing Environments

Cognito doesn't have a built-in sandbox. Your options:

Option 1: Dedicated test AWS account — create separate Cognito user pools in a test account. Safe isolation, real Cognito behavior. Recommended.

Option 2: Separate user pool in the same account — create a myapp-test user pool alongside myapp-prod. Cheaper, slightly riskier (shares account quotas).

Option 3: Moto (Python mocking library) — mocks AWS services locally. Good for unit tests. Doesn't catch Cognito-specific edge cases.

Option 4: LocalStack Pro — fully local Cognito emulation. Good for CI, needs a Pro subscription for Cognito.

# Environment variables for test configuration
<span class="hljs-built_in">export AWS_REGION=us-east-1
<span class="hljs-built_in">export COGNITO_USER_POOL_ID=us-east-1_TestPoolId
<span class="hljs-built_in">export COGNITO_CLIENT_ID=test-client-id
<span class="hljs-built_in">export COGNITO_IDENTITY_POOL_ID=us-east-1:test-identity-pool-id
<span class="hljs-comment"># Test users pre-created in the test user pool
<span class="hljs-built_in">export TEST_USER_EMAIL=testuser@example.com
<span class="hljs-built_in">export TEST_USER_PASSWORD=TestPass123!

Unit Testing with Moto

Moto mocks AWS services in memory for unit tests.

# tests/unit/test_cognito_auth.py
import pytest
import boto3
from moto import mock_aws
from unittest.mock import patch
import os

os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
os.environ["AWS_ACCESS_KEY_ID"] = "testing"
os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"

@pytest.fixture
def cognito_setup():
    with mock_aws():
        client = boto3.client("cognito-idp", region_name="us-east-1")
        
        # Create a test user pool
        pool = client.create_user_pool(
            PoolName="TestPool",
            Policies={
                "PasswordPolicy": {
                    "MinimumLength": 8,
                    "RequireUppercase": True,
                    "RequireLowercase": True,
                    "RequireNumbers": True,
                    "RequireSymbols": False
                }
            },
            Schema=[
                {
                    "Name": "email",
                    "AttributeDataType": "String",
                    "Required": True,
                    "Mutable": True
                }
            ],
            AutoVerifiedAttributes=["email"]
        )
        pool_id = pool["UserPool"]["Id"]
        
        # Create a user pool client
        app_client = client.create_user_pool_client(
            UserPoolId=pool_id,
            ClientName="TestClient",
            ExplicitAuthFlows=[
                "ALLOW_USER_PASSWORD_AUTH",
                "ALLOW_REFRESH_TOKEN_AUTH"
            ]
        )
        client_id = app_client["UserPoolClient"]["ClientId"]
        
        # Create a test user
        client.admin_create_user(
            UserPoolId=pool_id,
            Username="testuser@example.com",
            TemporaryPassword="Temp123!",
            UserAttributes=[
                {"Name": "email", "Value": "testuser@example.com"},
                {"Name": "email_verified", "Value": "true"}
            ]
        )
        
        # Set permanent password (skip NEW_PASSWORD_REQUIRED challenge)
        client.admin_set_user_password(
            UserPoolId=pool_id,
            Username="testuser@example.com",
            Password="TestPass123!",
            Permanent=True
        )
        
        yield {
            "client": client,
            "pool_id": pool_id,
            "client_id": client_id
        }

def test_successful_authentication(cognito_setup):
    client = cognito_setup["client"]
    
    response = client.initiate_auth(
        AuthFlow="USER_PASSWORD_AUTH",
        AuthParameters={
            "USERNAME": "testuser@example.com",
            "PASSWORD": "TestPass123!"
        },
        ClientId=cognito_setup["client_id"]
    )
    
    assert "AuthenticationResult" in response
    assert "AccessToken" in response["AuthenticationResult"]
    assert "IdToken" in response["AuthenticationResult"]
    assert "RefreshToken" in response["AuthenticationResult"]

def test_wrong_password_raises_not_authorized(cognito_setup):
    client = cognito_setup["client"]
    
    with pytest.raises(client.exceptions.NotAuthorizedException):
        client.initiate_auth(
            AuthFlow="USER_PASSWORD_AUTH",
            AuthParameters={
                "USERNAME": "testuser@example.com",
                "PASSWORD": "WrongPassword!"
            },
            ClientId=cognito_setup["client_id"]
        )

def test_nonexistent_user_raises_not_authorized(cognito_setup):
    """Cognito returns NotAuthorizedException for nonexistent users to prevent enumeration."""
    client = cognito_setup["client"]
    
    with pytest.raises(client.exceptions.NotAuthorizedException):
        client.initiate_auth(
            AuthFlow="USER_PASSWORD_AUTH",
            AuthParameters={
                "USERNAME": "nobody@example.com",
                "PASSWORD": "SomePass123!"
            },
            ClientId=cognito_setup["client_id"]
        )

def test_token_refresh(cognito_setup):
    client = cognito_setup["client"]
    
    # First, get tokens
    auth = client.initiate_auth(
        AuthFlow="USER_PASSWORD_AUTH",
        AuthParameters={
            "USERNAME": "testuser@example.com",
            "PASSWORD": "TestPass123!"
        },
        ClientId=cognito_setup["client_id"]
    )
    refresh_token = auth["AuthenticationResult"]["RefreshToken"]
    
    # Use refresh token
    refresh = client.initiate_auth(
        AuthFlow="REFRESH_TOKEN_AUTH",
        AuthParameters={"REFRESH_TOKEN": refresh_token},
        ClientId=cognito_setup["client_id"]
    )
    
    assert "AccessToken" in refresh["AuthenticationResult"]
    assert "IdToken" in refresh["AuthenticationResult"]

JWT Validation Tests

Cognito issues JWTs that your application must validate. Test the validation logic thoroughly.

# tests/unit/test_jwt_validation.py
import pytest
import jwt
import json
import time
import requests
from unittest.mock import patch, MagicMock
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend

# Your application's JWT validation function
from app.auth import validate_cognito_token, TokenValidationError

def generate_test_rsa_key_pair():
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )
    return private_key, private_key.public_key()

@pytest.fixture
def rsa_keys():
    return generate_test_rsa_key_pair()

def make_cognito_token(private_key, payload_overrides=None, headers=None):
    payload = {
        "sub": "user-uuid-123",
        "email": "user@example.com",
        "cognito:username": "testuser",
        "cognito:groups": ["users"],
        "iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_TestPoolId",
        "aud": "test-client-id",
        "token_use": "id",
        "iat": int(time.time()),
        "exp": int(time.time()) + 3600,
        "event_id": "test-event-id"
    }
    if payload_overrides:
        payload.update(payload_overrides)
    
    token_headers = {"kid": "test-key-id", "alg": "RS256"}
    if headers:
        token_headers.update(headers)
    
    return jwt.encode(payload, private_key, algorithm="RS256", headers=token_headers)

def test_valid_id_token_passes(rsa_keys, mock_jwks):
    private_key, public_key = rsa_keys
    token = make_cognito_token(private_key)
    
    result = validate_cognito_token(token, expected_audience="test-client-id")
    
    assert result["sub"] == "user-uuid-123"
    assert result["email"] == "user@example.com"

def test_expired_token_rejected(rsa_keys, mock_jwks):
    private_key, _ = rsa_keys
    token = make_cognito_token(private_key, payload_overrides={
        "exp": int(time.time()) - 3600  # Expired 1 hour ago
    })
    
    with pytest.raises(TokenValidationError, match="expired"):
        validate_cognito_token(token, expected_audience="test-client-id")

def test_wrong_issuer_rejected(rsa_keys, mock_jwks):
    private_key, _ = rsa_keys
    token = make_cognito_token(private_key, payload_overrides={
        "iss": "https://evil.example.com/fake-pool"
    })
    
    with pytest.raises(TokenValidationError, match="issuer"):
        validate_cognito_token(token, expected_audience="test-client-id")

def test_wrong_audience_rejected(rsa_keys, mock_jwks):
    private_key, _ = rsa_keys
    token = make_cognito_token(private_key, payload_overrides={
        "aud": "wrong-client-id"
    })
    
    with pytest.raises(TokenValidationError, match="audience"):
        validate_cognito_token(token, expected_audience="test-client-id")

def test_wrong_token_use_rejected(rsa_keys, mock_jwks):
    """Access tokens should not be accepted where ID tokens are expected."""
    private_key, _ = rsa_keys
    # Access token has token_use=access, not id
    token = make_cognito_token(private_key, payload_overrides={
        "token_use": "access"
    })
    
    with pytest.raises(TokenValidationError, match="token_use"):
        validate_cognito_token(token, expected_audience="test-client-id", token_use="id")

def test_tampered_signature_rejected(rsa_keys, mock_jwks):
    private_key, _ = rsa_keys
    token = make_cognito_token(private_key)
    
    # Tamper with the signature
    parts = token.split(".")
    tampered = parts[0] + "." + parts[1] + ".invalidsignature"
    
    with pytest.raises(TokenValidationError):
        validate_cognito_token(tampered, expected_audience="test-client-id")

def test_groups_extracted_from_claims(rsa_keys, mock_jwks):
    private_key, _ = rsa_keys
    token = make_cognito_token(private_key, payload_overrides={
        "cognito:groups": ["admins", "users", "developers"]
    })
    
    result = validate_cognito_token(token, expected_audience="test-client-id")
    assert set(result["groups"]) == {"admins", "users", "developers"}

Lambda Trigger Testing

Cognito Lambda triggers customize the auth flow. Test them in isolation.

# tests/unit/test_lambda_triggers.py
import pytest
import json
from lambda_triggers.pre_signup import handler as pre_signup_handler
from lambda_triggers.post_confirmation import handler as post_confirmation_handler
from lambda_triggers.pre_token_generation import handler as pre_token_handler

def make_cognito_trigger_event(trigger_type, user_attributes=None, **kwargs):
    """Create a Cognito trigger event in the format AWS sends."""
    event = {
        "version": "1",
        "triggerSource": trigger_type,
        "region": "us-east-1",
        "userPoolId": "us-east-1_TestPool",
        "userName": "testuser",
        "callerContext": {
            "awsSdkVersion": "aws-sdk-python-3.x",
            "clientId": "test-client-id"
        },
        "request": {
            "userAttributes": user_attributes or {
                "email": "test@example.com",
                "email_verified": "false"
            }
        },
        "response": {}
    }
    event.update(kwargs)
    return event

class TestPreSignupTrigger:
    """Test the pre-signup Lambda trigger."""
    
    def test_allowed_email_domain_passes(self):
        event = make_cognito_trigger_event(
            "PreSignUp_SignUp",
            user_attributes={"email": "user@allowed-domain.com"}
        )
        
        result = pre_signup_handler(event, {})
        
        # Should return event unchanged (no error means allowed)
        assert result["response"] is not None
    
    def test_blocked_email_domain_raises(self):
        event = make_cognito_trigger_event(
            "PreSignUp_SignUp",
            user_attributes={"email": "user@blocked-domain.com"}
        )
        
        with pytest.raises(Exception) as exc_info:
            pre_signup_handler(event, {})
        
        assert "not allowed" in str(exc_info.value).lower() or \
               "blocked" in str(exc_info.value).lower()
    
    def test_auto_confirms_verified_email(self):
        """Pre-signup trigger should auto-confirm users with verified emails."""
        event = make_cognito_trigger_event(
            "PreSignUp_AdminCreateUser",
            user_attributes={
                "email": "admin-created@example.com",
                "email_verified": "true"
            }
        )
        
        result = pre_signup_handler(event, {})
        
        assert result["response"]["autoConfirmUser"] is True

class TestPostConfirmationTrigger:
    """Test the post-confirmation Lambda trigger."""
    
    def test_welcome_email_sent_on_confirmation(self, mock_ses):
        event = make_cognito_trigger_event(
            "PostConfirmation_ConfirmSignUp",
            user_attributes={
                "email": "newuser@example.com",
                "name": "New User"
            }
        )
        
        post_confirmation_handler(event, {})
        
        # Verify SES was called with welcome email
        mock_ses.send_email.assert_called_once()
        call_args = mock_ses.send_email.call_args[1]
        assert "newuser@example.com" in str(call_args["Destination"])
        assert "Welcome" in call_args["Message"]["Subject"]["Data"]
    
    def test_user_added_to_default_group_on_confirmation(self, mock_cognito_idp):
        event = make_cognito_trigger_event(
            "PostConfirmation_ConfirmSignUp",
            user_attributes={"email": "newuser@example.com"}
        )
        
        post_confirmation_handler(event, {})
        
        # Verify user was added to "Users" group
        mock_cognito_idp.admin_add_user_to_group.assert_called_with(
            UserPoolId="us-east-1_TestPool",
            Username="testuser",
            GroupName="Users"
        )

class TestPreTokenGenerationTrigger:
    """Test the pre-token-generation Lambda trigger."""
    
    def test_custom_claims_added_to_token(self):
        event = make_cognito_trigger_event(
            "TokenGeneration_Authentication",
            user_attributes={
                "email": "user@example.com",
                "custom:subscription_tier": "pro"
            }
        )
        
        result = pre_token_handler(event, {})
        
        # Trigger should add custom claims to the token
        claims_to_add = result["response"].get("claimsOverrideDetails", {}).get("claimsToAddOrOverride", {})
        assert "subscription_tier" in claims_to_add
        assert claims_to_add["subscription_tier"] == "pro"
    
    def test_groups_included_in_token_claims(self):
        event = make_cognito_trigger_event(
            "TokenGeneration_Authentication",
            groupConfiguration={
                "groupsToOverride": ["admins", "users"],
                "iamRolesToOverride": [],
                "preferredRole": None
            }
        )
        
        result = pre_token_handler(event, {})
        
        # Groups should not be suppressed
        suppress = result["response"].get("claimsOverrideDetails", {}).get("groupOverrideDetails", {})
        assert suppress.get("groupsToOverride") != []

Integration Testing Against Real Cognito

# tests/integration/test_cognito_integration.py
import boto3
import pytest
import os

USER_POOL_ID = os.environ["COGNITO_USER_POOL_ID"]
CLIENT_ID = os.environ["COGNITO_CLIENT_ID"]
TEST_EMAIL = os.environ["TEST_USER_EMAIL"]
TEST_PASSWORD = os.environ["TEST_USER_PASSWORD"]

@pytest.fixture(scope="session")
def cognito():
    return boto3.client("cognito-idp", region_name="us-east-1")

def test_authenticate_and_get_tokens(cognito):
    response = cognito.initiate_auth(
        AuthFlow="USER_PASSWORD_AUTH",
        AuthParameters={
            "USERNAME": TEST_EMAIL,
            "PASSWORD": TEST_PASSWORD
        },
        ClientId=CLIENT_ID
    )
    
    result = response["AuthenticationResult"]
    assert "AccessToken" in result
    assert "IdToken" in result
    assert "RefreshToken" in result
    assert result["TokenType"] == "Bearer"
    assert result["ExpiresIn"] > 0

def test_tokens_are_valid_jwts(cognito):
    import jwt
    
    response = cognito.initiate_auth(
        AuthFlow="USER_PASSWORD_AUTH",
        AuthParameters={"USERNAME": TEST_EMAIL, "PASSWORD": TEST_PASSWORD},
        ClientId=CLIENT_ID
    )
    
    id_token = response["AuthenticationResult"]["IdToken"]
    
    # Decode without verification to inspect claims
    payload = jwt.decode(id_token, options={"verify_signature": False})
    
    assert payload["email"] == TEST_EMAIL
    assert payload["token_use"] == "id"
    assert payload["aud"] == CLIENT_ID
    assert "us-east-1" in payload["iss"]

def test_get_user_from_access_token(cognito):
    response = cognito.initiate_auth(
        AuthFlow="USER_PASSWORD_AUTH",
        AuthParameters={"USERNAME": TEST_EMAIL, "PASSWORD": TEST_PASSWORD},
        ClientId=CLIENT_ID
    )
    
    access_token = response["AuthenticationResult"]["AccessToken"]
    
    user = cognito.get_user(AccessToken=access_token)
    
    assert user["Username"] is not None
    attrs = {a["Name"]: a["Value"] for a in user["UserAttributes"]}
    assert attrs["email"] == TEST_EMAIL

def test_global_sign_out_invalidates_tokens(cognito):
    # Get tokens
    response = cognito.initiate_auth(
        AuthFlow="USER_PASSWORD_AUTH",
        AuthParameters={"USERNAME": TEST_EMAIL, "PASSWORD": TEST_PASSWORD},
        ClientId=CLIENT_ID
    )
    access_token = response["AuthenticationResult"]["AccessToken"]
    
    # Sign out
    cognito.global_sign_out(AccessToken=access_token)
    
    # Token should now be invalid
    with pytest.raises(cognito.exceptions.NotAuthorizedException):
        cognito.get_user(AccessToken=access_token)

Testing Amplify Auth

If your frontend uses AWS Amplify, test the Amplify auth configuration.

// tests/amplify/auth.test.js
import { Amplify } from 'aws-amplify';
import { signIn, signOut, getCurrentUser, fetchAuthSession } from 'aws-amplify/auth';
import amplifyConfig from '../../src/aws-exports.js';

beforeAll(() => {
  Amplify.configure(amplifyConfig);
});

describe('Amplify Auth', () => {
  afterEach(async () => {
    try {
      await signOut();
    } catch {
      // Ignore if not signed in
    }
  });

  it('signs in with valid credentials', async () => {
    const result = await signIn({
      username: process.env.TEST_USER_EMAIL,
      password: process.env.TEST_USER_PASSWORD
    });
    
    expect(result.isSignedIn).toBe(true);
    expect(result.nextStep.signInStep).toBe('DONE');
  });

  it('rejects invalid credentials', async () => {
    await expect(signIn({
      username: process.env.TEST_USER_EMAIL,
      password: 'wrong-password'
    })).rejects.toThrow('Incorrect username or password');
  });

  it('returns authenticated session after sign in', async () => {
    await signIn({
      username: process.env.TEST_USER_EMAIL,
      password: process.env.TEST_USER_PASSWORD
    });
    
    const session = await fetchAuthSession();
    
    expect(session.tokens?.accessToken).toBeTruthy();
    expect(session.tokens?.idToken).toBeTruthy();
    
    const payload = session.tokens.idToken.payload;
    expect(payload.email).toBe(process.env.TEST_USER_EMAIL);
  });

  it('returns current user after sign in', async () => {
    await signIn({
      username: process.env.TEST_USER_EMAIL,
      password: process.env.TEST_USER_PASSWORD
    });
    
    const user = await getCurrentUser();
    expect(user.username).toBeTruthy();
    expect(user.userId).toBeTruthy();
  });

  it('clears session on sign out', async () => {
    await signIn({
      username: process.env.TEST_USER_EMAIL,
      password: process.env.TEST_USER_PASSWORD
    });
    
    await signOut();
    
    await expect(getCurrentUser()).rejects.toThrow();
  });
});

Continuous Monitoring

Cognito configurations can break without code changes — a user pool policy change, a Lambda trigger deployment failure, or an OIDC config update can break auth silently.

Use HelpMeTest to monitor your Cognito-backed auth continuously:

# Test that OIDC discovery endpoint is accessible
GET https://cognito-idp.us-east-1.amazonaws.com/us-east-1_YourPoolId/.well-known/openid-configuration
Status Should Be 200
Page Should Contain "jwks_uri"
Page Should Contain "authorization_endpoint"

# Test that login page loads
Go To https://your-domain.auth.us-east-1.amazoncognito.com/login
Page Should Contain "Sign In"

Run these every 5 minutes so you know within minutes when authentication breaks — not when users start complaining.

Common Cognito Testing Mistakes

Reusing test users across tests — Cognito tracks failed auth attempts and can lock accounts. Create fresh test users per test suite or use admin APIs to reset user state.

Ignoring the NEW_PASSWORD_REQUIRED challenge — Cognito forces a password change on first login for admin-created users. Your test must handle this challenge, or pre-set permanent passwords with admin_set_user_password.

Testing with implicit grant — the implicit flow (access tokens in URL fragments) is deprecated and disabled by default. Test with authorization code flow + PKCE, even in tests.

Not testing Lambda trigger failures — a Lambda trigger that throws an unhandled exception blocks the entire auth flow. Test your error handling paths: what happens when your database is down during post-confirmation?

Missing token_use validation — access tokens and ID tokens have different claims and purposes. Code that accepts access tokens where ID tokens are expected (or vice versa) creates security vulnerabilities. Always validate token_use.

Read more