Home› Blog› AWS Architecture Series #89 — Your app picks the role; the trust policy decides whether it may…
AWS Architecture AWS Architecture Series

AWS Architecture Series #89 — Your app picks the role; the trust policy decides whether it may

An identity pool has two authentication flows that end in the same place and decide the role in different places. In the enhanced flow the pool picks the role. In the basic flow your application does — and AWS states the boundary plainly: the app can request credentials for any IAM role 'that you have configured with a sufficient trust policy'.

Verified against current vendor documentation on 21 October 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.

Business Challenge

An identity pool is the thing that turns a login into AWS credentials. “Amazon Cognito… delivers temporary, limited-privilege credentials to your application to access AWS resources.”

“Amazon Cognito has two different flows for authentication with public providers: enhanced and basic.” They reach the same destination, and the difference between them is one sentence long.

Enhanced: “Enhanced authentication manages the logic of IAM role selection and credentials retrieval in your identity pool configuration.”

Basic: “When you implement the basic authentication flow, your application selects the IAM role that you want users to assume.”

1In the basic flow the client picks the role, and the trust policy is what decides whether it may

AWS states the scope of that plainly, and it is broader than "the role you configured":

“The AssumeRoleWithWebIdentity request in the classic workflow grants your app a greater ability to request credentials for any AWS Identity and Access Management role that you have configured with a sufficient trust policy.”

Read that as an inventory question rather than a feature description. The set of roles your client can obtain is not the set you intended it to use — it is the set whose trust policies happen to accept a token from this identity pool. Any role in the account with a loose enough cognito-identity.amazonaws.com trust relationship is reachable, whether or not anyone thought of it as a mobile app role.

And the documentation is unusually direct about what that depends on: “To implement basic authentication, first evaluate the trust relationships of your IAM roles for web identities. Then build the logic for role selection into your client and secure the client against modification by users.”

"Secure the client against modification by users" is a remarkable sentence to find in a security guide, and it is the honest one. Role selection in the client is a decision made on a device you do not control.

Fix

Prefer the enhanced flow, which AWS calls “the most secure choice with the lowest level of developer effort”. If you are on the basic flow, the audit is not of your app — it is of every role in the account whose trust policy names cognito-identity.amazonaws.com. That list is your actual blast radius, and nothing in the identity pool shortens it.

2You cannot have role mappings and the basic flow, and AWS blocks the combination

This is the detail that makes the design coherent, and it is easy to miss because it appears as an error string:

“When you attempt GetOpenIdToken in an identity pool with role mappings, you receive the following error. Basic (classic) flow is not supported with RoleMappings, please use enhanced flow.”

And the corollary, stated as a property of pools that do support the basic flow: “You can sign in with the Basic authflow in user pools that don't have role mappings. This type of identity pool doesn't have a default authenticated or unauthenticated role, and doesn't have role-based or attribute-based access control configured.”

So the two models are mutually exclusive by enforcement rather than by convention. You cannot configure careful pool-side role mapping and then let a client select its own role — which matters because that configuration would look like a control while controlling nothing.

This is the service refusing to let you hold a false belief. The alternative design, where both are configured and the client silently wins, is the one that produces an audit finding two years later.

Fix

Treat the choice as architectural and made once. If your pool has role mappings, the basic flow is not available to you and your role-selection logic belongs in the pool. If your pool has no mappings, understand that there is no default role either — nothing is deciding on your behalf.

3A guest identity is still an identity, and it still has a role

Guest access is a first-class configuration choice, not an absence of one: “In Configure identity pool trust, choose to set up your identity pool for Authenticated access, Guest access, or both”, and then “In Configure permissions, choose a default IAM role for authenticated or guest users in your identity pool.”

So an unauthenticated caller does not get no credentials. It gets credentials from a role: “You can also define a separate IAM role with limited permissions for guest users who are not authenticated.”

What separates the two at the IAM layer is one condition in the trust policy. AWS's example restricts the aud to the pool, and then: “the policy specifies that one of the array members of the multi-value amr claim of the token issued by the Amazon Cognito GetOpenIdToken API action has the value authenticated.”

Which means the authenticated role is protected from guest tokens by the presence of that amr condition and by nothing else. Omit it and a guest token satisfies a trust policy written for signed-in users.

Fix

Read the trust policy of your authenticated role and confirm the amr condition is there. AWS is explicit about the purpose: the trust policy exists so the role “can only be assumed by Amazon Cognito for authenticated users in your identity pool” — and both halves of that sentence, the pool restriction and the authenticated restriction, are separate conditions you have to have written.

Architecture

A token in, an identity ID in the middle, AWS credentials out — and a decision about where the role is chosen that changes the whole security model.

Diagram: how Amazon Cognito identity pools turn a provider token into AWS credentials, and how the two authentication flows differ in where the IAM role is selected. AWS describes Cognito as creating unique identifiers for end users kept consistent across devices and platforms, and as delivering temporary, limited-privilege credentials to an application to access AWS resources. Identity pools accept four kinds of authentication artifact: an ID token from an Amazon Cognito user pool, an ID token from OpenID Connect, a SAML assertion from SAML 2.0, and an access token from a social provider, so the social provider case is asymmetric in presenting an access token rather than an ID token. There are two flows for authentication with public providers, enhanced and basic, and they reach the same destination while deciding the role in different places. In the enhanced flow, the application presents proof of authentication in a GetId request, the pool returns an identity ID, the application combines that identity ID with the same proof of authentication in a GetCredentialsForIdentity request, and the pool returns AWS credentials; the order of operations is GetId then GetCredentialsForIdentity. Enhanced authentication manages the logic of IAM role selection and credentials retrieval in the identity pool configuration, the GetCredentialsForIdentity request requests a role based on the contents of an access token, the pool can be configured to select a default role or to apply attribute-based or role-based access control to role selection, and the resulting AWS credentials are valid for one hour. In the basic or classic flow, the application selects the IAM role that it wants users to assume. The application presents proof of authentication in a GetId request, combines the identity ID with the same proof in a GetOpenIdToken request which returns a new OAuth 2.0 token issued by the identity pool, then presents that token in an AssumeRoleWithWebIdentity request to AWS Security Token Service, which returns AWS credentials; the order is GetId, GetOpenIdToken, AssumeRoleWithWebIdentity. The central finding concerns the scope of what the client can then request. AWS states that the AssumeRoleWithWebIdentity request in the classic workflow grants the app a greater ability to request credentials for any AWS Identity and Access Management role that has been configured with a sufficient trust policy, and that a custom role session duration can also be requested. So the set of roles the client can obtain is not the set the developer intended it to use but the set whose trust policies happen to accept a token from this identity pool, meaning any role in the account with a loose enough cognito-identity.amazonaws.com trust relationship is reachable. AWS is direct about the prerequisites, advising that to implement basic authentication one should first evaluate the trust relationships of IAM roles for web identities, then build the logic for role selection into the client and secure the client against modification by users, which is an honest statement that role selection in the client is a decision made on a device the developer does not control. AWS also states that when creating a new identity pool one should not activate basic classic authentication by default, as a best practice, and calls the enhanced flow the most secure choice with the lowest level of developer effort because it reduces the complexity, size and rate of API requests, removes the need for additional requests to AWS STS, has the identity pool evaluate users for the IAM role credentials they should receive, and avoids embedding role selection logic in the client. Crucially the two models cannot be mixed, and this is enforced rather than conventional: attempting GetOpenIdToken in an identity pool with role mappings returns the error Basic classic flow is not supported with RoleMappings, please use enhanced flow. Conversely, sign-in with the basic authflow works in pools that do not have role mappings, and such a pool has no default authenticated or unauthenticated role and no role-based or attribute-based access control configured. The service therefore refuses to let a customer configure careful pool-side role mapping and then allow a client to select its own role, which would look like a control while controlling nothing. On guest access, identity pool trust is configured for Authenticated access, Guest access, or both, and a default IAM role is chosen for authenticated or guest users, so an unauthenticated caller receives credentials from a role rather than no credentials, with AWS noting a separate IAM role with limited permissions can be defined for guest users who are not authenticated. What separates the two at the IAM layer is the trust policy: AWS states it is important to add the appropriate trust policy for each role so that it can only be assumed by Amazon Cognito for authenticated users in the identity pool, and the example policy allows federated users from cognito-identity.amazonaws.com, restricts the aud of the token, in this case the identity pool ID, to match the identity pool, and specifies that one of the array members of the multi-value amr claim of the token issued by the GetOpenIdToken API action has the value authenticated. The authenticated role is therefore protected from guest tokens by the presence of that amr condition and by nothing else. Two further details matter: permissions for each user are controlled through IAM roles the customer creates with rules choosing the role based on claims in the user's ID token, and granting iam PassRole permission to pass a role to the set-identity-pool-roles API allows a user to set roles with permissions in excess of their own. Also, a Custom developer provider cannot be modified or deleted after the identity pool is created, and in the developer-authenticated flow the claims presented are, from the pool's perspective, arbitrary identifiers whose authentication is authorized by IAM credentials encoded in the application. The recommended actions are to prefer the enhanced flow, to audit every role in the account whose trust policy names cognito-identity.amazonaws.com if the basic flow is in use because that list is the real blast radius, to confirm the amr condition is present on the authenticated role, and to treat the flow choice as an architectural decision made once.
Same destination. The role is decided in two different places, with two different boundaries.

What the pool accepts, and the one asymmetry in it

ProviderAuthentication artifact
Amazon Cognito user poolID token
OpenID Connect (OIDC)ID token
SAML 2.0SAML assertion
Social providerAccess token

Three of those are assertions about identity. The fourth is a bearer credential for an API. That is not a defect — it is what social providers issue — but it is worth knowing that the artifact your pool trusts from a social login is a different kind of object from the one it trusts from OIDC.

The two flows, step for step

Enhanced (simplified)Basic (classic)
Order of operations GetId → GetCredentialsForIdentity GetId → GetOpenIdToken → AssumeRoleWithWebIdentity
Who selects the role The pool — it “manages the logic of IAM role selection” Your application
What bounds the choice Pool configuration: default role, RBAC or ABAC “any… role that you have configured with a sufficient trust policy”
Credential lifetime “valid for one hour” “You can also request a custom role session duration”
Calls AWS STS directly No — “doesn't need to make additional API requests to AWS STS” Yes
Works with role mappings Yes No — the request errors
The token in the middle of the basic flow is issued by your pool, not by your provider

“GetOpenIdToken returns a new OAuth 2.0 token that is issued by your identity pool.”

That is the token STS evaluates, and it is why the trust policy conditions are written against cognito-identity.amazonaws.com and the pool's own aud and amr claims rather than against your upstream provider. The provider's assertion is consumed earlier and does not reach STS.

Where role selection can be configured, when the pool is doing it

“Permissions for each user are controlled through IAM roles that you create. You can define rules to choose the role for each user based on claims in the user's ID token.” And the enhanced flow's own mechanism: “The GetCredentialsForIdentity request of the enhanced authflow requests a role based on the contents of an access token.”

So in the enhanced flow the claims decide, evaluated server-side by the pool. In the basic flow the client decides and the claims only matter insofar as a trust policy condition tests them. Same claims, two completely different enforcement points.

And one permission that lets someone exceed their own access

“To allow a user to set roles with permissions in excess of the user's existing permissions on an identity pool, grant them iam:PassRole permission to pass the role to the set-identity-pool-roles API.”

Whoever can set the pool's roles can grant the pool's users more than that person holds. That is how PassRole is supposed to work, and it means the right to configure an identity pool is a privilege-escalation path unless the roles that can be passed are constrained.

One configuration choice that cannot be undone

“If you configure a Custom developer provider, you can't modify or delete it after you create your identity pool.”

Worth noting at creation time rather than discovering later, because the developer-authenticated flow is the one where the pool trusts you completely: “the claims that you present in your request for an identity are arbitrary identifiers, and the authentication is authorized by IAM credentials that you encode in your application.”

Why This Architecture Holds Up

Refusing to combine the two models is the best decision here

The error string is the whole argument: “Basic (classic) flow is not supported with RoleMappings, please use enhanced flow.”

A service that allowed both would let you configure role mapping, read it back in the console, show it to an auditor, and have it decide nothing because the client passes its own role ARN to STS. Making the combination an error means the configuration you can see is the configuration that applies. That is worth more than the flexibility it removes.

The enhanced flow is recommended on grounds that are actually about security

“The enhanced flow is the most secure choice with the lowest level of developer effort”, because “Your identity pool evaluates your users for the IAM role credentials that they should receive. You don't need to embed logic for role selection in your client.”

Moving a decision off an untrusted device and into a service is the actual security property, and AWS names it rather than selling the flow on request count. The request-count benefit is real and listed second.

Telling you to secure the client is better than implying you need not

“build the logic for role selection into your client and secure the client against modification by users” is an uncomfortable instruction, and it is the correct one. The basic flow's security depends on client integrity; saying so lets a reader decide whether that assumption holds for a mobile app or a single-page application. The alternative — documenting the flow as "more granular control" and stopping — would leave the dependency unstated.

It is paired with an explicit default: “When you create a new identity pool, don't activate basic (classic) authentication by default, as a best practice.”

Key Architecture Decisions

Decision Choice Reasoning
Which flow Enhanced, unless you have a specific reason AWS calls it “the most secure choice with the lowest level of developer effort”.
If using the basic flow Audit every role trusting cognito-identity.amazonaws.com The client can request “any… role… with a sufficient trust policy”.
Authenticated role trust policy Require the amr condition explicitly It is what distinguishes an authenticated token from a guest one.
Role trust policy aud Pin it to the identity pool ID AWS's example restricts the aud “to match the identity pool”.
Guest access Decide deliberately; it is a configured role An unauthenticated identity receives credentials from a role you choose.
Role mappings plus basic flow Not possible — pick one model GetOpenIdToken returns an error on a pool with role mappings.
Who may call set-identity-pool-roles Constrain it, and constrain what can be passed iam:PassRole here can grant more than the caller holds.
Custom developer provider Decide before creating the pool You “can't modify or delete it after you create your identity pool”.
Session duration One hour on enhanced; custom only on basic A longer session is a reason some teams choose basic — weigh it against client-side selection.
Social provider logins Note the artifact is an access token Unlike OIDC and user pools, which present ID tokens.

The audit worth running

List every IAM role in the account whose trust policy has Federated: cognito-identity.amazonaws.com. For each one, check two conditions: is the aud pinned to a specific pool, and is there an amr condition. A role missing the first is reachable from any of your pools; a role missing the second is reachable by guests.

Then establish which flow each client uses. If any client uses the basic flow, the list above is the set of roles it can request — not the one role you had in mind. That is the number to put in front of whoever owns the risk.

Closing Thought

The interesting thing about identity pools is not that the basic flow is riskier — it is that AWS made the two models impossible to mix and said so with an error rather than a warning. You cannot have a role-mapping configuration that looks authoritative while a client chooses for itself. One or the other.

That decision is worth noticing because the series keeps finding the opposite shape. #84 had a filter that looked like a boundary and was not. #88 had a lifecycle attached to a handle rather than to the secret. Here, the one configuration that could have misled you is the one the service refuses to let you create.

What remains your job is the sentence about trust policies. In the basic flow the bound on what your app can request is not in the pool at all — it is distributed across every role in the account that happens to trust Cognito. That list is discoverable, and it is the only version of "what can this app get" that is true.

Next in this series

Security & Identity — AWS Network Access Analyzer: what a Network Access Scope actually asserts, why a finding is a reachable path rather than a misconfiguration, and how its answers differ from a security group review.

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent