Guide · AWS Cognito · Identity Pools

Cognito Identity Pools for MCP Servers — Federated AWS Credentials and Cross-Account Access

Cognito Identity Pools (formerly Cognito Federated Identities) bridge the gap between user authentication and AWS service authorization. While a Cognito User Pool authenticates users and issues JWTs, an Identity Pool exchanges those JWTs for temporary AWS credentials (STS AccessKeyId, SecretAccessKey, SessionToken) scoped to an IAM role. For MCP server deployments, this unlocks a powerful pattern: agent code on the client side can directly access S3 buckets, DynamoDB tables, or other AWS services under the user's identity — without every request flowing through your MCP server as a proxy. The credentials expire in approximately one hour and carry the user's Cognito identity in the session context for row-level access control. This guide covers creating an identity pool linked to a User Pool, configuring authenticated and unauthenticated IAM roles, implementing the GetId / GetCredentialsForIdentity flow, and using cognito-identity.amazonaws.com:sub in IAM condition keys for per-user row-level security. For the authentication layer that produces the tokens exchanged here see Cognito User Pools for MCP Servers.

TL;DR

Create an identity pool linked to your User Pool, define authenticated and unauthenticated IAM roles with trust policies that include the Cognito identity pool condition, then exchange a User Pool ID token for temporary AWS credentials via GetId + GetCredentialsForIdentity. Use ${cognito-identity.amazonaws.com:sub} in DynamoDB/S3 IAM conditions for per-user data isolation. For the JWT tokens that feed into this exchange see Verifying Cognito JWTs. For OAuth2 token acquisition see OAuth2 and OIDC Flows.

Architecture: User Pools vs Identity Pools

User Pools and Identity Pools are complementary services that are often used together but serve distinct purposes:

The combination allows an AI agent using your MCP server to authenticate as a user (via User Pool), then obtain time-limited AWS credentials (via Identity Pool) to directly read or write to per-user S3 prefixes or DynamoDB rows — without your MCP server acting as a data proxy for every API call.

Create the Identity Pool

The identity pool specifies which identity providers it trusts. For integration with a Cognito User Pool, you provide the pool's provider name and the app client ID.

# Store values from your User Pool setup
USER_POOL_ID="us-east-1_AbCdEfGhI"
REGION="us-east-1"
USER_POOL_CLIENT_ID="abcdefg1234567890"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

# Create the identity pool linked to your Cognito User Pool
aws cognito-identity create-identity-pool \
  --identity-pool-name mcp-server-identity-pool \
  --allow-unauthenticated-identities \
  --cognito-identity-providers \
    "ProviderName=cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID},ClientId=${USER_POOL_CLIENT_ID},ServerSideTokenCheck=true" \
  --tags Key=Service,Value=mcp-server Key=Environment,Value=production
# Output: { "IdentityPoolId": "us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }

IDENTITY_POOL_ID="us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

ServerSideTokenCheck=true causes Cognito to validate the User Pool token on the server side before issuing credentials — it checks that the token is not revoked (important if you use token revocation via logout). Without this, a revoked refresh token's previously issued ID tokens could still be used to obtain identity pool credentials until they expire naturally.

Create IAM roles for the identity pool

The identity pool needs two IAM roles: one for authenticated users (have logged in with a Cognito User Pool, Google, etc.) and optionally one for unauthenticated users (guests). The trust policies must include specific condition keys that prevent role assumption from outside the identity pool context.

# Create the trust policy document for the authenticated role
cat > /tmp/cognito-authenticated-trust.json << 'EOF'
{
  "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:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        },
        "ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "authenticated"
        }
      }
    }
  ]
}
EOF

# Create the authenticated role
AUTHENTICATED_ROLE_ARN=$(aws iam create-role \
  --role-name CognitoMCPAuthenticatedRole \
  --assume-role-policy-document file:///tmp/cognito-authenticated-trust.json \
  --query 'Role.Arn' \
  --output text)

echo "Authenticated role ARN: $AUTHENTICATED_ROLE_ARN"
# Create the trust policy for unauthenticated (guest) role
cat > /tmp/cognito-unauthenticated-trust.json << 'EOF'
{
  "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:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        },
        "ForAnyValue:StringLike": {
          "cognito-identity.amazonaws.com:amr": "unauthenticated"
        }
      }
    }
  ]
}
EOF

UNAUTHENTICATED_ROLE_ARN=$(aws iam create-role \
  --role-name CognitoMCPUnauthenticatedRole \
  --assume-role-policy-document file:///tmp/cognito-unauthenticated-trust.json \
  --query 'Role.Arn' \
  --output text)

# Attach roles to identity pool
aws cognito-identity set-identity-pool-roles \
  --identity-pool-id $IDENTITY_POOL_ID \
  --roles \
    "authenticated=${AUTHENTICATED_ROLE_ARN},unauthenticated=${UNAUTHENTICATED_ROLE_ARN}"

IAM permission policies for MCP server use cases

The authenticated role's permission policy defines what AWS services users can access with their temporary credentials. Use the ${cognito-identity.amazonaws.com:sub} condition key variable to scope access to the specific user's data.

# Permission policy: authenticated users can access their own S3 prefix and DynamoDB rows
cat > /tmp/cognito-authenticated-permissions.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3UserPrefix",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::mcp-server-user-data/${cognito-identity.amazonaws.com:sub}",
        "arn:aws:s3:::mcp-server-user-data/${cognito-identity.amazonaws.com:sub}/*"
      ]
    },
    {
      "Sid": "AllowDynamoDBUserRows",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/mcp-server-user-context",
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": [
            "${cognito-identity.amazonaws.com:sub}"
          ]
        }
      }
    },
    {
      "Sid": "AllowCognitoIdentityRefresh",
      "Effect": "Allow",
      "Action": [
        "cognito-sync:*",
        "cognito-identity:*"
      ],
      "Resource": "*"
    }
  ]
}
EOF

aws iam put-role-policy \
  --role-name CognitoMCPAuthenticatedRole \
  --policy-name MCPUserDataAccess \
  --policy-document file:///tmp/cognito-authenticated-permissions.json
# Permission policy for unauthenticated (guest) role — minimal permissions
# Guests can read public MCP tool definitions but cannot write user data
cat > /tmp/cognito-unauthenticated-permissions.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPublicToolDefinitions",
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::mcp-server-public-tools/*"
    },
    {
      "Sid": "DenyEverythingElse",
      "Effect": "Deny",
      "Action": [
        "s3:PutObject",
        "s3:DeleteObject",
        "dynamodb:*"
      ],
      "Resource": "*"
    }
  ]
}
EOF

aws iam put-role-policy \
  --role-name CognitoMCPUnauthenticatedRole \
  --policy-name MCPGuestAccess \
  --policy-document file:///tmp/cognito-unauthenticated-permissions.json

Role types comparison

Role typeamr conditionWhen it appliesTypical permissionsUse in MCP servers
AuthenticatedauthenticatedUser has logged in via a trusted identity provider (User Pool, Google, SAML, etc.)Read/write user's own data; invoke specific Lambda functions; read from shared resourcesAgents acting on behalf of a logged-in user; direct S3/DynamoDB access with per-user isolation
UnauthenticatedunauthenticatedGuest access — identity pool created an anonymous identity for the userRead-only access to public resources; no writesPre-login exploration; public tool discovery; onboarding flows before requiring login
Developer-authenticatedCustom provider nameYour backend validates the user and calls GetOpenIdTokenForDeveloperIdentity to issue credentialsSame as authenticated — defined by the role attached to the developer providerMigration scenarios; custom identity validation; linking social logins to your own user records
Role-based enhanced flown/a (enhanced auth flow)Identity pool chooses role based on token claims (e.g., Cognito group membership)Different roles for different user groups (e.g., admin group gets broader permissions)Multi-tier MCP access (free vs pro vs admin) without backend logic

Get temporary AWS credentials (the exchange flow)

The credential exchange is a two-step process. First, call GetId with the User Pool ID token to obtain a Cognito Identity ID. Second, call GetCredentialsForIdentity with that Identity ID and the same token to receive STS credentials. Both calls are made using the Cognito Identity client (not Cognito IDP), using anonymous credentials (no AWS credentials needed for the initial call).

# CLI example: exchange a Cognito User Pool ID token for temporary AWS credentials
# In production, this is done client-side using the AWS SDK

# Step 1: GetId — exchange the User Pool ID token for a Cognito Identity ID
# The identity ID is stable for the same user across sessions (persisted by Cognito)
ID_TOKEN="eyJra..."  # The id_token from your User Pool authentication response

IDENTITY_ID=$(aws cognito-identity get-id \
  --account-id $ACCOUNT_ID \
  --identity-pool-id $IDENTITY_POOL_ID \
  --logins "cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}=${ID_TOKEN}" \
  --query 'IdentityId' \
  --output text)

echo "Cognito Identity ID: $IDENTITY_ID"
# e.g. "us-east-1:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"

# Step 2: GetCredentialsForIdentity — get STS credentials
# Credentials include AccessKeyId, SecretAccessKey, SessionToken, Expiration (~1h)
aws cognito-identity get-credentials-for-identity \
  --identity-id $IDENTITY_ID \
  --logins "cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}=${ID_TOKEN}"
# Output:
# {
#   "IdentityId": "us-east-1:aaaaaaaa-...",
#   "Credentials": {
#     "AccessKeyId": "ASIAIOSFODNN7EXAMPLE",
#     "SecretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
#     "SessionToken": "AQoDYXdzEJr...",
#     "Expiration": "2026-10-09T15:30:00Z"
#   }
# }
// JavaScript SDK example: complete identity pool credential flow
// Used in Claude Desktop or browser-based MCP clients
const { CognitoIdentityClient, GetIdCommand, GetCredentialsForIdentityCommand } = require("@aws-sdk/client-cognito-identity");
const { S3Client, GetObjectCommand } = require("@aws-sdk/client-s3");

const REGION = "us-east-1";
const USER_POOL_ID = "us-east-1_AbCdEfGhI";
const IDENTITY_POOL_ID = "us-east-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";

async function getAWSCredentialsForUser(idToken) {
  const cognitoIdentity = new CognitoIdentityClient({ region: REGION });
  const loginKey = `cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}`;

  // Step 1: Get or create the stable Cognito Identity ID for this user
  const getIdResponse = await cognitoIdentity.send(new GetIdCommand({
    AccountId: "123456789012",
    IdentityPoolId: IDENTITY_POOL_ID,
    Logins: { [loginKey]: idToken },
  }));

  const identityId = getIdResponse.IdentityId;

  // Step 2: Exchange identity ID + token for temporary STS credentials
  const credResponse = await cognitoIdentity.send(new GetCredentialsForIdentityCommand({
    IdentityId: identityId,
    Logins: { [loginKey]: idToken },
  }));

  const { AccessKeyId, SecretKey, SessionToken, Expiration } = credResponse.Credentials;
  return { AccessKeyId, SecretKey, SessionToken, Expiration, identityId };
}

async function accessUserS3Data(idToken, objectKey) {
  const creds = await getAWSCredentialsForUser(idToken);

  // Use the temporary credentials to access S3 as the user
  const s3 = new S3Client({
    region: REGION,
    credentials: {
      accessKeyId: creds.AccessKeyId,
      secretAccessKey: creds.SecretKey,
      sessionToken: creds.SessionToken,
    },
  });

  // The user can only access their own prefix (enforced by IAM policy)
  const response = await s3.send(new GetObjectCommand({
    Bucket: "mcp-server-user-data",
    // IAM policy uses ${cognito-identity.amazonaws.com:sub} = identityId
    Key: `${creds.identityId}/${objectKey}`,
  }));

  return response.Body;
}

Enhanced auth flow vs basic auth flow

Cognito Identity Pools support two authentication flows. Enhanced flow (the default) is simpler: the identity pool automatically selects the IAM role based on the token's claims or the pool's default role mapping. Basic flow requires the client to specify the exact role ARN to assume, giving more control but requiring extra logic.

# Enhanced auth flow (default) — identity pool picks the role
# The role is determined by:
# 1. Role mappings configured on the identity pool (e.g., by Cognito group)
# 2. Default authenticated role if no mapping matches

# Configure role mappings (enhanced flow): assign roles based on Cognito group membership
# Map the "mcp-admins" group to the admin role; all others get the standard role
aws cognito-identity set-identity-pool-roles \
  --identity-pool-id $IDENTITY_POOL_ID \
  --roles "authenticated=${AUTHENTICATED_ROLE_ARN}" \
  --role-mappings "{
    \"cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}:${USER_POOL_CLIENT_ID}\": {
      \"Type\": \"Rules\",
      \"AmbiguousRoleResolution\": \"AuthenticatedRole\",
      \"RulesConfiguration\": {
        \"Rules\": [
          {
            \"Claim\": \"cognito:groups\",
            \"MatchType\": \"Contains\",
            \"Value\": \"mcp-admins\",
            \"RoleARN\": \"${ADMIN_ROLE_ARN}\"
          }
        ]
      }
    }
  }"

# Basic auth flow — client specifies the role ARN
# Used when you need to assume a cross-account role or override the default
aws cognito-identity get-credentials-for-identity \
  --identity-id $IDENTITY_ID \
  --logins "cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}=${ID_TOKEN}" \
  --custom-role-arn "arn:aws:iam::123456789012:role/SpecificMCPRole"
# Note: the specified role must trust cognito-identity.amazonaws.com and
# the identity pool ID must be in its Condition block

Row-level security with cognito:sub in IAM conditions

The most powerful feature of identity pool credentials is the ability to use the authenticated user's Cognito identity ID as a condition key in IAM policies. This enforces data isolation at the AWS service level — no application code needed to filter by user ID.

# DynamoDB table with user isolation using cognito-identity sub
# Table schema: partition key = userId (Cognito Identity ID)
aws dynamodb create-table \
  --table-name mcp-server-user-context \
  --attribute-definitions \
    AttributeName=userId,AttributeType=S \
    AttributeName=contextKey,AttributeType=S \
  --key-schema \
    AttributeName=userId,KeyType=HASH \
    AttributeName=contextKey,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1

# IAM policy using LeadingKeys condition for row-level security
# ${cognito-identity.amazonaws.com:sub} is resolved to the user's identity ID at runtime
# This means each user can only access rows where userId == their Cognito Identity ID
cat > /tmp/dynamodb-row-level-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/mcp-server-user-context",
      "Condition": {
        "ForAllValues:StringEquals": {
          "dynamodb:LeadingKeys": [
            "${cognito-identity.amazonaws.com:sub}"
          ]
        }
      }
    }
  ]
}
EOF

# The condition variable ${cognito-identity.amazonaws.com:sub} is set automatically
# by AWS when AssumeRoleWithWebIdentity is called with a valid Cognito identity token
# It equals the Cognito Identity ID (e.g., "us-east-1:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
# This is NOT the same as the User Pool's "sub" claim — it is the Identity Pool's stable ID
# S3 per-user prefix isolation
# Each user can only access s3://mcp-server-user-data/{their-identity-id}/
cat > /tmp/s3-per-user-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::mcp-server-user-data",
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "${cognito-identity.amazonaws.com:sub}/",
            "${cognito-identity.amazonaws.com:sub}/*"
          ]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::mcp-server-user-data/${cognito-identity.amazonaws.com:sub}/*"
    }
  ]
}
EOF

AliveMCP and identity pool health monitoring

Identity pool credential exchange failures can silently degrade your MCP server — users authenticate successfully with their User Pool token but cannot access S3 or DynamoDB because the identity pool's IAM role trust policy is misconfigured, the identity pool was deleted, or STS is throwing errors in the region. These failures don't appear as HTTP 5xx on your MCP server's main endpoint — they surface as errors in the agent's AWS SDK calls, often mistaken for application bugs.

AliveMCP monitors your MCP server's end-to-end health, including the authenticated data access paths. By configuring a synthetic health-check user whose credentials AliveMCP uses to test the full flow (User Pool login → Identity Pool credential exchange → S3/DynamoDB access), AliveMCP can alert you when any layer of the authentication stack fails — not just when the MCP server process is unreachable.

Failure modes reference

SymptomCauseFix
NotAuthorizedException: Invalid identity pool configuration on GetIdIdentity pool ID is wrong; identity pool was deleted; region mismatch between the SDK call and the identity poolVerify identity pool ID with aws cognito-identity describe-identity-pool --identity-pool-id $ID; check the region in the identity pool ID prefix matches the SDK region
NotAuthorizedException: Invalid login tokenThe login key string is malformed (must be exactly cognito-idp.{region}.amazonaws.com/{userPoolId}); or the token passed is expired; or it is the access token instead of the ID token (identity pools require the ID token)Use the ID token (not access token) for identity pool authentication; check the login provider key format exactly matches the user pool; refresh the token if expired
AccessDeniedException when using credentials from identity poolRole trust policy missing the identity pool ARN condition; cognito-identity.amazonaws.com:aud condition points to wrong identity pool ID; the role's permission policy does not include the required AWS service actionCheck the role's trust policy includes StringEquals: "cognito-identity.amazonaws.com:aud": "{identityPoolId}"; verify the attached permission policy has the required actions
Row-level DynamoDB access denied even though userId matchesThe DynamoDB condition uses dynamodb:LeadingKeys but the put/get request is using a different key attribute name; or the request is querying a secondary index instead of the table primary keyVerify the DynamoDB table partition key name matches what the IAM condition expects; check if the operation targets the table ARN or index ARN (conditions on the table ARN don't apply to indexes — add a separate statement for the index ARN)
Credentials expire before the user session endsSTS credentials from identity pools expire in ~1 hour; no automatic refresh in the SDK unless configuredUse @aws-sdk/credential-providers fromCognitoIdentityPool which handles automatic credential refresh; or implement a credential refresh loop using the stored ID token to call GetCredentialsForIdentity again before expiry
InvalidIdentityPoolConfigurationException: Basic (classic) flow is not enabledBasic auth flow is disabled on the identity pool but the client called GetCredentialsForIdentity with a custom-role-arnEither enable Basic flow on the identity pool (AllowClassicFlow=true) or use enhanced flow without specifying a custom-role-arn