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.