Fixing AWS InvalidIdentityToken: Cognito AssumeRoleWithWebIdentity Audience Mismatch in IAM Trust Policy
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke:
AssumeRoleWithWebIdentityis rejecting your Cognito JWT because theaudclaim in the token (your Cognito App Client ID) does not match thetoken.actions.githubusercontent.com:audorcognito-identity.amazonaws.com:audcondition value hardcoded in your IAM role's trust policy. - How to fix it: Pull the exact App Client ID from Cognito → App clients, and paste it verbatim into the
StringEqualscondition forcognito-identity.amazonaws.com:aud(or the correct OIDC audience key) in your trust policy. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your trust policy JSON and your Cognito pool config and get the corrected diff without sending your ARNs to any external server.
The Incident (What does the error mean?)
You hit this raw error from STS:
An error occurred (InvalidIdentityToken) when calling the AssumeRoleWithWebIdentity operation:
Token is not valid: Token audience is invalid
Or in CloudTrail:
{
"eventName": "AssumeRoleWithWebIdentity",
"errorCode": "InvalidIdentityToken",
"errorMessage": "Token is not valid: Token audience is invalid"
}
Immediate consequence: Every federated authentication attempt fails. Your mobile app, SPA, or backend service cannot obtain temporary AWS credentials. If this is a user-facing login flow backed by Cognito Identity Pools, your entire auth pipeline is down.
STS validates the JWT's aud claim against the StringEquals condition in the trust policy before it even evaluates the role's permission policies. There is no partial success here — it's a hard reject at the federation handshake layer.
The Attack Vector / Blast Radius
This is not just a misconfiguration inconvenience — the audience condition is a security boundary.
Why the aud check exists: Without a strict audience match, any valid JWT issued by your Cognito User Pool — regardless of which App Client it was issued for — could be used to assume the role. An attacker who obtains a JWT from a low-privilege App Client (e.g., a public-facing web client with minimal scopes) could attempt to assume a role intended only for a trusted backend service client.
Blast radius if you "fix" this by removing the aud condition entirely:
- Any token from any App Client in your User Pool can assume the role.
- If your User Pool has a public self-registration flow, an attacker registers, gets a JWT, and calls
AssumeRoleWithWebIdentitydirectly against your backend role. - Combined with an overly permissive role policy, this is a full privilege escalation path from anonymous user to AWS API access.
The correct fix is not to remove the condition — it's to set the right value.
How to Fix It (The Solution)
Root Cause Diagnosis
Decode your Cognito JWT (use jwt.io locally, never paste prod tokens into public tools). Check the aud claim:
{
"sub": "us-east-1_XXXXXXXXX|<user-sub>",
"aud": "3abc4defgh5ijklmno6pqrstu",
"token_use": "id"
}
Now check your IAM trust policy. The aud value in the condition must be an exact string match to the above.
Basic Fix
Navigate to: AWS Console → Cognito → User Pools → [Your Pool] → App clients and copy the App Client ID. Then update your trust policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
- "cognito-identity.amazonaws.com:aud": "us-east-1:WRONG-IDENTITY-POOL-ID-OR-TYPO"
+ "cognito-identity.amazonaws.com:aud": "us-east-1:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "authenticated"
}
}
}
]
}
⚠️ Critical distinction: If you are using Cognito Identity Pools (Federated Identities), the
audvalue is the Identity Pool ID (format:us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), NOT the User Pool App Client ID. If you are using a custom OIDC trust directly against the User Pool issuer, theaudIS the App Client ID. Mixing these up is the #1 cause of this error.
Enterprise Best Practice
For production, lock down both the audience AND the identity pool, and scope by authenticated users only. Use Terraform to prevent drift:
data "aws_iam_policy_document" "cognito_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = ["cognito-identity.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "cognito-identity.amazonaws.com:aud"
- values = ["us-east-1:PLACEHOLDER_NEVER_REPLACED"]
+ values = [var.cognito_identity_pool_id]
}
condition {
test = "ForAnyValue:StringEquals"
variable = "cognito-identity.amazonaws.com:amr"
- # Missing amr condition — allows unauthenticated role assumption
+ values = ["authenticated"]
}
}
}
Store cognito_identity_pool_id in your Terraform variable file or AWS SSM Parameter Store — never hardcode it inline where it can drift from the actual deployed pool.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov — catch placeholder/mismatched audience at plan time:
Write a custom Checkov check or use the built-in CKV_AWS_62 (ensure Cognito roles have constrained trust). Add to your pipeline:
# .github/workflows/iac-scan.yml
- name: Checkov IaC Scan
uses: bridgecrewio/checkov-action@master
with:
directory: ./terraform
check: CKV_AWS_62,CKV_AWS_156
soft_fail: false
2. OPA/Conftest — enforce that aud condition is never a wildcard or empty:
# policies/cognito_trust.rego
package aws.iam.trust
deny[msg] {
stmt := input.Statement[_]
stmt.Action == "sts:AssumeRoleWithWebIdentity"
not stmt.Condition.StringEquals["cognito-identity.amazonaws.com:aud"]
msg := "AssumeRoleWithWebIdentity trust policy missing cognito:aud StringEquals condition"
}
deny[msg] {
stmt := input.Statement[_]
aud := stmt.Condition.StringEquals["cognito-identity.amazonaws.com:aud"]
contains(aud, "PLACEHOLDER")
msg := sprintf("Cognito aud condition contains unreplaced placeholder: %v", [aud])
}
3. Terraform validation block — fail fast before apply:
variable "cognito_identity_pool_id" {
type = string
description = "Cognito Identity Pool ID for IAM trust policy audience"
validation {
condition = can(regex("^[a-z]{2}-[a-z]+-[0-9]:[a-f0-9-]{36}$", var.cognito_identity_pool_id))
error_message = "cognito_identity_pool_id must match format: us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
4. CloudTrail Alarm — alert on repeated InvalidIdentityToken in production:
aws cloudwatch put-metric-filter \
--log-group-name CloudTrail/DefaultLogGroup \
--filter-name CognitoAudienceMismatch \
--filter-pattern '{ ($.eventName = "AssumeRoleWithWebIdentity") && ($.errorCode = "InvalidIdentityToken") }' \
--metric-transformations metricName=InvalidIdentityTokenCount,metricNamespace=Security/STS,metricValue=1
This catches both misconfiguration regressions after deploys and active token-probing attempts by external actors.