Lab 12 — Federation¶
In one sentence
Sign in to AWS without IAM users, the two ways the exam asks about: workforce users through IAM Identity Center (permission sets, ABAC across accounts) and customer users through Cognito (user pool authenticates, identity pool hands out temporary credentials scoped to their own data).
The idea¶
WORKFORCE (Part A) CUSTOMERS (Parts B–D)
sec12-alice (Identity Center user, Department=finance) bob / carol (Cognito user pool sec12-users)
│ member of sec12-analysts │ USER_PASSWORD_AUTH → ID token (JWT)
▼ ▼
permission sets sec12-ReadOnly, sec12-DeptS3 identity pool sec12_ids
│ assigned to the member account │ GetId → GetCredentialsForIdentity
▼ │ (STS AssumeRoleWithWebIdentity under the hood)
IAM roles AWSReservedSSO_sec12-* (SAML trust) ▼
│ session tag Department=finance (ABAC) sec12-cognito-user-role → s3://sec12-appdata-…/<identity-id>/*
▼ sec12-cognito-admin-role → whole bucket (carol, via group)
s3://sec12-docs-222222222222/finance/* only sec12-cognito-guest-role → public/* only (no login)
What happened in the lab run¶
Part A — IAM Identity Center + ABAC¶
Each permission set became an AWSReservedSSO_<name>_<random> role in the member account, trusting the
Identity Center SAML provider with sts:AssumeRoleWithSAML + sts:TagSession. The access control attribute
Department ← ${path:enterprise.department} arrives as a session tag, read by the policy as
${aws:PrincipalTag/Department}.
| Who / profile | Action | Result |
|---|---|---|
| alice (finance), DeptS3 | list / read finance/ |
✅ |
| alice (finance), DeptS3 | list / read hr/ |
❌ AccessDenied |
| alice, ReadOnly | list the bucket | ✅ |
| alice, ReadOnly | delete the bucket | ❌ read-only |
alice → department changed to hr, cached role creds still in place |
read hr/ |
❌ old session still tagged finance |
| alice, after clearing the CLI's cached role credentials | read finance/ / hr/ |
❌ / ✅ — same permission set, no policy edit |
Gotchas:
- The CLI shorthand syntax can't parse
${path:...}→ pass the attribute configuration as a JSON file. - Changing the department needed
AttributePath=aws:identitystore:enterprisewith an object value;enterprise.departmentas a path was rejected. - Signing in to the browser portal doesn't log in the CLI —
aws sso loginneeds its own device-code approval.
Part B — Cognito user pool (authentication)¶
| Test | Result |
|---|---|
InitiateAuth with USER_PASSWORD_AUTH, no AWS credentials (--no-sign-request) |
✅ ID, access and refresh JWTs |
| ID token claims | iss = user pool URL, aud = app client, token_use: id, sub = user-pool user ID |
| Hosted UI authorization-code flow: redeem the code | ✅ tokens |
| Redeem the same code again | ❌ invalid_grant — codes are single-use |
| PKCE: redeem without the verifier | ❌ invalid_code_verifier |
| PKCE: wrong verifier attempt | ❌ and it burned the code (correct verifier afterwards failed too) |
Part C — Identity pool (authorization → AWS credentials)¶
Enhanced flow: GetId → GetCredentialsForIdentity. The role trust checks aud = pool ID and
amr = authenticated:
"Condition": {
"StringEquals": { "cognito-identity.amazonaws.com:aud": "us-east-2:11111111-2222-3333-4444-555555555555" },
"ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" }
}
| Who | Action | Result |
|---|---|---|
| bob | write to a folder named after his user-pool sub |
❌ — the policy variable isn't that |
| bob | write/list <his identity ID>/ |
✅ ${cognito-identity.amazonaws.com:sub} = identity ID |
| bob | write someone-else/ |
❌ |
| bob | list the whole bucket | ❌ |
| bob | read public/ |
✅ |
| bob | call other services | ❌ |
| guest (no login) | read public/ |
✅ — and nothing else |
| anyone | GetCredentialsForIdentity with another user's identity ID, no token |
❌ NotAuthorizedException |
Part D — Group roles and the classic flow¶
| Test | Result |
|---|---|
carol in group admins (with a role), token-based role mapping |
✅ token has cognito:groups + cognito:preferred_role → admin role, lists/reads everything |
| bob (no group) | ✅ default authenticated role |
GetOpenIdToken while role mappings exist |
❌ "Basic (classic) flow is not supported with RoleMappings" |
Classic flow: bob's OpenID token → AssumeRoleWithWebIdentity on the user role |
✅ |
… on the admin role (bob is NOT in admins) |
⚠️ ✅ privilege escalation |
| … on the guest role | ❌ amr mismatch |
Admin role trust + cognito-identity.amazonaws.com:sub = carol's identity ID |
bob ❌ · carol ✅ |
| Classic flow turned off again | ✅ |
Key lessons¶
- Identity Center roles are managed objects. Permission set →
AWSReservedSSO_*role per account with a SAML trust; change the permission set, never the role. - ABAC tags are fixed at session start. Changing a user's attribute only applies at the next role session — old sessions keep the old tag until they expire.
- User pool authenticates, identity pool authorizes. The JWT says who you are; the identity pool swaps it
for STS credentials, and
${cognito-identity.amazonaws.com:sub}is the identity ID, not the JWTsub. - In the classic flow the client picks the role, and STS only checks the trust policy. Group membership is not a security boundary there — restrict the trust policy itself, or stay on the enhanced flow.
Exam notes¶
- Workforce, many accounts → IAM Identity Center. Identity sources: built-in directory, AD (Managed Microsoft AD / AD Connector), or an external IdP (Okta, Entra ID) via SAML 2.0 + SCIM.
- Permission set = role template: AWS managed policies, customer managed policy references (must exist by name in every target account), inline policy, permissions boundary. Session duration 1–12 h.
- Never edit
AWSReservedSSO_*roles — Identity Center re-provisions and overwrites them. - Delegated admin for Identity Center can't manage permission sets assigned to the management account.
- ABAC attributes: from the identity source (
${path:…}) or SAML assertion attributes → session tags →${aws:PrincipalTag/…}. - Old way without Identity Center: an IAM SAML provider +
sts:AssumeRoleWithSAMLrole trust in each account. - User pool = sign-up/sign-in, MFA, hosted UI, social/SAML/OIDC IdPs → ID, access, refresh JWTs. API Gateway / ALB validate user pool tokens directly — no identity pool needed.
- Hosted UI: authorization codes are single-use; use PKCE for public clients (mobile/SPA).
- Identity pool = token (or nothing, for guests) → temporary AWS credentials. Trust must check
aud= pool ID (else any identity pool could assume it) andamr(else guests get the authenticated role). - Enhanced flow (default):
GetId→GetCredentialsForIdentity; Cognito picks the role. Basic/classic flow:GetId→GetOpenIdToken→ app callsAssumeRoleWithWebIdentity; off by default, not allowed with role mappings. - Role mapping: Token (
cognito:preferred_role/ group roles) or Rules (claim → role); ambiguous → authenticated role or Deny. AssumeRoleWithWebIdentityis also what EKS IRSA and GitHub Actions OIDC use (IAM OIDC provider + trust on…:sub/…:aud).
Review questions¶
Question 1
300 employees sign in with Okta and need access to 40 AWS accounts with different rights per team. What do you build, and which two protocols connect Okta?
Show answer
IAM Identity Center (in the Organizations management account or a delegated admin) with Okta as the external identity source. SAML 2.0 handles sign-in (federation), SCIM automatically provisions users and groups from Okta. Create a permission set per access level and assign Okta groups to the right accounts; Identity Center creates the roles in every account. No IAM users, no per-account SAML setup.
Question 2
An admin edits the AWSReservedSSO_DataTeam_… role's policy in one account. Next week the change is gone. Why?
Show answer
That role is owned by Identity Center: it's provisioned from the DataTeam permission set and
re-provisioned whenever the permission set is updated or re-synced, overwriting manual edits. Make the
change in the permission set (and re-provision it); it then applies to every assigned account
consistently. If only one account needs different rights, use a separate permission set.
Question 3
A mobile app must let each user upload photos to their own S3 folder only. Which Cognito pieces, and which policy variable?
Show answer
A user pool to authenticate users and an identity pool that exchanges their ID token for temporary
AWS credentials of an authenticated role. The role policy allows s3:PutObject (and Get/List) only on
arn:aws:s3:::bucket/${cognito-identity.amazonaws.com:sub}/*. That variable is the identity pool
identity ID (us-east-2:…), not the user pool sub — so the folder must be named after the identity ID,
as the lab proved when writing to the user-pool-sub folder was denied.
Question 4
Your Cognito authenticated role's trust policy has only "Principal": {"Federated": "cognito-identity.amazonaws.com"}. What can an attacker do, and what two conditions fix it?
Show answer
Any identity pool — including one the attacker creates in their own account — can get tokens that
satisfy this trust, so they can assume your role (via the basic flow / AssumeRoleWithWebIdentity) and
use its permissions. Add StringEquals cognito-identity.amazonaws.com:aud = your identity pool ID,
and ForAnyValue:StringLike cognito-identity.amazonaws.com:amr = authenticated (otherwise
unauthenticated guests of your pool could get the authenticated role).
Question 5
An API only needs to know the caller is a signed-in app user. It never calls AWS services itself. Do you need an identity pool?
Show answer
No. An identity pool only exists to hand out AWS credentials. Use a user pool and put a Cognito user pool authorizer on API Gateway (or ALB's Cognito/OIDC authentication), which validates the JWT directly; the access token's scopes can drive authorization. Fewer moving parts, no IAM roles for users.
Question 6
Finance and HR use the same permission set, but each must only see its own S3 prefix. How, without two permission sets?
Show answer
ABAC with Identity Center. Configure an access control attribute, e.g. Department ←
${path:enterprise.department} (or a SAML attribute from the IdP). It becomes a session tag, and the
permission set's policy uses arn:aws:s3:::bucket/${aws:PrincipalTag/Department}/* (and an s3:prefix
condition for listing). Each user only reaches their own department's prefix; moving someone to another
department is an attribute change, effective at their next session.
Question 7
Enhanced vs basic flow: which one makes your app call STS itself, and when would you turn it on?
Show answer
The basic (classic) flow: GetId → GetOpenIdToken → the app calls
sts:AssumeRoleWithWebIdentity and chooses the role. It's off by default and can't be combined with role
mappings. Turn it on only when you need to control the STS call yourself (e.g. pick between many roles,
custom session policies or durations) — and then lock every role's trust policy down, because any
authenticated identity can request any role that trusts the pool (the lab's bob → admin escalation).
Otherwise use the enhanced flow, where Cognito picks the role.
Trap seen in the run
Group-based roles looked secure in the enhanced flow, but with the classic flow on, a user outside the
admins group assumed the admin role. STS never sees Cognito groups — only the trust policy.