Skip to content

Lab 12 — Federation

✅ Done 🧭 Identity and Access Management 💲 $0
Services: IAM Identity Center, Identity Store, Amazon Cognito (user pools, identity pools), STS, IAM, S3

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:enterprise with an object value; enterprise.department as a path was rejected.
  • Signing in to the browser portal doesn't log in the CLI — aws sso login needs 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

  1. Identity Center roles are managed objects. Permission set → AWSReservedSSO_* role per account with a SAML trust; change the permission set, never the role.
  2. 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.
  3. 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 JWT sub.
  4. 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:AssumeRoleWithSAML role 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) and amr (else guests get the authenticated role).
  • Enhanced flow (default): GetId → GetCredentialsForIdentity; Cognito picks the role. Basic/classic flow: GetId → GetOpenIdToken → app calls AssumeRoleWithWebIdentity; 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.
  • AssumeRoleWithWebIdentity is 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.