Valtik Studios
Back to blog
AWShigh2026-02-1412 min

AWS Cognito: Identity Pool Misconfiguration and the IAM Role Confusion Attack

AWS Cognito has two parts: User Pools (authentication) and Identity Pools (authorization for AWS services). Most Cognito security thinking focuses on User Pools. Password policies, MFA, account security. The much more dangerous failure mode is in Identity Pools, where misconfigurations let unauthenticated users assume IAM roles with excessive privilege. A deep dive into the role confusion attacks we find on Cognito deployments.

TT
Tre Trebucchi·Founder, Valtik Studios. Penetration Tester

Founder of Valtik Studios. Pentester. Based in Connecticut, serving US mid-market.

The two Cognitos

We see this pattern show up on almost every engagement.

AWS Cognito is two services:

Cognito User Pools: user authentication. Username/password, MFA, social login. Produces ID tokens and access tokens. Like Auth0 or Okta but AWS-native.

Cognito Identity Pools: federated AWS authorization. Takes identity tokens (from Cognito User Pools, third-party OIDC providers like Google/Facebook, or "unauthenticated" guest identities) and exchanges them for AWS IAM credentials. Used to let mobile apps directly call AWS services (S3, DynamoDB) without going through a backend.

Most Cognito guidance and most developers focus on User Pools. The security model there's well-understood: strong passwords, MFA, proper password reset flows.

Identity Pools are where the real damage potential lives. They give your authentication result direct access to AWS services. A misconfigured Identity Pool is a direct path from "web visitor" to "AWS resource access". Potentially with admin-level IAM privileges.

This post walks through the specific Cognito attack patterns we find, particularly focused on Identity Pools. And the hardening that prevents role confusion and privilege escalation.

How Identity Pools work

Identity Pool architecture:

  1. User authenticates. Via Cognito User Pool, Google, Facebook, OIDC provider, or unauthenticated
  2. User receives identity token (JWT from the identity provider)
  3. User calls Cognito Identity Pool. Provides the token (or claims unauthenticated)
  4. Identity Pool validates the token. Confirms it's from a trusted provider
  5. Identity Pool matches the identity to an IAM role. Based on configuration
  6. Identity Pool returns AWS credentials. Temporary session credentials via STS
  7. User makes AWS API calls. Using the returned credentials

The IAM role returned determines what AWS resources the authenticated user can access. Two role slots per Identity Pool:

  • Authenticated role. For users who have authenticated via a linked identity provider
  • Unauthenticated role. For guest users (if unauthenticated access is enabled)

The critical security question: what permissions do these IAM roles have? This is where role confusion starts.

Attack pattern 1: Unauthenticated role with excessive permissions

The Identity Pool's unauthenticated role is assumable by anyone on the internet who knows the Identity Pool ID (which is typically embedded in frontend code and trivially enumerable).

The attack:

// Attacker code
const {CognitoIdentityClient, GetIdCommand, GetCredentialsForIdentityCommand} = require('@aws-sdk/client-cognito-identity');

Const client = new CognitoIdentityClient({region: 'us-east-1'});

// Get identity ID (unauthenticated)
const {IdentityId} = await client.send(new GetIdCommand({
  IdentityPoolId: 'us-east-1:12345678-1234-1234-1234-123456789012'
}));

// Get credentials
const {Credentials} = await client.send(new GetCredentialsForIdentityCommand({
  IdentityId
}));

// Credentials.AccessKeyId, SecretKey, SessionToken - ready to use
console.log('Credentials:', Credentials);

// Now use these to call AWS services with whatever permissions the unauthenticated role has

What the attacker now has:

Whatever permissions the unauthenticated role has. Common over-permissive configurations:

  • s3:ListBucket on the company's S3 buckets → enumerate all files
  • s3:GetObject * → download any file
  • dynamodb:Scan → dump database tables
  • iam:* (catastrophic but we've seen it) → full IAM control
  • cognito-idp:* → can manipulate User Pool itself

Real finding: a mobile app's Identity Pool had an unauthenticated role with s3:GetObject on the app's main S3 bucket. The bucket contained user profile photos. Which was intended to be publicly accessible. It also contained a backup directory with users.json dumps that weren't intended for public access. The role's permissions didn't distinguish, so anyone with the Identity Pool ID could download the user data dumps.

The fix:

  • Disable unauthenticated access on Identity Pools unless specifically needed
  • If enabled, unauthenticated role should have minimum possible permissions. Typically nothing, or access to truly public content only
  • Scope permissions with conditions. s3:GetObject on a specific path pattern, not wildcards
  • Audit unauthenticated role permissions against the principle: "what if the attacker could do this directly without authentication?"

Attack pattern 2: Authenticated role shared across all authenticated users

Identity Pools have a single authenticated role by default. Every user who successfully authenticates gets the same IAM role.

The problem: this role must be permissive enough for the most privileged user but is assumed by every authenticated user including low-privilege ones.

Real finding: a SaaS product had customers at different plan tiers. The authenticated role granted dynamodb:* on the app's tables. Needed because admin users needed full access. But the same role was assumed by every paying customer. Any customer could read every other customer's data via direct DynamoDB queries using their authenticated credentials.

The fix:

  • Use IAM policy conditions based on identity claims. cognito-identity.amazonaws.com:sub as a condition ensures users only access their own data:
{
    "Effect": "Allow",
    "Action": "dynamodb:GetItem",
    "Resource": "arn:aws:dynamodb:*:*:table/user-data",
    "Condition": {
      "ForAllValues:StringEquals": {
        "dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]
      }
    }
  }

  • Multiple authenticated roles based on user groups (Cognito supports role-based claims)
  • Per-user resource isolation enforced via IAM conditions

Attack pattern 3: Role mapping rule exploitation

Identity Pools can map users to different roles based on their Cognito User Pool group membership or custom claims. Called "role-based access control" in Cognito.

The configuration:

IF user is in "Admin" group THEN assume admin-role
ELSE IF user is in "Editor" group THEN assume editor-role
ELSE assume default-role

Common bugs:

Bug 3a: Group membership from JWT without validation

If the mapping trusts JWT claims without validating the JWT signature correctly, attackers who can forge JWTs (via weak secrets, key confusion, etc.) can claim admin group membership and assume admin role.

Bug 3b: Custom claims from user-writable sources

If the mapping uses custom:role claim. And users can update their own custom:role attribute via the User Pool API, they can promote themselves.

Cognito User Pool attributes can be configured as:

  • Read-only. Can only be set by admin
  • Writable. User can update via API

If custom:role is set as writable and the Identity Pool mapping uses it, self-promotion is trivial.

Real finding: a marketplace app had Identity Pool mapping that assumed admin role when custom:isAdmin = "true". The custom:isAdmin attribute was writable by users. Attacker updated their own attribute via Cognito User Pool API → refreshed their tokens → new token had custom:isAdmin = true → Identity Pool assumed admin role → full AWS admin access.

The fix:

  • Attributes used in role mapping must be admin-only (read-only for users)
  • Validate JWT signatures correctly in custom mapping code
  • Test role mapping by manipulating attributes and confirming privileges don't escalate
  • Audit writable attributes for any that are security-relevant

Attack pattern 4: Identity Pool ID exposure

Identity Pool IDs look like us-east-1:12345678-abcd-1234-abcd-123456789012. They're not secret. They're embedded in frontend code by design.

Enumeration:

# Find Cognito Identity Pool IDs in frontend code
curl https://target.example.com | grep -oE 'us-[a-z]+-[0-9]+:[a-f0-9-]+'
# Or in main bundle
curl https://target.example.com/main.js | grep -oE 'us-[a-z]+-[0-9]+:[a-f0-9-]+'

Nearly every Cognito-using frontend has the Identity Pool ID visible in source.

The security assumption is that knowing the Identity Pool ID doesn't help an attacker. Only valid authentication (or unauthenticated-if-enabled) gets IAM credentials.

When this assumption breaks:

  • Unauthenticated access is enabled with excessive permissions (pattern 1)
  • Authenticated role is over-privileged (pattern 2)
  • Role mapping has bugs (pattern 3)

So while the Identity Pool ID itself being public isn't the vulnerability, it's the entry point that makes the other patterns exploitable.

Attack pattern 5: User Pool password policy and bypass

Beyond Identity Pools, User Pool security matters. Common issues:

Weak password policies

Default Cognito password policy requires 8 characters with one uppercase, one lowercase, one number, one special. Easily bypassable with passwords like Password1!.

The fix: stronger policy (12+ characters minimum, or passphrase-style encouraged).

MFA not enforced

Cognito supports MFA (SMS and TOTP) but it's optional by default. Many deployments don't enforce it.

The fix: enforce MFA for all users or at minimum for admin users.

Password reset flow bypass

Cognito's password reset flow uses a confirmation code sent via email or SMS. If the email is compromised or SMS is intercepted, password reset gives attacker full access.

The fix: additional verification (security questions, admin approval for sensitive accounts) for password reset.

User enumeration via signup

Some Cognito configurations return different responses when a user exists vs. doesn't, enabling enumeration of valid user accounts.

The fix: use "preventUserExistenceErrors: 'ENABLED'" setting.

Attack pattern 6: Lambda trigger logic bugs

Cognito supports Lambda triggers that run at various lifecycle events:

  • PreSignUp (validate signup data)
  • PostConfirmation (runs after email verification)
  • PreAuthentication (checks before auth)
  • PostAuthentication (runs after successful auth)
  • PreTokenGeneration (modify token claims)
  • CustomMessage (customize email/SMS content)

Vulnerability patterns:

Pattern 6a: PreTokenGeneration adding unsafe claims

// BAD
exports.handler = async (event) => {
  event.response.claimsOverrideDetails = {
    claimsToAddOrOverride: {
      'custom:admin': event.request.userAttributes['custom:admin']
      // User-controlled attribute becomes a claim
    }
  };
  return event;
};

If custom:admin is user-writable, this Lambda promotes users to admin in their tokens. Downstream systems consuming the token see admin claims.

Pattern 6b: PreSignUp auto-confirming users

// Auto-confirm users without email verification
exports.handler = async (event) => {
  event.response.autoConfirmUser = true;
  event.response.autoVerifyEmail = true;
  return event;
};

Accounts created without email verification. Attacker registers with victim's email, never needs to prove ownership. Often intended for testing but left in production.

Pattern 6c: Email enumeration via error messages

Custom Lambda triggers that return different errors for existing vs. nonexistent users enable enumeration.

The fix:

  • Review all Lambda triggers for security implications
  • Validate inputs strictly
  • Use least-privilege trigger permissions
  • Test triggers from attacker perspective

Attack pattern 7: Federation with untrusted providers

Cognito Identity Pools can federate with:

  • Cognito User Pools
  • Google, Facebook, Amazon, Apple
  • Any OIDC provider
  • Any SAML provider
  • Custom (with a developer-authenticated flow)

Federation risks:

Custom OIDC / SAML providers without email verification

If the custom provider doesn't verify email ownership, attackers can claim any email. Then Cognito accepts them as authenticated.

Account linking via email without verification

Cognito's Identity Pool can link accounts across providers by email. If one provider doesn't verify email, attacker signs up there with victim's email, links to Cognito, impersonates.

Developer-authenticated flow without proper validation

The developer-authenticated flow lets your backend vouch for a user's identity without going through a standard OIDC provider. If your backend vouches incorrectly (bug in the validation logic), attackers get authenticated.

The fix:

  • Email verification required for all providers
  • Don't automatically link accounts across providers
  • Careful review of developer-authenticated flow implementation
  • Audit which providers are federated

Attack pattern 8: Refresh token lifetime

Cognito tokens:

  • Access token / ID token: short-lived (default 1 hour)
  • Refresh token: long-lived (default 30 days, configurable up to 10 years)

An attacker who captures a refresh token can keep generating new access/ID tokens indefinitely up to the refresh token's lifetime.

Common issues:

  • Default 30-day refresh token is long
  • Configurable refresh token extended to years without reason
  • Refresh tokens stored insecurely (localStorage in browsers)
  • No refresh token revocation process

The fix:

  • Shorter refresh token lifetimes (hours or days, not months)
  • Refresh token storage in secure cookies with httpOnly, secure, sameSite
  • Revocation on logout, password change, suspicious activity
  • Monitor for unusual refresh patterns

Attack pattern 9: Cross-origin resource sharing (CORS) on Cognito endpoints

Cognito's hosted UI and token endpoints need to be reachable by your application. CORS configuration on these endpoints matters.

Issues we find:

  • Cognito endpoints responding with overly broad CORS headers
  • Custom redirect URLs that include attacker-controllable domains
  • Post-authentication redirect URLs validated incompletely

The fix:

  • Specific allowed callback URLs (no wildcards)
  • Validated post-authentication redirects
  • CORS configured specifically for your application's origins

Attack pattern 10: Logging and monitoring gaps

Cognito authentication events are loggable via CloudTrail and CloudWatch. Common gaps:

  • CloudTrail not enabled for Cognito region
  • CloudWatch metrics not reviewed regularly
  • No alerting on anomalous patterns (brute force, impossible travel, credential stuffing)
  • User Pool analytics not integrated with SIEM

The fix:

  • CloudTrail enabled for all Cognito regions
  • CloudWatch dashboards for authentication metrics
  • Alerts on failed authentication spikes, unusual geolocations, impossible travel
  • Integrate with SIEM (Splunk, Datadog, SumoLogic) for correlation

The hardening checklist

For any AWS Cognito deployment:

User Pool

  • [ ] Strong password policy (12+ characters or passphrase)
  • [ ] MFA enforced for all users or at minimum admins
  • [ ] Email verification required
  • [ ] Phone verification required for SMS MFA users
  • [ ] preventUserExistenceErrors: ENABLED
  • [ ] Account takeover protection enabled (Cognito's Advanced Security)
  • [ ] Custom attributes correctly set as read-only if security-relevant

Identity Pool

  • [ ] Unauthenticated access disabled (unless specifically needed)
  • [ ] If unauthenticated enabled, role has minimum permissions
  • [ ] Authenticated role uses IAM conditions scoped to user identity
  • [ ] Multiple roles per group/tier (not single shared role)
  • [ ] Role mapping uses admin-only attributes
  • [ ] Conditions in IAM policies prevent cross-user access

Federation

  • [ ] Email verification required from all identity providers
  • [ ] Account linking explicit, requires re-authentication
  • [ ] Custom providers validated correctly
  • [ ] Developer-authenticated flow reviewed for security

Lambda triggers

  • [ ] Triggers reviewed for security implications
  • [ ] User-controlled data not used for privilege escalation
  • [ ] Inputs validated
  • [ ] No auto-confirm in production

Tokens

  • [ ] Access/ID token lifetime appropriate (default 1 hour fine)
  • [ ] Refresh token lifetime reasonable (hours/days, not months/years)
  • [ ] Tokens stored securely in applications
  • [ ] Revocation process for suspected compromise

Monitoring

  • [ ] CloudTrail enabled for Cognito
  • [ ] CloudWatch metrics reviewed
  • [ ] Alerting on anomalies
  • [ ] Integration with SIEM
  • [ ] Advanced Security Features enabled (risk-based authentication)

IAM policy patterns that work

Per-user S3 folder access

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-bucket/users/${cognito-identity.amazonaws.com:sub}/*"
    }
  ]
}

Each user can only access their own folder. Identity substitution enforces isolation.

Per-user DynamoDB row access

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem"],
      "Resource": "arn:aws:dynamodb:*:*:table/user-data",
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"]
        }
      }
    }
  ]
}

User can only access rows where the partition key matches their identity.

Read-only access to shared read data + per-user write

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-bucket/public/*"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-bucket/users/${cognito-identity.amazonaws.com:sub}/*"
    }
  ]
}

For different deployment types

Mobile apps using Cognito for direct AWS access

The canonical use case. Tight IAM policies with identity substitution are essential. Never expose broad permissions even to authenticated users.

Web apps using Cognito for authentication, backend for authorization

If your backend validates Cognito tokens and uses its own IAM credentials to call AWS services, Cognito's role in IAM authorization is narrower. Focus security on:

  • Token validation in the backend
  • Session management
  • Logout and revocation

Multi-tenant SaaS using Cognito

Tenant isolation via IAM conditions is critical. Each tenant's data should be inaccessible to other tenants' authenticated users. Test this thoroughly.

For Valtik clients

Valtik's AWS Cognito audits include:

  • User Pool configuration review (password policy, MFA, attributes)
  • Identity Pool role audit (unauthenticated and authenticated roles)
  • IAM policy review (per-user scoping, condition usage)
  • Lambda trigger security review
  • Federation configuration review
  • Token configuration review
  • Monitoring and logging review

If you use Cognito in production and haven't had an explicit security review, reach out via https://valtikstudios.com.

The honest summary

Cognito is a powerful AWS-native identity service. Its security depends heavily on configuration. Particularly Identity Pool role assignments. The attack patterns in this post are real findings from audits.

Focus your Cognito security thinking on Identity Pools more than User Pools. Unauthenticated access is the most dangerous setting. Authenticated role scoping via IAM conditions is the most important control.

Audit your Cognito deployment. Your users' data accessibility depends on configurations that are easy to get wrong.

Sources

  1. AWS Cognito Documentation
  2. Cognito Identity Pools Security
  3. Cognito User Pools Security
  4. IAM Policy Variables
  5. Cognito Advanced Security Features
  6. AWS CloudTrail with Cognito
  7. OWASP Authentication Cheat Sheet
  8. AWS Well-Architected Security Pillar
  9. CIS AWS Foundations Benchmark
  10. AWS Cognito Security Research. Various
aws cognitoaws securityidentityiamrole confusionpenetration testingcloud securityapplication securityresearch

Want us to check your AWS setup?

Our scanner detects this exact misconfiguration. plus dozens more across 38 platforms. Free website check available, no commitment required.

Get new research in your inbox
No spam. No newsletter filler. Only new posts as they publish.