Advanced Identity (Cognito, STS, cross-account roles) — DVA-C02 Mastery Guide
- Sources: course chapters 20 (Cognito) and 22 (Advanced Identity). The course serves as the syllabus only; all wording here is original.
- Written 2026-10-12; values checked against AWS docs on 2026-10-12. The marker
⚠️ Changed: <current>. Older material may say <old>.flags a value or behaviour that differs from the course or older practice tests: learn the current value.⚠️ unverifiedmarks a value that could not be confirmed in AWS documentation: don't rely on it. Every change is listed in Changed vs older material; once you confirm them they also go tonotes/current-facts.md.- Tier 1 (highest exam weight), bank target 40 questions. Domain: Security (task statement "authentication and authorization for applications and AWS services").
- How to use it:
/explain <topic>teaches from these sections (concept → exam facts → developer angle → traps → 3 check questions);/quiz advanced-identityasks bank questions whosesource_notelinks point at the## N.headings below;/drill advanced-identityfires short recall prompts fromnotes/advanced-identity-cheat-sheet.md. Suggested loop: read a section and answer its two practice questions before opening the answers →/explainwhat is still unclear →/quiz→/drill.
How Advanced Identity (Cognito, STS, cross-account roles) appears on DVA-C02
Security is 26 % of the exam, and its first task statement is exactly this module. Questions rarely ask for definitions; they describe an application and ask which identity component, token, API call or policy statement makes it work securely:
- Who signs the user in and who hands out AWS credentials: user pool vs identity pool vs both; Cognito vs IAM users vs Directory Service.
- Tokens: which JWT goes to which consumer, how a backend proves a token is genuine, how sessions refresh and end.
- Customising Cognito: the Lambda trigger that auto-confirms, migrates, enriches tokens or blocks a sign-in; managed login vs your own screens; which OAuth grant.
- Delegation with STS: cross-account roles, external IDs, session-duration errors, MFA-protected calls.
- Reading policies: explicit deny, identity vs resource policies in one account vs across accounts, policy variables,
iam:PassRolefailures.
Deciding constraints you will meet: no long-term credentials in the app, least privilege, least custom code / operational overhead (managed login, ALB authentication, triggers instead of hand-written auth), a third party must not misuse the role (external ID), guests must stay limited (unauthenticated role).
Domain map: every topic is Security; §2–§4 and §8 also appear as Development with AWS Services questions (SDK calls, triggers), §9 and §11–§13 as Troubleshooting (AccessDenied diagnosis).
Out of scope: Active Directory administration (forests, group policy design, domain controllers), VPN or Direct Connect design for directories, and SAML/OIDC protocol internals beyond what a developer configures.
Owned by other modules (only cross-referenced here): IAM users, groups, policy grammar, MFA devices and access keys → notes/iam-guide.md · CLI profiles (role_arn, mfa_serial, source_profile), credential chain, sts decode-authorization-message, SigV4 → notes/cli-sdk-policies-guide.md · instance profiles, IMDS, ALB listeners and target groups → notes/ec2-mastery-guide.md §1, §2, §8 · API Gateway Cognito, IAM and Lambda authorizers → notes/api-gateway-guide.md · KMS key policies → notes/security-encryption-guide.md · bucket policies → notes/s3-guide.md · dynamodb:LeadingKeys → notes/dynamodb-guide.md · ECS task vs execution role → notes/containers-guide.md · Lambda execution role → notes/lambda-guide.md · AppSync and Amplify Auth → notes/other-services-guide.md · CloudTrail → notes/monitoring-guide.md.
| # | Topic | Why it is on the exam |
|---|---|---|
| 1 | Cognito user pools vs identity pools | The classic Cognito question: authenticate users (JWTs), authorize them to AWS (credentials), or both |
| 2 | User pool sign up sign in and MFA | App clients, auth flows, SECRET_HASH, confirmation states, MFA factors, the exceptions you must recognise |
| 3 | User pool tokens and JWT validation | ID vs access vs refresh token, lifetimes, verifying a JWT, revocation and rotation |
| 4 | User pool Lambda triggers | The trigger that auto-confirms, migrates, customises tokens or messages; 5-second limit; invoke permission |
| 5 | Managed login domains and OAuth flows | Hosted pages, custom domain with a us-east-1 certificate, code + PKCE vs implicit vs client credentials |
| 6 | Feature plans and threat protection | Lite / Essentials / Plus; adaptive authentication and compromised-credential checks need Plus |
| 7 | ALB authentication with Cognito and OIDC | Sign-in at the load balancer with no app code: HTTPS listener, callback URL, claims headers |
| 8 | Identity pools and temporary AWS credentials | Guest vs authenticated roles, trust-policy conditions, role selection, enhanced vs basic flow |
| 9 | STS AssumeRole and cross account access | Trust policy + identity policy, external ID, session-duration limits, the STS API set |
| 10 | STS with MFA | GetSessionToken, MFA parameters on AssumeRole, aws:MultiFactorAuthPresent pitfalls |
| 11 | Policy evaluation logic | Explicit deny wins; identity vs resource policies; cross-account needs both sides; guardrails never grant |
| 12 | Dynamic policies and policy variables | One policy for many principals: ${aws:username}, principal tags, identity-pool sub |
| 13 | Managed vs inline policies and passing roles | Policy types, versions and quotas; scoping iam:PassRole; one role per workload |
| 14 | AWS Directory Service | Managed Microsoft AD vs AD Connector vs Simple AD, and when the answer is Cognito instead |
1. Cognito user pools vs identity pools
Concept
- Amazon Cognito is two separate services under one name. A user pool (API prefix
cognito-idp) is a managed user directory and OpenID Connect provider: it registers and signs in your application's end users, either with credentials it stores itself (local users) or by federating Google, Facebook, Sign in with Apple, Login with Amazon, SAML 2.0 or OIDC identity providers. It proves the result with tokens: an ID token and an access token, both JSON Web Tokens (JWTs), plus an encrypted refresh token that only the user pool can read (§3). - An identity pool (API prefix
cognito-identity, once called "federated identities") is an exchange counter: it takes proof of sign-in from a user pool or another identity provider, or no proof at all for guests, and gives back short-lived AWS credentials for an IAM role, obtained from STS. - Mental split: the user pool answers "who is this person?" (authentication); the identity pool answers "which AWS permissions does this caller get?" (authorization to AWS APIs). Many apps chain them: sign in at the user pool → hand the ID token to the identity pool → call S3 or DynamoDB directly with the returned credentials.
- Either can stand alone. A user pool alone is enough when your own API layer (API Gateway, AppSync, an ALB, your code) checks the JWT. An identity pool alone is enough when users sign in with a social or SAML provider and only need AWS credentials, or when anonymous guests need narrow AWS access.
- IAM users are for the people and tools that build and run your AWS account. Application customers (thousands to millions, mobile and web, social logins) belong in Cognito, never in IAM.
Exam facts
- What comes out: user pool → ID and access tokens (JWTs) plus an opaque, encrypted refresh token (§3); identity pool →
AccessKeyId,SecretKey,SessionToken,Expiration(enhanced flow: valid for 1 hour, §8). - Guests: only identity pools have unauthenticated identities (
AllowUnauthenticatedIdentities); every user pool user signs in. - Token consumers: API Gateway REST
COGNITO_USER_POOLSauthorizer or HTTP API JWT authorizer (notes/api-gateway-guide.md), AppSync, ALBauthenticate-cognito(§7), your own code (§3). Credential consumers: any AWS API that uses IAM and SigV4, including API Gateway methods withAWS_IAMauthorization (execute-api:Invoke). - User pool groups can carry an IAM role ARN and a precedence value; an identity pool can pick that role from the token (
cognito:preferred_role, §8). Feature plans (Lite, Essentials, Plus) apply to user pools only (§6). - Quotas: 1,000 user pools per Region (adjustable up to 10,000); 1,000 app clients per user pool (adjustable up to 10,000); 40,000,000 users per user pool (adjustable); 1,000 identity pools per account (adjustable); identities per identity pool: unlimited.
- Signal words: "sign-up, password reset, social or SAML login, hosted login page, JWT, user directory" → user pool. "Temporary AWS credentials, call S3 or DynamoDB straight from the device, guest access, IAM role per type of user" → identity pool. Both sets → both pools.
- ⚠️ Changed: Amazon Cognito Sync is no longer open to new customers (existing users keep it, with no new features); use AWS AppSync for real-time cross-device sync or DynamoDB for per-user data. Older material may say "use Cognito Sync to synchronise user preferences across devices".
- Workforce sign-in to AWS accounts is IAM Identity Center, not Cognito; Microsoft AD integration is Directory Service (§14). AWS's own directory guidance points SaaS and consumer subscriber directories to Cognito.
Developer angle
Sign in at the user pool, exchange the ID token at the identity pool, call S3 with the result (boto3; assumes no MFA challenge):
import boto3
REGION, POOL_ID = "eu-west-1", "eu-west-1_Example1"
IDENTITY_POOL_ID = "eu-west-1:11111111-2222-3333-4444-555555555555"
auth = boto3.client("cognito-idp", region_name=REGION).initiate_auth(
ClientId="1example23456789", # public app client: no secret
AuthFlow="USER_PASSWORD_AUTH", # needs ALLOW_USER_PASSWORD_AUTH on the client
AuthParameters={"USERNAME": "maria@example.com", "PASSWORD": "Corr3ct-Horse-Battery"})
logins = {f"cognito-idp.{REGION}.amazonaws.com/{POOL_ID}": auth["AuthenticationResult"]["IdToken"]}
cid = boto3.client("cognito-identity", region_name=REGION)
identity_id = cid.get_id(IdentityPoolId=IDENTITY_POOL_ID, Logins=logins)["IdentityId"]
creds = cid.get_credentials_for_identity(IdentityId=identity_id, Logins=logins)["Credentials"]
s3 = boto3.client("s3", region_name=REGION, aws_access_key_id=creds["AccessKeyId"],
aws_secret_access_key=creds["SecretKey"], # identity pools call it SecretKey
aws_session_token=creds["SessionToken"])
s3.put_object(Bucket="photo-uploads", Key=f"{identity_id}/cat.jpg", Body=b"jpeg-bytes")
- Two CLIs and two IAM prefixes:
aws cognito-idp …/cognito-idp:*for user pools,aws cognito-identity …/cognito-identity:*for identity pools. - The
Loginsmap key for a user pool iscognito-idp.<region>.amazonaws.com/<user-pool-id>and its value is the ID token. In browsers and mobile apps let the SDK do the exchange and the refresh: AWS SDK for JavaScript v3fromCognitoIdentityPool(§8) or Amplify Auth. - Prefer SRP (
USER_SRP_AUTH) in real client apps (the SDKs and Amplify implement it); the password flow above keeps the example short.
Traps
- Trap: "Create an IAM user per customer and ship its keys in the app." → Reality: user pool for identities, identity pool (or your own API) for access; never long-term keys in clients.
- Trap: "The user pool hands out AWS credentials." → Reality: it issues tokens only (ID and access JWTs plus a refresh token); AWS credentials come from an identity pool or from your backend's own role.
- Trap: "The identity pool stores users and passwords." → Reality: it stores identity IDs and role mappings; authentication happens at the user pool or the external IdP.
- Trap: "Anonymous visitors need a user pool account." → Reality: identity pool unauthenticated (guest) role.
- Trap: "Use Cognito Sync for the new app's settings sync." → Reality: closed to new customers; AppSync or DynamoDB.
- Trap: "Cognito gives employees console access to AWS accounts." → Reality: that is IAM Identity Center (with Directory Service or an external IdP as identity source).
Practice questions
- A startup builds a mobile photo app. Users must sign up with email and password or with Google, and the app must upload each user's photos directly to an S3 prefix that only that user can access, without running a backend. Which solution meets these requirements?
- A. Create a Cognito user pool with Google federation and a Lambda function behind an API Gateway Cognito authorizer that returns S3 presigned URLs.
- B. Create a Cognito user pool with Google federation, an identity pool that trusts it, and an authenticated role whose S3 resource uses
${cognito-identity.amazonaws.com:sub}. - C. Create an IAM user for each customer at sign-up and store its access keys in the device keychain.
- D. Create an identity pool with developer-authenticated identities and one unauthenticated role that allows
s3:*on the bucket.
- A news website shows article metadata from a DynamoDB table to anonymous visitors and lets signed-in readers post comments to a second table. The browser code must call DynamoDB directly with temporary credentials. Which TWO steps are required? (Select TWO.)
- A. Enable unauthenticated identities in an identity pool and give its guest role read-only access to the metadata table.
- B. Add the user pool as an authentication provider of the identity pool and give the authenticated role write access to the comments table.
- C. Create one shared user pool account for anonymous visitors and embed its password in the website.
- D. Create an API key in an API Gateway usage plan and embed it in the website to authorize the DynamoDB writes.
- E. Embed an IAM user's access key in the website with a policy restricted to the two tables.
Answers
- B. The user pool signs users in (local or Google), the identity pool turns the ID token into role credentials, and the identity-ID variable confines each user to their own prefix. A works but runs a backend (API Gateway and Lambda), which the requirement rules out; C puts long-term keys on devices; D gives every guest full bucket access.
- A, B. Guests get the unauthenticated role, signed-in users the authenticated role. C publishes a credential and still yields tokens, not AWS credentials; D: API keys identify usage-plan clients and authorize nothing in DynamoDB; E ships a long-term key to every browser.
2. User pool sign up sign in and MFA
Concept
- An app client is the per-application configuration inside a user pool: which sign-in flows it accepts, its OAuth settings (§5), token lifetimes (§3), which user attributes it may read and write, and optionally a client secret. Browser and mobile apps are public clients (no secret); server-side apps can be confidential clients (with a secret).
- Sign-up lifecycle:
SignUpcreates the user as UNCONFIRMED; the user proves the email or phone with a code (ConfirmSignUp), or an administrator confirms (AdminConfirmSignUp), or a pre sign-up trigger auto-confirms (§4) → CONFIRMED. Users created by an administrator (AdminCreateUser) start in FORCE_CHANGE_PASSWORD and meet aNEW_PASSWORD_REQUIREDchallenge at first sign-in; imported users start in RESET_REQUIRED. - Sign-in is a challenge loop:
InitiateAuth(orAdminInitiateAuth) returns tokens or a challenge plus aSessionvalue; the app answers withRespondToAuthChallengeuntil tokens arrive. Challenges includePASSWORD_VERIFIER(SRP),SMS_MFA,SOFTWARE_TOKEN_MFA,EMAIL_OTP,SELECT_MFA_TYPE,MFA_SETUP,NEW_PASSWORD_REQUIRED,CUSTOM_CHALLENGEandWEB_AUTHN. - The user pools API mixes three authorization models. Public operations need only the app client ID (plus
SECRET_HASHfor a client with a secret):SignUp,ConfirmSignUp,ResendConfirmationCode,ForgotPassword,ConfirmForgotPassword,InitiateAuth. Token-authorized operations take the user's own access token or session:GetUser,UpdateUserAttributes,ChangePassword,GlobalSignOut,RespondToAuthChallenge. IAM-authorized operations need SigV4 and IAM permissions: everyAdmin*call and all pool configuration calls.
Exam facts
- Auth flows (
ExplicitAuthFlows):ALLOW_USER_SRP_AUTH(the password never leaves the client; AWS's best practice for custom-built apps),ALLOW_USER_PASSWORD_AUTH(password sent in the request over TLS; one of the flows that can invoke the migrate user trigger, §4),ALLOW_ADMIN_USER_PASSWORD_AUTH(server side with IAM credentials; replacedADMIN_NO_SRP_AUTH),ALLOW_CUSTOM_AUTH,ALLOW_REFRESH_TOKEN_AUTH,ALLOW_USER_AUTH(choice-based sign-in covering password, SRP, email/SMS one-time codes and passkeys; Essentials or Plus). Not specified →ALLOW_REFRESH_TOKEN_AUTH,ALLOW_USER_SRP_AUTH,ALLOW_CUSTOM_AUTH. SECRET_HASH= Base64(HMAC-SHA256(key = client secret, message = username + client ID)). A client with a secret needs it in every public and admin sign-in call; when it is missing or wrong the call fails with "Unable to verify secret hash for client <client-id>".- Whether an app client has a secret is fixed at creation: a public client can't gain one and a confidential client can't drop it (create a new app client). A confidential client can hold up to 2 secrets at once for rotation (
AddUserPoolClientSecret,DeleteUserPoolClientSecret; generated or custom values; the last secret can't be deleted). - MFA setting (
MfaConfiguration): Off / Optional / Required. Second factors: SMS (Amazon SNS through an IAM role; an account new to SMS may start in the SNS SMS sandbox, which only reaches verified numbers), TOTP authenticator apps (AssociateSoftwareToken→VerifySoftwareToken→SetUserMFAPreference), email one-time codes (Essentials or Plus, with your own Amazon SES configuration). Passkeys and passwordless one-time codes are Essentials/Plus first factors. - Federated users get MFA from their IdP, never from Cognito. MFA can't be set to Required while passwordless one-time codes are a sign-in option. Adaptive authentication needs MFA set to Optional (§6).
- Fixed at creation: sign-in identifiers (username, or email/phone as username, or aliases) and required attributes. Custom attributes: up to 50,
custom:prefix, can't be removed or renamed, mutable or immutable.subis the fixed unique user ID: key your database on it, not on the email. PreventUserExistenceErrors:ENABLEDreturns a generic incorrect-username-or-password error and pretends codes were sent;LEGACY(the API default) returnsUserNotFoundException, which lets attackers enumerate users.- Exceptions to recognise:
UserNotConfirmedException(sign-in before confirmation),PasswordResetRequiredException(RESET_REQUIRED users),NotAuthorizedException(wrong password, or writing an attribute the app client may not write),UsernameExistsException,AliasExistsException,CodeMismatchException,ExpiredCodeException,InvalidPasswordException,UserLambdaValidationException(a trigger rejected the request),TooManyRequestsException(rate quota),ForbiddenException(an AWS WAF web ACL blocked it),FeatureUnavailableInTierException(feature not in your plan). - Code and session lifetimes: sign-up confirmation code 24 h; forgot-password code 1 h; MFA code 3–15 min; authentication session (
AuthSessionValidity) 3–15 min (default ⚠️ unverified; AWS examples show 3). Rate quotas per account and Region:UserAuthentication120 RPS (adjustable; sign-in and token refresh),UserCreation50 RPS (adjustable),UserFederation25 RPS (adjustable),UserAccountRecovery30 RPS (not adjustable). The built-in Cognito email sender allows 50 emails per day per account → configure Amazon SES for production. UpdateUserPoolandUpdateUserPoolClientreset every parameter you omit to its default. Read the current configuration (DescribeUserPool,DescribeUserPoolClient) and send it back with your change, or manage the pool with CloudFormation or CDK.- Removing
COGNITOfrom an app client's identity providers only hides local sign-in in managed login; SDK sign-in still works. Block it with an AWS WAF web ACL associated with the user pool.
Developer angle
Public calls need no AWS credentials; admin calls need IAM permissions:
# Public operations: only the app client ID, no AWS credentials
aws cognito-idp sign-up --client-id 1example23456789 --username maria@example.com \
--password 'Corr3ct-Horse-Battery' --user-attributes Name=email,Value=maria@example.com
aws cognito-idp confirm-sign-up --client-id 1example23456789 --username maria@example.com --confirmation-code 123456
# IAM-authorized: the caller needs cognito-idp:AdminConfirmSignUp on the pool
aws cognito-idp admin-confirm-sign-up --user-pool-id eu-west-1_Example1 --username maria@example.com
# Sign in, then answer the MFA challenge with the Session value from the first response
aws cognito-idp initiate-auth --client-id 1example23456789 --auth-flow USER_PASSWORD_AUTH \
--auth-parameters USERNAME=maria@example.com,PASSWORD='Corr3ct-Horse-Battery'
aws cognito-idp respond-to-auth-challenge --client-id 1example23456789 --challenge-name SOFTWARE_TOKEN_MFA \
--session "$SESSION" --challenge-responses USERNAME=maria@example.com,SOFTWARE_TOKEN_MFA_CODE=654321
Computing SECRET_HASH in a confidential server-side client:
import base64, hashlib, hmac
def secret_hash(username: str, client_id: str, client_secret: str) -> str:
"""Base64(HMAC-SHA256(key=client secret, message=username + client ID))."""
digest = hmac.new(client_secret.encode(), (username + client_id).encode(), hashlib.sha256).digest()
return base64.b64encode(digest).decode()
- A backend using the admin flow needs only
cognito-idp:AdminInitiateAuthandcognito-idp:AdminRespondToAuthChallengeonarn:aws:cognito-idp:<region>:<account>:userpool/<user-pool-id>. - IAM policies can't restrict public operations such as
SignUporInitiateAuth: there is no IAM evaluation for them. Protect them with WAF, a pre sign-up trigger, or by keeping the app client ID within your own apps.
Traps
- Trap: "Add a client secret to the existing app client." → Reality: not possible; create a new app client (other clients and the pool stay untouched).
- Trap: "Put the client secret in the mobile app." → Reality: distributed apps are public clients without a secret; a secret in an app is not a secret.
- Trap: "Users can sign in right after
SignUp." → Reality: they stay UNCONFIRMED until a code, an admin or a pre sign-up trigger confirms them (UserNotConfirmedException). - Trap: "Use an IAM policy to stop strangers calling
SignUp." → Reality: public operations ignore IAM; use AWS WAF or a pre sign-up trigger. - Trap: "Switch the pool to email-as-username later." → Reality: sign-in identifiers and required attributes are fixed at creation; plan them or migrate to a new pool.
- Trap: "An
UpdateUserPoolClientcall with one new setting only changes that setting." → Reality: omitted parameters fall back to defaults.
Practice questions
- A developer runs a Node.js service on Amazon ECS that signs users in with
AdminInitiateAuthagainst an app client that has a client secret. The task role allowscognito-idp:AdminInitiateAuthon the user pool. Every request fails with the messageUnable to verify secret hash for client. What should the developer change?- A. Add
cognito-idp:AdminRespondToAuthChallengeto the task role. - B. Switch the request to
USER_SRP_AUTHso that the password isn't sent. - C. Confirm the users with
AdminConfirmSignUpbefore they sign in. - D. Send
SECRET_HASH, computed from the username and client ID with the client secret, inAuthParameters.
- A. Add
- A fintech company builds its own sign-in screens on a Cognito user pool. Every local user must enter an authenticator-app code at each sign-in. Which TWO actions are required? (Select TWO.)
- A. Add a custom message Lambda trigger that emails a TOTP code to the user at each sign-in.
- B. Set the user pool MFA configuration to required with software-token (TOTP) MFA enabled.
- C. Create an IAM role that allows Amazon Cognito to publish SMS messages through Amazon SNS.
- D. During onboarding, call
AssociateSoftwareTokenand thenVerifySoftwareTokenwith the user's first code. - E. Enable email MFA on the Essentials plan with an Amazon SES configuration.
Answers
- D. A client with a secret must prove knowledge of it in every sign-in call. A would surface as an IAM
AccessDenied, not a hash error; B changes the flow but still needs the hash; C addressesUserNotConfirmedException, a different error. - B, D. Required MFA with TOTP enabled forces the second factor, and the authenticator must be registered and verified once. A: TOTP codes come from the user's app, not from Cognito messages; C enables SMS, not TOTP; E adds email codes, not an authenticator app.
3. User pool tokens and JWT validation
Concept
- A successful sign-in returns three tokens. The ID token is a JWT about the person:
sub, attributes such asemail,cognito:groups, andaud= the app client ID. The access token is a JWT about permissions:scope,client_id,username,cognito:groups; it authorizes calls to your APIs and the user's self-service user pool calls. The refresh token is opaque and encrypted; it buys new ID and access tokens. - A JWT is three base64url parts: header (
kid,alg=RS256), payload (claims) and signature. Anyone can decode the first two parts; only checking the signature against the user pool's public keys proves the token came from your pool and wasn't edited. - Validation recipe: find the key by
kidin the pool's JWKS, verify the signature, then checkexp,iss(=https://cognito-idp.<region>.amazonaws.com/<user-pool-id>), the audience (audin ID tokens,client_idin access tokens) andtoken_use(idoraccess). The checks run offline, with no call to Cognito. - Managed integrations do this for you: API Gateway Cognito authorizers and HTTP API JWT authorizers, AppSync, the ALB (§7). In your own code AWS recommends the
aws-jwt-verifylibrary (Node.js) or any reputable JWT library.
Exam facts
- Lifetimes per app client: ID and access tokens default 1 h (range 5 minutes to 1 day); refresh token default 30 days (range 1 hour to 3,650 days). Settings:
IdTokenValidity,AccessTokenValidity,RefreshTokenValidity,TokenValidityUnits. Refreshing:InitiateAuth/AdminInitiateAuthwithREFRESH_TOKEN_AUTH, theGetTokensFromRefreshTokenoperation, or the token endpoint withgrant_type=refresh_token. - ⚠️ Changed: optional refresh token rotation per app client (
RefreshTokenRotation, grace period up to 60 seconds for the old token) returns a new refresh token on every refresh; it works only withGetTokensFromRefreshTokenor the token endpoint (REFRESH_TOKEN_AUTHmust be off), and each new refresh token keeps the original expiry window. Older material may say refresh tokens are never rotated. - Ending sessions:
RevokeToken(one refresh token plus the access and ID tokens issued from it; token revocation is on by default for new app clients and addsjti/origin_jticlaims);GlobalSignOut(with the user's own access token, all their sessions);AdminUserGlobalSignOut(IAM-authorized, any user). Managed login keeps a 1-hour session cookie that only the/logoutendpoint clears. Offline validators can't see revocation: a revoked but unexpired access token still passes signature andexpchecks. Keep access tokens short-lived or confirm sensitive calls with Cognito. - Which token for what: identity claims → ID token; OAuth scopes → access token. A REST API Cognito authorizer accepts either in the
Authorizationheader, but methods with authorization scopes need the access token. Identity pools take the user pool ID token. - Access tokens from API sign-in carry only the
aws.cognito.signin.user.adminscope; custom scopes come from OAuth flows with resource servers (§5) or from a pre token generation trigger (§4). Access tokens haveclient_idand normally noaud(when bound to a resource server,audis that resource identifier). - JWKS URL:
https://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/jwks.json. The pool has separate key pairs for ID and access tokens; cache keys bykidand refetch when an unknownkidarrives (keys can rotate). - Reuse tokens for about 75 % of their lifetime instead of signing in again; for M2M tokens put a cache in front of the token endpoint, because the
ClientAuthenticationquota (150 RPS) isn't adjustable. - Newer, niche: AWS now recommends an "updated issuer"
https://issuer-cognito-idp.<region>.amazonaws.com/<user-pool-id>(JWKS hosted in several Regions; needed for multi-Region replication), but it doesn't yet work with ALB Cognito authentication or REST API Cognito authorizers. The original issuer above is still the default.
Developer angle
Verifying access tokens in a Lambda function behind your own API (Node.js, aws-jwt-verify):
import { CognitoJwtVerifier } from "aws-jwt-verify";
const verifier = CognitoJwtVerifier.create({
userPoolId: "eu-west-1_Example1",
tokenUse: "access", // "id" for ID tokens
clientId: "1example23456789", // checked against client_id (access) or aud (id)
scope: "orders/read", // optional: required OAuth scope
});
export const handler = async (event) => {
const token = (event.headers?.authorization ?? "").replace(/^Bearer /, "");
try {
const claims = await verifier.verify(token); // signature, exp, iss, token_use, client
return { statusCode: 200, body: JSON.stringify({ user: claims.sub }) };
} catch {
return { statusCode: 401, body: "invalid token" };
}
};
Token lifetimes, refresh and revocation from the CLI:
# Shorter tokens for a sensitive app (omitted client settings fall back to defaults!)
aws cognito-idp update-user-pool-client --user-pool-id eu-west-1_Example1 --client-id 1example23456789 \
--access-token-validity 15 --id-token-validity 15 --refresh-token-validity 7 \
--token-validity-units AccessToken=minutes,IdToken=minutes,RefreshToken=days \
--explicit-auth-flows ALLOW_USER_SRP_AUTH ALLOW_REFRESH_TOKEN_AUTH
aws cognito-idp get-tokens-from-refresh-token --client-id 1example23456789 \
--refresh-token "$REFRESH_TOKEN" # also returns a new refresh token if rotation is on
aws cognito-idp revoke-token --client-id 1example23456789 --token "$REFRESH_TOKEN"
aws cognito-idp admin-user-global-sign-out --user-pool-id eu-west-1_Example1 --username maria@example.com
- Identify the user by
sub(immutable), never by email or username, which can change or be reassigned. A token whoseissnames another pool or Region fails verification: check the pool ID the verifier was created with.
Traps
- Trap: "Decode the JWT and trust its claims." → Reality: decoding proves nothing; verify the signature with the pool's JWKS.
- Trap: "Call
GetUseron every request to check expiry." → Reality: readexplocally; network calls add latency and consume quota. - Trap: "Scopes are in the ID token." → Reality: scopes live in the access token; the ID token describes the user.
- Trap: "Check
audon access tokens." → Reality: access tokens carryclient_id(anaudappears only when bound to a resource server). - Trap: "Set the ID token to last 7 days." → Reality: maximum 1 day; long sessions come from the refresh token.
- Trap: "Rotation extends the session every time the token rotates." → Reality: the window is fixed at first sign-in.
- Trap: "After
GlobalSignOut, every API rejects the old access token at once." → Reality: offline validators keep accepting it untilexp.
Practice questions
- A team runs a containerized Python microservice that receives Cognito user pool access tokens in the
Authorizationheader of every request. It must reject forged or expired tokens while adding the least latency per request. What should the service do?- A. Call the user pool
GetUseroperation with each token and treat any error as an invalid token. - B. Base64-decode the token payload and accept the token when its
expclaim is in the future. - C. Verify the RS256 signature with the user pool's cached JWKS keys, then check
exp,iss,client_idandtoken_use. - D. Call
AdminGetUserwith the token'susernameclaim and compare the stored attributes with the token claims.
- A. Call the user pool
- A banking app's security team requires that when a customer reports a stolen phone, the thief can no longer renew the session and already-issued access tokens stop working soon, without signing out any other customer. The APIs validate tokens offline. Which TWO actions meet this requirement? (Select TWO.)
- A. Call
AdminUserGlobalSignOutfor the affected user from the support backend. - B. Increase the refresh token validity so that the customer doesn't have to sign in again.
- C. Delete and recreate the app client used by the mobile app.
- D. Call
RevokeTokenwith the stolen access token. - E. Reduce the app client's access token validity to a short value such as 15 minutes.
- A. Call
Answers
- C. Local signature and claim checks are both secure and fast. A works but adds a network call per request and burns quota; B accepts forged tokens; D adds an IAM-authorized call per request and doesn't check the signature.
- A, E. Global sign-out kills the refresh tokens, and short access tokens limit how long an offline validator keeps accepting the stolen one. B lengthens exposure; C signs out every customer; D:
RevokeTokentakes a refresh token, not an access token.
4. User pool Lambda triggers
Concept
- Triggers insert your code at fixed points of the user pool's flows. Cognito sends an event (
version,triggerSource,region,userPoolId,userName,callerContext.clientId,request,response); your function returns the same event withresponsefilled in. Returning it unchanged means "carry on with the default behaviour"; throwing an error stops the operation and shows your message to the caller. - Families: sign-up (pre sign-up, post confirmation, migrate user), authentication (pre authentication, post authentication, pre token generation), custom challenges (define, create and verify auth challenge), messages (custom message, custom email sender, custom SMS sender) and federation (inbound federation, newer: reshapes IdP attributes before the user is created or updated). Every trigger except the custom senders runs synchronously, so keep functions fast, idempotent and free of error text you don't want users to see.
Exam facts
| Trigger | Fires | Typical use | Key response fields |
|---|---|---|---|
| Pre sign-up | SignUp, AdminCreateUser, first federated sign-in (PreSignUp_ExternalProvider) |
Validate or reject; auto-confirm trusted users | autoConfirmUser, autoVerifyEmail, autoVerifyPhone |
| Post confirmation | After ConfirmSignUp, AdminConfirmSignUp, ConfirmForgotPassword, a federated user's first sign-in |
Welcome flow, create the profile row in DynamoDB | — |
| Pre authentication | Before Cognito checks the credentials | Block a tenant, a device or a user | throw an error |
| Post authentication | After a successful sign-in | Audit and analytics | — |
| Pre token generation | Before tokens are issued, also on refresh | Add, change or suppress claims; override groups and roles | claimsOverrideDetails (V1) / claimsAndScopeOverrideDetails (V2, V3) |
| Migrate user | User not in the pool at password sign-in or ForgotPassword |
Just-in-time migration from a legacy store | userAttributes, finalUserStatus, messageAction |
| Custom message | Before codes, invitations or MFA messages go out | Branded, localised text | smsMessage, emailMessage, emailSubject (must contain the code placeholder) |
| Define / Create / Verify auth challenge | CUSTOM_AUTH flow |
CAPTCHA, magic link, extra question | challengeName, issueTokens, failAuthentication / challenge parameters / answerCorrect |
| Custom email / SMS sender | Every message delivery | Third-party email or SMS provider | your function sends it |
- 5-second response limit, not adjustable: if the function doesn't answer in time and the call can be retried, Cognito may retry it, and the trigger times out once every attempt has failed (AWS doesn't say which calls are retryable or how many attempts it makes). Cognito never retries a Lambda
Invokeerror with HTTP status 500–599 (a configuration problem that keeps Lambda from launching the function). A rejection reaches the caller asUserLambdaValidationException"<trigger> failed with error <your message>"; a malformed response givesInvalidLambdaResponseException. - Invoke permission: adding a trigger in the Cognito console also writes the Lambda resource-based policy for
cognito-idp.amazonaws.com(scoped withaws:SourceArn= the user pool ARN andaws:SourceAccount). With the CLI, CloudFormation, CDK or a function in another account you add it yourself withlambda:AddPermission; without it the trigger fails. The console only offers functions in the pool's Region; cross-account functions and alias ARNs (LambdaArn= alias ARN) are set through the API (CreateUserPool/UpdateUserPool). - Pre token generation event versions:
V1_0(default) customises the ID token and overrides groups;V2_0also customises access tokens (claims and scopes) for users;V3_0adds M2M access tokens (client credentials andGetClientToken). V2 and V3 need Essentials or Plus; Lite pools that had advanced security features before 22 November 2024 keep V2. ⚠️ Changed: access tokens can be customised with V2/V3 events. Older material may say this trigger only adds or removes ID-token claims (the course does). - Claims you can't add or change include
sub,iss,exp,iat,token_use,client_id,cognito:usernameand every othercognito:claim exceptcognito:groups,cognito:preferred_roleandcognito:roles(overridable). Existing plus added claims and scopes per transaction: 5,000 (adjustable quota). - Migrate user isn't invoked for SRP or passwordless sign-in: during migration the app client must allow
USER_PASSWORD_AUTH,ADMIN_USER_PASSWORD_AUTHorUSER_AUTHwith a password.finalUserStatusCONFIRMEDkeeps the old password; anything else ends asRESET_REQUIRED;messageActionSUPPRESSskips the welcome message. - Federated sign-ins never invoke custom auth challenges, migrate user, custom message or custom sender triggers. Managed login doesn't support custom auth challenges at all.
- Custom senders are configured only through the API or CLI (
LambdaConfigwithKMSKeyID). Cognito encrypts codes and temporary passwords with your symmetric KMS key; the function decrypts them with the AWS Encryption SDK and needskms:Decrypt.
Developer angle
Pre sign-up that admits only company addresses and confirms them automatically (Python):
ALLOWED_DOMAIN = "@example.com"
def lambda_handler(event, context):
if event["triggerSource"] == "PreSignUp_SignUp":
if not event["request"]["userAttributes"].get("email", "").endswith(ALLOWED_DOMAIN):
raise Exception("Sign-up is limited to company email addresses")
event["response"].update(autoConfirmUser=True, autoVerifyEmail=True)
return event
Pre token generation (event version V2_0) that adds a tenant claim to both tokens and a custom scope to the access token:
def lambda_handler(event, context):
tenant = event["request"]["userAttributes"].get("custom:tenant_id", "none")
event["response"]["claimsAndScopeOverrideDetails"] = {
"idTokenGeneration": {"claimsToAddOrOverride": {"tenant_id": tenant}},
"accessTokenGeneration": {"claimsToAddOrOverride": {"tenant_id": tenant},
"scopesToAdd": ["orders/read"]},
}
return event
Wiring the functions outside the console:
aws lambda add-permission --function-name tenant-claims --statement-id cognito-pretoken \
--action lambda:InvokeFunction --principal cognito-idp.amazonaws.com --source-account 111122223333 \
--source-arn arn:aws:cognito-idp:eu-west-1:111122223333:userpool/eu-west-1_Example1
# UpdateUserPool resets omitted settings: list every trigger (and setting) you want to keep
aws cognito-idp update-user-pool --user-pool-id eu-west-1_Example1 \
--lambda-config 'PreSignUp=arn:aws:lambda:eu-west-1:111122223333:function:signup-guard,PreTokenGenerationConfig={LambdaArn=arn:aws:lambda:eu-west-1:111122223333:function:tenant-claims,LambdaVersion=V2_0}'
- Use the
triggerSourcevalue to branch inside one function (PreSignUp_SignUpvsPreSignUp_AdminCreateUservsPreSignUp_ExternalProvider;TokenGeneration_RefreshTokenson refresh). Pass extra context withClientMetadata; Cognito neither stores, validates nor encrypts it, so never put secrets in it.
Traps
- Trap: "Auto-confirm users in the post confirmation trigger." → Reality: confirmation happens in the pre sign-up response (
autoConfirmUser). - Trap: "Add claims from the post authentication trigger." → Reality: only pre token generation changes token contents.
- Trap: "Custom attributes appear in the access token." → Reality: attributes go to the ID token; access-token claims need a V2/V3 pre token generation trigger.
- Trap: "The trigger can call a slow legacy API for 20 seconds." → Reality: 5-second synchronous limit.
- Trap: "Migrate users while the app keeps using SRP." → Reality: the migrate user trigger never fires for SRP sign-in.
- Trap: "The CloudFormation-created trigger works without anything else." → Reality: Cognito also needs the Lambda resource-based permission.
Practice questions
- A company is moving 200,000 users from a legacy MySQL table with salted password hashes into a Cognito user pool. Users must keep their current passwords and must not receive password-reset emails. Which approach meets these requirements with the LEAST effort?
- A. Add a migrate user trigger that checks credentials against the legacy store and returns
finalUserStatusCONFIRMED, and allowUSER_PASSWORD_AUTHon the app client. - B. Export the users to a CSV file, run a user import job and let users sign in with their old passwords.
- C. Create every user with
AdminCreateUserand a temporary password while suppressing the invitation message. - D. Add a pre authentication trigger that copies the legacy password hash into a custom attribute at each sign-in.
- A. Add a migrate user trigger that checks credentials against the legacy store and returns
- An API behind an API Gateway Cognito authorizer needs a
tenant_idclaim in the access tokens that users present. The value comes from a DynamoDB lookup at sign-in. Which TWO steps are required? (Select TWO.)- A. Add a post authentication trigger that returns
tenant_idin its response. - B. Store the value in a
custom:tenant_idattribute and read it from the access token. - C. Configure a pre token generation trigger with event version
V2_0that sets the claim underaccessTokenGeneration. - D. Make sure that the user pool is on the Essentials or Plus feature plan.
- E. Enable token revocation on the app client.
- A. Add a post authentication trigger that returns
Answers
- A. Just-in-time migration validates the old password once and creates a confirmed user with it. B: CSV import can't carry passwords, so imported users must reset; C forces a password change; D can't log anyone in, since Cognito never sees the legacy hash format.
- C, D. Access-token claims need a V2 (or V3) pre token generation event, which requires Essentials or Plus. A: post authentication can't change tokens; B: custom attributes reach the ID token only; E: revocation has nothing to do with claims.
5. Managed login domains and OAuth flows
Concept
- Giving a user pool a domain switches on its OAuth 2.0 / OpenID Connect authorization server and hosted pages. The pages come in two branding versions per domain: managed login (current) or the classic hosted UI. They handle sign-up, sign-in, MFA, passkey registration and password reset, plus the redirects to federated identity providers.
- Endpoints on the domain:
/oauth2/authorize(start sign-in, or jump straight to an IdP),/loginand/logout,/oauth2/token(exchange codes, refresh, client credentials),/oauth2/userInfo,/oauth2/revoke, and the IdP callbacks/oauth2/idpresponseand/saml2/idpresponse. Discovery and keys live athttps://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/openid-configurationand…/jwks.json. - Domain types: a prefix domain
<prefix>.auth.<region>.amazoncognito.com, or a custom domain such asauth.example.com, which Cognito serves through an AWS-managed CloudFront distribution: the ACM certificate must be in us-east-1, the parent domain needs a DNS A record, and you point an alias record at the CloudFront name Cognito returns. - OAuth grants: authorization code (browser gets a one-time code, the app swaps it for tokens; add PKCE for public clients — the most secure choice), implicit (tokens in the URL fragment, no refresh token, no PKCE; testing and legacy only), client credentials (machine-to-machine: needs a client secret, returns only an access token with custom scopes).
- Resource servers define custom scopes (
<identifier>/<scope>, e.g.orders/read) that access tokens carry to your APIs. - Federated users (Google, SAML, OIDC…) are created in the pool through attribute mapping and can sign in only through the authorize endpoint or managed login, never through
InitiateAuth.
Exam facts
- ⚠️ Changed: managed login (since 22 November 2024) adds a no-code branding editor, passkeys, localisation (
langquery parameter) and dark mode, and needs Essentials or Plus; the classic hosted UI (logo plus a CSS file) remains in every plan and is the only choice in Lite; each domain has one branding version for all its app clients (ManagedLoginVersion1 = hosted UI classic, 2 = managed login). Older material may say the hosted UI can only be customised with a logo and CSS. - App clients created through the API get no branding style: call
CreateManagedLoginBranding(e.g. withUseCognitoProvidedValues) before managed login pages work for them. The console assigns a default style automatically. - Custom domain: certificate in us-east-1 whatever the pool's Region (unchanged); up to 4 custom domains per Region; managed login requires a TLS 1.2+ security policy; a prefix domain is ready in about a minute, a custom domain in about five, a replaced certificate in up to an hour. ⚠️ Changed: in the current console the domain is set under Branding → Domain (API
CreateUserPoolDomainwithCustomDomainConfig). Older material may say the custom domain is defined in the "App integration" section. - Callback URLs: absolute, pre-registered, no fragment, HTTPS except
http://localhostfor testing; up to 100 per app client.AllowedOAuthFlowsUserPoolClientmust betruebefore OAuth flows, scopes and callback URLs can be set. - Scopes:
openidis needed to receive an ID token;email,phone,profileadd attribute claims;aws.cognito.signin.user.adminallows the user's self-service user pool calls; custom scopes come from resource servers. - Client credentials: only on a client with a secret and without code or implicit grants; returns an access token only (no ID token); billed separately;
GetClientToken(flowALLOW_CLIENT_TOKEN_AUTH) gets the same M2M access token without a domain. - Managed login sets a 1-hour session cookie (re-sign-in without credentials until it expires or
/logoutclears it); it offers no custom auth challenges and no profile self-service (editing attributes, choosing MFA), and it accepts no custom CORS policy (the token, revoke and userInfo endpoints answerAccess-Control-Allow-Origin: *).
Developer angle
Authorization code grant with PKCE (split over lines for reading; one URL in practice):
https://auth.example.com/oauth2/authorize?response_type=code&client_id=1example23456789
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid+email+orders%2Fread
&state=4f1c9a2b&code_challenge_method=S256&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
# Swap the code for tokens (public client: code_verifier instead of a secret)
curl -s -X POST https://auth.example.com/oauth2/token -H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code -d client_id=1example23456789 -d code="$AUTH_CODE" \
--data-urlencode redirect_uri=https://app.example.com/callback -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
# Machine-to-machine: confidential client, custom scopes only, access token only
curl -s -X POST https://auth.example.com/oauth2/token -u "7m2mclient890abc:$CLIENT_SECRET" \
-H "Content-Type: application/x-www-form-urlencoded" -d grant_type=client_credentials -d scope=orders/read
# Resource server with two custom scopes
aws cognito-idp create-resource-server --user-pool-id eu-west-1_Example1 --identifier orders --name OrdersAPI \
--scopes ScopeName=read,ScopeDescription=ReadOrders ScopeName=write,ScopeDescription=WriteOrders
# Custom domain with managed login; certificate in us-east-1; the response's CloudFrontDomain is the alias target
aws cognito-idp create-user-pool-domain --user-pool-id eu-west-1_Example1 --domain auth.example.com \
--managed-login-version 2 \
--custom-domain-config CertificateArn=arn:aws:acm:us-east-1:111122223333:certificate/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111
# Default branding style for an app client created with the API
aws cognito-idp create-managed-login-branding --user-pool-id eu-west-1_Example1 \
--client-id 1example23456789 --use-cognito-provided-values
Traps
- Trap: "Request the ACM certificate in the user pool's Region." → Reality: us-east-1 only, because CloudFront serves the custom domain.
- Trap: "Implicit grant is the secure choice for single-page apps." → Reality: authorization code + PKCE; implicit exposes tokens in the URL and gives no refresh token.
- Trap: "Client credentials return an ID token for the service." → Reality: an access token with custom scopes only.
- Trap: "Managed login on a Lite pool." → Reality: Lite gets the classic hosted UI only.
- Trap: "Sign federated Google users in with
InitiateAuth." → Reality: federated sign-in goes through/oauth2/authorizeor managed login. - Trap: "The app client made with the CLI should show managed login straight away." → Reality: create a branding style first.
Practice questions
- A single-page application signs users in through managed login. It needs refresh tokens, and tokens must never appear in the browser's address bar or history. Which OAuth 2.0 grant should the application use?
- A. Implicit grant with
response_type=token. - B. Authorization code grant with PKCE.
- C. Client credentials grant called from the browser.
- D.
USER_PASSWORD_AUTHwith the password kept in local storage.
- A. Implicit grant with
- A company wants its managed login pages at
https://login.example.comfor a user pool in eu-central-1. Which TWO actions are required? (Select TWO.)- A. Request a public ACM certificate for
login.example.comin us-east-1. - B. Request a public ACM certificate for
login.example.comin eu-central-1. - C. Create a CloudFront distribution that uses the user pool's prefix domain as its origin.
- D. Create a DNS alias record that points
login.example.comto the CloudFront domain that Cognito returns. - E. Upload the certificate to IAM and reference its ARN in the user pool domain.
- A. Request a public ACM certificate for
Answers
- B. The code travels in the URL, but the tokens come back from the token endpoint, and PKCE binds the code to the app that started the flow. A puts tokens in the URL fragment without a refresh token; C needs a secret and issues no user tokens; D stores the password in the browser.
- A, D. Cognito creates and manages the CloudFront distribution, so you only supply a us-east-1 ACM certificate and the DNS alias. B is the wrong Region; C duplicates what Cognito already runs; E: user pool domains take ACM certificates, not IAM server certificates.
6. Feature plans and threat protection
Concept
- A user pool runs on one of three feature plans (
UserPoolTier), chosen per pool and switchable at any time: Lite (core sign-up and sign-in, federation, classic hosted UI, SMS/TOTP MFA, ID-token customisation, Lambda triggers, M2M), Essentials (the default for new pools: adds managed login branding, passkeys, passwordless one-time codes, email MFA, choice-based sign-in, access-token customisation) and Plus (Essentials plus threat protection and user-activity logs). - Threat protection (formerly "advanced security features") watches each sign-in, sign-up and password change. Adaptive authentication rates each session (no risk, low, medium, high) from signals like the IP address, user agent and distance from earlier sign-ins, then applies the response you chose for that level; compromised-credentials detection compares submitted passwords with breached-password lists.
- It starts in audit-only mode (scores and logs, CloudWatch metrics, no action; AWS suggests at least two weeks) before full-function mode applies the automatic responses. Settings exist per pool, can be overridden per app client, and are separate for standard and custom authentication.
Exam facts
- ⚠️ Changed: Lite, Essentials and Plus plans since 22 November 2024, with Essentials as the default for new pools; using a feature outside your plan returns
FeatureUnavailableInTierException. Older material may say there is one per-user price plus an optional "advanced security features" add-on. - ⚠️ Changed: the former advanced security features now sit in Essentials or Plus; threat protection (adaptive authentication, compromised credentials, log export) is Plus only, and
AdvancedSecurityModeAUDITorENFORCEDrequires tierPLUS. Older material may say adaptive authentication and compromised-credential blocking are a separately priced add-on or a basic user pool feature. - Compromised credentials: checks sign-in, sign-up and password change; actions block or allow; works only where Cognito sees the password (
USER_PASSWORD_AUTH,ADMIN_USER_PASSWORD_AUTH), not with SRP or custom authentication. - Adaptive authentication: works with password, SRP and custom authentication. Responses per risk level: allow, optional MFA (
MFA_IF_CONFIGURED), require MFA (MFA_REQUIRED), block (BLOCK), each optionally with an email notification through SES; a higher risk level can't get a weaker response than a lower one. The pool's MFA setting must be Optional. - Not covered: federated sign-ins and M2M (client credentials) requests. Threat protection scores single requests and does no rate limiting: put an AWS WAF web ACL on the user pool against floods. Full-function mode adds always-allow and always-block IP lists (CIDR); blocked requests still count against request quotas.
- Logs: user activity and risk details exported to CloudWatch Logs, Amazon S3 or Amazon Data Firehose; audit mode also publishes CloudWatch metrics.
- Apps built with an SDK should pass
UserContextData(encoded device data, plus the source IP when the app client hasEnablePropagateAdditionalUserContextData, which requires a client secret) so the risk engine sees the real user rather than your server. - APIs:
SetRiskConfiguration(pool, or pool +ClientId),UpdateUserPoolwithUserPoolAddOns.AdvancedSecurityMode,AdminListUserAuthEvents,AdminUpdateAuthEventFeedback(mark an event valid or invalid to tune the model). Prices per monthly active user differ by plan and aren't worth memorising.
Developer angle
Start in audit mode on the Plus plan, then define the responses for full-function mode:
# UpdateUserPool resets omitted settings: include your current configuration as well
aws cognito-idp update-user-pool --user-pool-id eu-west-1_Example1 \
--user-pool-tier PLUS --user-pool-add-ons AdvancedSecurityMode=AUDIT
aws cognito-idp set-risk-configuration --cli-input-json file://risk.json
aws cognito-idp admin-list-user-auth-events --user-pool-id eu-west-1_Example1 --username maria@example.com --max-results 10
risk.json (notifications off here; turning them on needs a NotifyConfiguration with an SES identity):
{
"UserPoolId": "eu-west-1_Example1",
"CompromisedCredentialsRiskConfiguration": {
"EventFilter": ["SIGN_IN", "SIGN_UP", "PASSWORD_CHANGE"], "Actions": { "EventAction": "BLOCK" } },
"AccountTakeoverRiskConfiguration": { "Actions": {
"LowAction": { "EventAction": "NO_ACTION", "Notify": false },
"MediumAction": { "EventAction": "MFA_IF_CONFIGURED", "Notify": false },
"HighAction": { "EventAction": "MFA_REQUIRED", "Notify": false } } }
}
- The configuration takes effect only after
AdvancedSecurityModeis switched toENFORCED. Switching to a lower plan may require turning off the features it lacks first.
Traps
- Trap: "Enable adaptive authentication on Essentials." → Reality: Plus only.
- Trap: "Compromised-credential checks protect SRP sign-ins." → Reality: they need the password in the request (password flows).
- Trap: "Threat protection stops a credential-stuffing flood." → Reality: no rate limiting; add AWS WAF.
- Trap: "Go straight to full-function mode." → Reality: AWS recommends audit-only first to learn normal behaviour.
- Trap: "Adaptive MFA with the pool's MFA set to Required." → Reality: MFA must be Optional for risk-based prompts.
- Trap: "Risk scores apply to Google or SAML sign-ins." → Reality: federated sign-in isn't covered.
Practice questions
- A retailer wants to observe Cognito's risk assessments of real sign-ins for two weeks before blocking anything, without affecting users. Which configuration meets this requirement?
- A. Essentials plan with a pre authentication trigger that writes each source IP address to CloudWatch Logs.
- B. Plus plan with
AdvancedSecurityModeset toENFORCEDand every risk level set to block. - C. Essentials plan with CloudTrail data events turned on for the user pool.
- D. Plus plan with
AdvancedSecurityModeset toAUDIT.
- A user pool must block sign-ins that use passwords found in public breaches and must require MFA for high-risk sign-ins. The custom app signs users in with
USER_SRP_AUTH, and MFA is set to optional. Which TWO changes are needed? (Select TWO.)- A. Set MFA to required for all users.
- B. Move to the Plus plan and enable full-function threat protection with compromised-credential blocking and
MFA_REQUIREDfor high risk. - C. Switch the app's sign-in flow from
USER_SRP_AUTHtoUSER_PASSWORD_AUTH. - D. Associate an AWS WAF rate-based rule with the user pool.
- E. Add a pre token generation trigger that compares passwords with a breach list.
Answers
- D. Audit-only mode scores and logs every event with no automatic response. A logs data but produces no risk scores; B blocks users immediately; C records API calls, not sign-in risk.
- B, C. Plus with full-function mode provides both responses, and the breached-password check only works in password flows. A breaks risk-based MFA (it needs Optional); D limits request rates, not breached passwords; E: the trigger never receives the password.
7. ALB authentication with Cognito and OIDC
Concept
- An ALB listener rule can authenticate browser users before it forwards their requests: the
authenticate-cognitoaction uses a user pool (which may itself federate social, SAML or OIDC providers) andauthenticate-oidctalks to any OpenID Connect provider directly. The targets receive signed user claims in headers and contain no sign-in code. - Flow: request without a valid session cookie → the ALB redirects the browser to the IdP's authorize endpoint → the user signs in → the IdP redirects to
https://<alb-dns-or-cname>/oauth2/idpresponsewith an authorization code → the ALB swaps it at the token endpoint and calls the userInfo endpoint → sets theAWSELBAuthSessionCookie→ redirects to the original URL. Later requests only present the cookie. - ⚠️ Changed: for APIs and machine clients the ALB can also verify bearer JWTs itself with the
jwt-validationrule action (12 November 2025), with no redirect. Older material may say token validation must happen in application code or in API Gateway.
Exam facts
- Both authenticate actions work only on HTTPS listeners. Cognito requirements: a user pool domain; an app client with a client secret, the authorization code grant and the scopes the ALB requests (default
openid, which returns an ID token;aws.cognito.signin.user.adminalone doesn't); callback URLshttps://<ALB DNS name>/oauth2/idpresponseand/orhttps://<CNAME>/oauth2/idpresponsein lowercase; whoever configures the rule needscognito-idp:DescribeUserPoolClient. - OIDC requirements: client ID and client secret, issuer, authorization, token and userInfo endpoints with publicly resolvable DNS and certificates from a public CA.
OnUnauthenticatedRequest:authenticate(default; redirect to sign-in),allow(forward without claims: generic page for anonymous users, personal page for signed-in ones),deny(HTTP 401, for AJAX calls of single-page apps). Defaults:SessionTimeout604,800 s (7 days);SessionCookieNameAWSELBAuthSessionCookie;Scopeopenid; up to 10AuthenticationRequestExtraParams.- Headers to the targets:
x-amzn-oidc-datacarries the user claims as a JWT signed by the ALB with ES256 — verify the signature and that thesignerheader field equals your ALB ARN; the public key comes fromhttps://public-keys.auth.elb.<region>.amazonaws.com/<kid>. ⚠️ Changed:x-amzn-oidc-accesstokenandx-amzn-oidc-identityare legacy unsigned plaintext headers, never a basis for authorization. Older material may say all three headers are equally trustworthy. The ALB passes the access token and user claims to targets, never the ID token. - Limits: sign-in must finish within 15 minutes (else HTTP 401); user claims plus access token above 11 KB → HTTP 500 and the
ELBAuthUserClaimsSizeExceededmetric; cookies are split into 4 KB shards (up to 16 KB). - Logout: the app expires the auth cookie (expiry -1) and redirects to the IdP's logout endpoint (Cognito
/logout); the logout landing page must not sit behind an authenticate rule. The ALB reaches the token and userInfo endpoints over IPv4 only, so an internal ALB needs an outbound path (e.g. a NAT gateway). jwt-validation: HTTPS listener; RS256 only;issandexpmandatory,nbfandiatchecked when present, up to 10 extra claims; JWKS response ≤ 150 KB and ≤ 10 keys; valid tokens are forwarded unchanged.
Developer angle
Authenticate, then forward (aws elbv2 create-rule … --actions file://auth-actions.json):
[
{ "Type": "authenticate-cognito", "Order": 1,
"AuthenticateCognitoConfig": {
"UserPoolArn": "arn:aws:cognito-idp:eu-west-1:111122223333:userpool/eu-west-1_Example1",
"UserPoolClientId": "4alb2client3456789", "UserPoolDomain": "auth.example.com",
"OnUnauthenticatedRequest": "authenticate", "Scope": "openid email", "SessionTimeout": 28800 } },
{ "Type": "forward", "Order": 2,
"TargetGroupArn": "arn:aws:elasticloadbalancing:eu-west-1:111122223333:targetgroup/web/0123456789abcdef" }
]
- In the target, verify
x-amzn-oidc-datawith any ES256-capable JWT library (oraws-jwt-verify): readkidandsignerfrom the unverified header, reject asignerother than your ALB ARN, fetch the PEM key fromhttps://public-keys.auth.elb.<region>.amazonaws.com/<kid>, then verify and use thesubclaim.
Bearer-token validation for M2M callers at the ALB:
[
{ "Type": "jwt-validation", "Order": 1,
"JwtValidationConfig": {
"JwksEndpoint": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_Example1/.well-known/jwks.json",
"Issuer": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_Example1",
"AdditionalClaims": [ { "Format": "string-array", "Name": "client_id", "Values": ["7m2mclient890abc"] } ] } },
{ "Type": "forward", "Order": 2,
"TargetGroupArn": "arn:aws:elasticloadbalancing:eu-west-1:111122223333:targetgroup/api/0123456789abcdef" }
]
- Lock the targets down so only the ALB can reach them (security group referencing the ALB's security group), otherwise anyone can forge the headers on a direct request.
Traps
- Trap: "Add
authenticate-cognitoto the HTTP:80 listener." → Reality: HTTPS listeners only. - Trap: "Use a public app client without a secret for the ALB." → Reality: the ALB is a confidential client and needs the secret.
- Trap: "Authorize on
x-amzn-oidc-identity." → Reality: unsigned; verifyx-amzn-oidc-data(ES256,signer). - Trap: "
denysends users to the login page." → Reality: requests with no session get HTTP 401 (only an expired session is redirected to the IdP). - Trap: "
authenticate-oidccan point at a SAML IdP." → Reality: SAML needs a user pool in between (authenticate-cognito). - Trap: "Use the ALB redirect flow for a mobile app's API calls." → Reality: use
jwt-validation, an API Gateway authorizer or token checks in code.
Practice questions
- An internal reporting web app runs on EC2 instances behind an Application Load Balancer. Employees must sign in with the company's SAML 2.0 identity provider, and the developers want to write no authentication code. Which solution meets these requirements?
- A. Add an
authenticate-oidcaction that points directly at the SAML identity provider's endpoints. - B. Add Amplify Auth to the web app and validate tokens in the application code.
- C. Federate the SAML IdP into a Cognito user pool and add an
authenticate-cognitoaction to the HTTPS listener rule. - D. Put CloudFront in front of the ALB and require signed cookies.
- A. Add an
- After a developer adds an
authenticate-cognitoaction to an ALB rule, users sign in successfully on the Cognito pages but then get a redirect error instead of the application. Which TWO changes are most likely needed? (Select TWO.)- A. Add
https://<ALB DNS name>/oauth2/idpresponseand the CNAME variant as allowed callback URLs of the app client. - B. Change the listener protocol to HTTP.
- C. Remove the client secret from the user pool app client.
- D. Set
OnUnauthenticatedRequesttoallow. - E. Enable the authorization code grant and the
openidscope on the app client.
- A. Add
Answers
- C. The user pool brokers SAML, and the ALB handles the browser flow, so the app gets signed claims without code. A: SAML isn't OIDC; B means writing auth code; D: signed cookies come from your own signing logic, not corporate sign-in.
- A, E. The ALB's callback must be registered, and the client must allow the code grant with
openid. B: authenticate actions require HTTPS; C: the ALB needs the secret; D only skips sign-in for anonymous users.
8. Identity pools and temporary AWS credentials
Concept
- An identity pool gives every user or device a stable identity ID (
<region>:<guid>) and swaps proof of sign-in for AWS credentials. Accepted proof: a user pool ID token, a Google, Sign in with Apple, Facebook or Login with Amazon token, an ID token from an OIDC provider registered in IAM, a SAML assertion from an IAM SAML provider, a developer-authenticated identity, or nothing at all for guests. - Enhanced (simplified) flow:
GetId→GetCredentialsForIdentity→ credentials. Cognito chooses the role (default role, rules, token or attributes), callssts:AssumeRoleWithWebIdentitybehind the scenes and, for guests, adds a scope-down session policy. AWS calls it the most secure option with the least code. - Basic (classic) flow:
GetId→GetOpenIdToken→ your app callssts:AssumeRoleWithWebIdentitywith a role of its choice. It allows a custom session duration and cross-account roles, but leaves role choice to the client; keep it off unless you need it (AllowClassicFlow), and it doesn't work with role mappings. - Developer-authenticated identities: your backend signs the user in its own way, calls
GetOpenIdTokenForDeveloperIdentitywith IAM credentials and returns the token; the client continues withGetCredentialsForIdentity.
Exam facts
- Role trust policy: principal
{"Federated": "cognito-identity.amazonaws.com"}, actionsts:AssumeRoleWithWebIdentity, conditioncognito-identity.amazonaws.com:aud= the identity pool ID (IAM refuses to save a trust policy for this principal without anaudcondition) andcognito-identity.amazonaws.com:amr=authenticatedorunauthenticated(ForAnyValue:StringLike). A wrong trust policy makes STS refuse the role: in the basic flow yourAssumeRoleWithWebIdentitycall getsAccessDenied -- Not authorized to perform sts:AssumeRoleWithWebIdentity; in the enhanced flowGetCredentialsForIdentityreturnsInvalidIdentityPoolConfigurationException(HTTP 400; also returned when the pool has no role for that kind of user). - ⚠️ Changed: the STS call behind identity pools is
AssumeRoleWithWebIdentity; in the enhanced flow Cognito makes it for you. Older material may say "AssumeRoleForWebIdentity" (the course's spelling). - Enhanced flow: credentials last 1 hour; the role must be in the identity pool's account; don't use
aws:SourceIpin the trust policy (the request comes from Cognito, so the condition never matches). - Guest sessions in the enhanced flow are capped by an inline session policy plus the managed
AmazonCognitoUnAuthedIdentitiesSessionPolicy: only a fixed list of services works, including S3, DynamoDB, Lambda, SQS, SNS, Kinesis, Firehose, API Gateway (execute-api), CloudWatch, CloudWatch Logs, AppSync, IoT, Polly, Translate, Transcribe, Rekognition and Comprehend (KMS, SageMaker and Textract only for listed actions). A guest call to any other service fails withNotAuthorizedExceptioneven if the role allows it: front it with API Gateway/Lambda or use the basic flow. - Authenticated role selection: the default authenticated role; choose role from token (user pool groups →
cognito:preferred_rolefrom the group with the lowest precedence number,cognito:roles, optionallyCustomRoleArninGetCredentialsForIdentity); rules (claimEquals,NotEqual,StartsWithorContainsa value → role; first match wins; up to 25 rules per provider). When nothing decides:AmbiguousRoleResolution=AuthenticatedRoleorDeny. - Attributes for access control (ABAC): token claims become principal (session) tags read by
${aws:PrincipalTag/<key>}; the trust policy must also allowsts:TagSession; default mapping for user pools isaud(client ID) andsub. Never map claims that users can change (attributes in an app client'sWriteAttributes) to privileged roles or tags; values containing*or?act as wildcards in policies. GetIdandGetCredentialsForIdentityare public APIs (no AWS credentials);GetOpenIdTokenForDeveloperIdentityandSetIdentityPoolRolesare IAM-authorized, and setting roles needsiam:PassRoleon them (§13). Rate quotas:GetId25 RPS,GetCredentialsForIdentity200 RPS,GetOpenIdToken200 RPS,GetOpenIdTokenForDeveloperIdentity50 RPS (raised through your account team). Cache the identity ID after the first call.
Developer angle
Trust policy of the authenticated role:
{ "Version": "2012-10-17",
"Statement": [{ "Effect": "Allow", "Principal": { "Federated": "cognito-identity.amazonaws.com" },
"Action": ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"],
"Condition": {
"StringEquals": { "cognito-identity.amazonaws.com:aud": "eu-west-1:11111111-2222-3333-4444-555555555555" },
"ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" } } }] }
Assign the default roles (the caller needs iam:PassRole on both):
aws cognito-identity set-identity-pool-roles \
--identity-pool-id eu-west-1:11111111-2222-3333-4444-555555555555 \
--roles authenticated=arn:aws:iam::111122223333:role/AppAuthRole,unauthenticated=arn:aws:iam::111122223333:role/AppGuestRole
Browser or React Native code lets the SDK handle GetId, credentials and refresh:
import { S3Client } from "@aws-sdk/client-s3";
import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers";
export const s3ForUser = (idToken, region = "eu-west-1") => new S3Client({
region,
credentials: fromCognitoIdentityPool({
clientConfig: { region },
identityPoolId: "eu-west-1:11111111-2222-3333-4444-555555555555",
logins: { "cognito-idp.eu-west-1.amazonaws.com/eu-west-1_Example1": idToken }, // omit for guests
}),
});
- Omitting
loginsgives guest credentials (if the pool allows unauthenticated identities); per-user data access uses${cognito-identity.amazonaws.com:sub}(the identity ID) in the role's policies (§12).
Traps
- Trap: "Trust
cognito-identity.amazonaws.comwithout conditions." → Reality: IAM requires theaudcondition; without it any identity pool could use the role. - Trap: "The guest role may call any service its policy allows." → Reality: the enhanced-flow scope-down caps guests to a fixed service list.
- Trap: "Ask for 12-hour credentials from
GetCredentialsForIdentity." → Reality: enhanced-flow credentials last 1 hour. - Trap: "Let the client choose the role in the basic flow for convenience." → Reality: that hands role choice to the user; prefer token- or rule-based selection in the enhanced flow.
- Trap: "Restrict the role to the office IP range with
aws:SourceIp." → Reality: never matches in the enhanced flow. - Trap: "Map the writable
custom:roleattribute to the admin role." → Reality: users could promote themselves; use groups or immutable admin-set attributes.
Practice questions
- A mobile app gets credentials for guest users from an identity pool with the enhanced flow. The unauthenticated role allows
states:StartExecution, yet the app's call to AWS Step Functions fails. What is the cause?- A. Guest sessions in the enhanced flow get a scope-down session policy that doesn't include Step Functions.
- B. The role's trust policy is missing the
cognito-identity.amazonaws.com:audcondition. - C. The identity pool exceeded its
GetIdrequest quota. - D. Guest credentials from identity pools expire after 15 minutes.
- Users in the user pool group
editorsmust receive an IAM role with write access, all other signed-in users a read-only role, and the client app must not be able to pick its own role. Which TWO configurations meet these requirements? (Select TWO.)- A. Enable the basic flow and let the app call
AssumeRoleWithWebIdentitywith whichever role it needs. - B. Attach the write role's ARN, with a precedence value, to the
editorsgroup in the user pool. - C. Map a user-writable custom attribute
custom:roleto role-selection rules. - D. Set the identity pool's authenticated role selection to choose the role from the token, with ambiguous role resolution set to the default (read-only) authenticated role.
- E. Create IAM users for the editors and give them access keys.
- A. Enable the basic flow and let the app call
Answers
- A. Step Functions isn't in the guest scope-down list, so the session can't use it whatever the role says; front it with API Gateway (allowed) or use the basic flow. B would stop the role being assumed at all; C would fail
GetId, not the Step Functions call; D: enhanced-flow credentials last 1 hour. - B, D. The group's role arrives in the ID token, and the identity pool honours it with a safe fallback. A gives the client free choice of role; C lets users elevate themselves; E abandons Cognito for long-term keys.
9. STS AssumeRole and cross account access
Concept
- AWS STS issues temporary credentials: an access key ID (
ASIA…), a secret access key, a session token and an expiration. A role has two policies: the trust policy (a resource-based policy saying who may assume it) and permissions policies (what the session may do). AssumeRole: an IAM user or role session asks STS for the role's credentials with aRoleArnand aRoleSessionName. STS checks the role's trust policy and, for callers from another account, that the caller's own identity policy allowssts:AssumeRoleon that role ARN. The session's permissions are the role's policies, narrowed further by any session policy passed in the call.- Cross-account delegation: the trusting account owns the resource and the role and names the trusted account (or one of its roles) in the trust policy; the trusted account's administrator decides which of its users or roles may call
sts:AssumeRoleon it. Switching to a role means giving up your own permissions for that session — unlike resource-based policies (S3, SQS, SNS, KMS…), where you keep your own permissions and gain access to one resource. - Third-party access: a vendor that serves many customers should be forced to present a per-customer external ID (
sts:ExternalIdcondition) so it can't be tricked into using your role for someone else (the confused deputy problem).
Exam facts
| STS API | Who calls it | Typical use | Credential lifetime |
|---|---|---|---|
AssumeRole |
IAM user or role session | Delegation in or across accounts | 900 s up to the role's maximum (1–12 h), default 1 h |
AssumeRoleWithSAML |
Anyone with a SAML assertion from a trusted IdP | Workforce SAML federation | same as AssumeRole |
AssumeRoleWithWebIdentity |
Anyone with an OIDC token from a trusted IdP | Identity pools, CI/CD with OIDC | same as AssumeRole |
GetSessionToken |
IAM user (root possible, not recommended) | MFA-protected API calls (§10) | 15 min–36 h, default 12 h (root ≤ 1 h) |
GetFederationToken |
IAM user (root possible) | Custom identity broker | 15 min–36 h, default 12 h (root ≤ 1 h) |
GetCallerIdentity |
Anyone | "Who am I?" — needs no permission | — |
DecodeAuthorizationMessage |
Caller with sts:DecodeAuthorizationMessage |
Decode an encoded authorization failure | — |
GetAccessKeyInfo |
Any IAM principal | Which account owns an access key ID (AKIA… long-term, ASIA… temporary) |
— |
AssumeRoot |
Organizations management account or delegated admin | Privileged root task on a member account | ≤ 900 s |
- ⚠️ Changed: role sessions default to 1 hour and can last from 900 seconds up to the role's maximum session duration, which you set between 1 and 12 hours (
aws iam update-role --max-session-duration); asking for more than the maximum fails; role chaining (using role credentials to assume another role) allows at most 1 hour whatever the role's maximum, and a chained request for more than 3,600 seconds fails;GetSessionTokenandGetFederationTokengo up to 36 hours. Older material may say temporary credentials last 15 minutes to 1 hour (the course does). - Same account: a user can assume a role if either the user's identity policy allows
sts:AssumeRole(and the trust policy trusts the account) or the trust policy names the user directly. Across accounts both are required. - Trust policy principals: an account (
arn:aws:iam::111122223333:root= the whole account, which then delegates through its own IAM policies), a specific role or user ARN, a service principal (lambda.amazonaws.com; addaws:SourceArn/aws:SourceAccountconditions against the confused deputy), or a federated principal. No wildcard inside a principal ARN; role names in ARNs are case-sensitive. - ⚠️ Changed: AWS recommends Amazon Cognito for mobile apps, and lists calling
AssumeRoleWithWebIdentitydirectly as a valid way to give temporary credentials to workloads that hold a JWT from an OIDC IdP configured in your account (for example a CI/CD pipeline). Older material may say AWS advises against calling it at all (the course does). - Role credentials can't call
GetSessionTokenorGetFederationToken. A role that needs to assume itself must be explicitly trusted; compute services (EC2, Lambda, ECS) refresh role credentials themselves, so don't re-assume the role you already have. - Session policies: one inline JSON policy (
Policy) plus up to 10 managed policy ARNs (PolicyArns), 2,048 characters combined; the result is the intersection with the role's policies, never more. Session tags: up to 50 per session (key ≤ 128 characters, value ≤ 256), needsts:TagSession;TransitiveTagKeyscarry them through role chaining.SourceIdentity(needssts:SetSourceIdentity) can't be changed once set and follows the chain into CloudTrail. RoleSessionName(2–64 characters) appears in the session ARNarn:aws:sts::<account>:assumed-role/<role>/<session>and in CloudTrail;sts:RoleSessionNamecan force it to equal${aws:username}.ExternalId: 2–1,224 characters.- Errors:
AccessDeniedwith… is not authorized to perform: sts:AssumeRole on resource: …— check the caller's identity policy, the trust principal, trust conditions (external ID, MFA, tags, dates) and the exact role name; "because no role trust policy allows the sts:AssumeRole action" points at the trust policy.RegionDisabled(HTTP 403) means STS isn't activated in that Region for the account asked to issue the credentials; only Regions enabled by default can have STS deactivated (IAM account settings), while an opt-in Region has STS active as soon as it is enabled. - STS request quota: 600 requests per second per account per Region, shared by
AssumeRole,GetCallerIdentity,GetSessionTokenand others; a cross-accountAssumeRolecounts against the caller's account only; AWS service principals don't consume it. - ⚠️ Changed: for Regions enabled by default, requests to the global endpoint
sts.amazonaws.comare now served in the Region where they originate (when resolved by the Amazon DNS server); opt-in Regions and other resolvers still go to us-east-1, and CloudTrail still logs global-endpoint calls in us-east-1. AWS recommends Regional endpoints (sts_regional_endpoints = regional/AWS_STS_REGIONAL_ENDPOINTS; AWS CLI v2 already uses them). Older material may say every global-endpoint request is served from us-east-1. - Token validity: session tokens from the global endpoint are valid only in Regions enabled by default, so calls in an opt-in Region such as
ap-east-1reject them as invalid (DynamoDB:UnrecognizedClientException) unless the IAM account setting for global-endpoint session tokens is changed to all Regions (aws iam set-security-token-service-preferences --global-endpoint-token-version v2Token; v2 tokens are longer). Tokens from Regional endpoints are valid in every Region, and STS is activated automatically when an opt-in Region is enabled (it can't be deactivated there). - ⚠️ Changed:
AssumeRoot(introduced with centralized root access for Organizations member accounts in November 2024) runs a privileged root task scoped by one of a few AWS managed task policies, lasts at most 900 seconds and needs a Regional STS endpoint (details:notes/iam-guide.md). Older material may say STS offers only the seven classic operations.
Developer angle
Account B (444455556666) owns the role; account A (111122223333) runs the caller. Trust policy of AuditReadOnly in account B, then the identity policy on nightly-audit in account A:
{ "Version": "2012-10-17",
"Statement": [{ "Effect": "Allow", "Action": "sts:AssumeRole",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/nightly-audit" } }] }
{ "Version": "2012-10-17",
"Statement": [{ "Effect": "Allow", "Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::444455556666:role/AuditReadOnly" }] }
Third-party variant: the vendor's account in Principal, plus "Condition": {"StringEquals": {"sts:ExternalId": "<value the vendor gave you>"}}.
aws sts assume-role --role-arn arn:aws:iam::444455556666:role/AuditReadOnly \
--role-session-name nightly-audit --duration-seconds 3600
aws iam update-role --role-name AuditReadOnly --max-session-duration 14400 # 4 h, in account B
aws sts get-caller-identity # with the new credentials: ...:assumed-role/AuditReadOnly/nightly-audit
- SDK:
boto3.client("sts", region_name="eu-west-1").assume_role(RoleArn=…, RoleSessionName=…)returnsCredentialswithAccessKeyId,SecretAccessKey,SessionTokenandExpiration; pass the first three to a new client. Setting a Region makes the SDK use the Regional STS endpoint. On a laptop, an assume-role profile (role_arn,source_profile, optionalexternal_id,mfa_serial,duration_seconds) lets the CLI and SDKs assume and refresh the role automatically — mechanics innotes/cli-sdk-policies-guide.md.
Traps
- Trap: "Updating the trust policy in account B is enough." → Reality: across accounts the caller also needs
sts:AssumeRoleon the role ARN in its own identity policy. - Trap: "Request
DurationSeconds14400 on a role with the default maximum." → Reality: the default maximum is 1 hour; raiseMaxSessionDurationfirst, and a chained request above 1 hour still fails. - Trap: "After assuming the role I still have my own permissions." → Reality: the session has only the role's permissions; use a resource-based policy when you need both.
- Trap: "Principal
…:rootmeans only the root user can assume the role." → Reality: it trusts the whole account, which delegates through IAM policies. - Trap: "Give the partner an IAM user's access keys." → Reality: cross-account role plus external ID.
- Trap: "The external ID is a secret that protects your own accounts." → Reality: it is an anti-confused-deputy identifier for third parties.
Practice questions
- A Lambda function in account A must stop and tag EC2 instances in account B every night. The security team forbids long-term credentials. Which solution follows least privilege?
- A. Create an IAM user in account B, store its access keys in Secrets Manager in account A and read them in the function.
- B. Add a resource-based policy to the function that allows account B to invoke it, and make the EC2 calls under that permission.
- C. Add account B's root principal to the trust policy of the function's execution role.
- D. Create a role in account B that trusts the execution role and allows only the needed EC2 actions, and have the function assume it.
- A developer in account A runs
aws sts assume-rolefor a role in account B and receivesAccessDenied. The role's trust policy names account A as its principal. Which TWO causes are possible? (Select TWO.)- A. The developer's identity-based policies don't allow
sts:AssumeRoleon the role's ARN. - B. The role's permissions policy doesn't allow the S3 actions the developer plans to run.
- C. The trust policy has an
sts:ExternalIdor MFA condition that the request doesn't satisfy. - D. The role's maximum session duration is set to 12 hours.
- E. Account A has a bucket policy that denies principals from account B.
- A. The developer's identity-based policies don't allow
Answers
- D. A narrowly scoped role in B, trusted by one execution role (which also needs
sts:AssumeRoleon it) and assumed on demand, uses only temporary credentials. A relies on long-term keys; B lets account B invoke the function but gives the function no rights in account B; C reverses the trust direction (it would let B assume A's role). - A, C. Cross-account assumption needs the caller's own permission, and every trust condition must be met. B affects later calls, not the assumption; D only allows longer sessions; E is unrelated to STS.
10. STS with MFA
Concept
- Policies can demand that a request was made by someone who proved a second factor recently:
aws:MultiFactorAuthPresentandaws:MultiFactorAuthAge. Long-term access keys never carry MFA information, so a CLI or SDK user first trades the keys plus an MFA code for temporary credentials, then makes the protected calls with those. - Two ways to get MFA-flagged credentials:
GetSessionToken(same IAM user, same permissions, now with MFA context) orAssumeRolewithSerialNumberandTokenCode(a role whose trust policy requires MFA). The code is entered once per session; policies then test the stored MFA context and its age.
Exam facts
GetSessionToken: called with an IAM user's long-term credentials (root possible, not recommended; roles can't call it); needs no permission. Duration for IAM users 900–129,600 s (36 h), default 43,200 s (12 h); root at most 3,600 s.- Returns
AccessKeyId,SecretAccessKey,SessionTokenandExpiration; the credentials have the user's permissions, can't call IAM APIs unless MFA was included, can call no STS operation exceptAssumeRoleandGetCallerIdentity, and can't be used for console federation. - MFA parameters (
GetSessionTokenandAssumeRole):SerialNumber(a hardware device serial such asGAHT12345678or a virtual device ARNarn:aws:iam::<account>:mfa/<name>) andTokenCode(6 digits); a wrong code returns access denied. aws:MultiFactorAuthPresentexists only in requests signed with temporary credentials; it is absent for long-term access keys and for federated identities. Requiring MFA to assume a role: trust policy condition"Bool": {"aws:MultiFactorAuthPresent": "true"}.- Recommended deny:
Deny+"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}blocks both non-MFA temporary credentials and long-term keys. Allow-only form:Allow+"Bool": {"aws:MultiFactorAuthPresent": "true"}. AvoidDeny+Boolfalse: long-term keys slip through because the key is missing.aws:MultiFactorAuthAge= seconds since the MFA check; useNumericLessThan(e.g. 3600) to demand recent MFA for destructive actions. - Devices: up to 8 MFA devices per IAM user or root user, passkeys included (see
notes/iam-guide.md); the CLI profile settingmfa_serialprompts for codes automatically (notes/cli-sdk-policies-guide.md).
Developer angle
Allow destructive EC2 actions only with MFA done within the last hour, and deny them otherwise:
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "AllowWithRecentMfa", "Effect": "Allow",
"Action": ["ec2:StopInstances", "ec2:TerminateInstances"], "Resource": "*",
"Condition": { "Bool": { "aws:MultiFactorAuthPresent": "true" },
"NumericLessThan": { "aws:MultiFactorAuthAge": "3600" } } },
{ "Sid": "DenyWithoutMfa", "Effect": "Deny",
"Action": ["ec2:StopInstances", "ec2:TerminateInstances"], "Resource": "*",
"Condition": { "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" } } }
]
}
# Long-term keys + MFA code -> 8-hour session credentials
aws sts get-session-token --serial-number arn:aws:iam::111122223333:mfa/maria --token-code 123456 --duration-seconds 28800
export AWS_ACCESS_KEY_ID="<AccessKeyId>" AWS_SECRET_ACCESS_KEY="<SecretAccessKey>" AWS_SESSION_TOKEN="<SessionToken>"
aws ec2 stop-instances --instance-ids i-0123456789abcdef0
# Role whose trust policy requires MFA
aws sts assume-role --role-arn arn:aws:iam::444455556666:role/ProdAdmin --role-session-name maria \
--serial-number arn:aws:iam::111122223333:mfa/maria --token-code 123456
- The
ProdAdmintrust policy names the trusted account ("AWS": "arn:aws:iam::111122223333:root") with"Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}}; without--serial-numberand--token-codethe call is denied.
Traps
- Trap: "Calls signed with access keys satisfy an MFA condition if the user has an MFA device." → Reality: the MFA key is absent for long-term keys; get session credentials first.
- Trap: "
DenywithBoolfalseblocks everything without MFA." → Reality: requests made with long-term keys pass; useBoolIfExists. - Trap: "A Lambda role calls
GetSessionTokento obtain MFA credentials." → Reality: only IAM users (or root) can call it. - Trap: "Enter the MFA code with every command." → Reality: once per session; the session carries the MFA context.
- Trap: "
GetSessionTokencredentials expire after 1 hour by default." → Reality: 12 hours by default for IAM users (1 hour maximum for root). - Trap: "MFA session credentials can call any IAM API." → Reality: IAM calls only if MFA was included; no STS calls besides
AssumeRoleandGetCallerIdentity.
Practice questions
- A policy statement denies
ec2:TerminateInstanceswith the condition"Bool": {"aws:MultiFactorAuthPresent": "false"}. A nightly script that uses an IAM user's long-term access keys can still terminate instances. What should the developer change?- A. Change the Deny condition to
"Bool": {"aws:MultiFactorAuthPresent": "true"}. - B. Change the Deny condition operator to
BoolIfExistswith the value"false". - C. Add a second Allow statement for
ec2:TerminateInstancesthat requires"Bool": {"aws:MultiFactorAuthPresent": "true"}. - D. Attach the policy to the script user's IAM group instead of the user.
- A. Change the Deny condition to
- A developer with an IAM user must run AWS CLI commands that are protected by MFA conditions for a full 8-hour working day without typing another code. Which TWO steps meet the requirement? (Select TWO.)
- A. Call
aws sts get-session-tokenwithout MFA parameters and rely on the MFA device registered for the user. - B. Call
aws sts get-session-tokenfrom an EC2 instance role to obtain MFA credentials. - C. Call
aws sts get-session-tokenwith--serial-number,--token-codeand--duration-seconds 28800. - D. Request
--duration-seconds 172800so that the credentials last two days. - E. Use the returned access key ID, secret access key and session token for the following commands.
- A. Call
Answers
- B. With
BoolIfExists, a missing MFA key counts as a match, so requests signed with long-term keys are denied too. A would deny MFA users instead; C adds an allow, and an extra allow never removes the permission the script already has; D only moves the same flawed statement. - C, E. One MFA-backed session of 8 hours (within the 36-hour limit), reused for every command. A yields credentials without MFA context; B: roles can't call
GetSessionToken; D exceeds the 129,600-second maximum.
11. Policy evaluation logic
Concept
- Every request starts as an implicit deny. AWS gathers every policy that applies and reaches a decision: one matching explicit
Denyanywhere ends it; otherwise the request needs anAllowfrom an identity-based or resource-based policy that also fits inside every guardrail (organization policies, permissions boundary, session policy). - Order within one account: 1) explicit deny in any policy → Deny; 2) resource control policies (RCPs) must allow; 3) service control policies (SCPs) must allow; 4) a resource-based policy
Allowcan decide the request on its own in the same account, depending on the principal type; 5) identity-based policies must allow; 6) the permissions boundary must allow; 7) for sessions, the session policy must allow. - Across accounts AWS evaluates twice: the trusted account (identity policy plus its guardrails) and the trusting account (resource-based policy or trust policy plus its guardrails). Both must allow.
- Guardrails — SCPs, RCPs, permissions boundaries, session policies — never grant anything; they only set ceilings.
Exam facts
- ⚠️ Changed: resource control policies (13 November 2024) are Organizations policies that cap the permissions on resources in member accounts, whoever calls (outside principals included), and are evaluated alongside SCPs; launched for S3, STS, KMS, SQS and Secrets Manager, they now cover dozens of services (DynamoDB, ECR, CloudWatch Logs, Cognito and more). Not affected: management-account resources, service-linked roles, AWS managed KMS keys. Older material may say only SCPs, permissions boundaries, session policies, identity-based and resource-based policies take part.
- Same account: a resource-based policy that names an IAM user ARN or a role session ARN grants access directly, even with an implicit deny in the identity policy, boundary or session policy; one that names a role ARN is still limited by implicit denies in a permissions boundary or session policy.
- Exceptions that must explicitly allow the principal: role trust policies and KMS key policies (an IAM allow alone doesn't open a KMS key unless its key policy delegates to the account) →
notes/security-encryption-guide.md. - S3 in one account: one allow on either side is enough (the role's policy, or a bucket policy that names the role), and one explicit deny on either side blocks the request, whatever the other side says.
- SCPs apply to every IAM user and role in member accounts, the member's root user included, but not to the management account or to service-linked roles. A user with
AdministratorAccessstill can't do what an SCP blocks. - Readable denials:
… is not authorized to perform: <action> on resource: <arn> with an explicit deny in a <type> policy[: <policy ARN>]or… because no <type> policy allows the <action> action, where the type is service control, resource control, identity-based, resource-based, session, permissions boundary, VPC endpoint or role trust policy (because no role trust policy allows the sts:AssumeRole action). The named type tells you which policy to fix; some services use other formats. - Encoded failure messages (EC2
UnauthorizedOperation) →aws sts decode-authorization-message, which needssts:DecodeAuthorizationMessage(notes/cli-sdk-policies-guide.md). Testing tools: IAM policy simulator (aws iam simulate-principal-policy, optionally with--resource-policy), IAM Access Analyzer policy validation and access previews.
Developer angle
Bucket policy in the same account: grants the app role access and denies non-TLS requests for everyone:
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "AllowReportsApp", "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/ReportsAppRole" },
"Action": ["s3:GetObject", "s3:PutObject"], "Resource": "arn:aws:s3:::reports-bucket/*" },
{ "Sid": "DenyInsecureTransport", "Effect": "Deny", "Principal": "*", "Action": "s3:*",
"Resource": ["arn:aws:s3:::reports-bucket", "arn:aws:s3:::reports-bucket/*"],
"Condition": { "Bool": { "aws:SecureTransport": "false" } } }
]
}
Check what a role may do before deploying:
aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::111122223333:role/ReportsAppRole \
--action-names s3:GetObject s3:DeleteObject --resource-arns arn:aws:s3:::reports-bucket/2026/q3.csv \
--query 'EvaluationResults[].{action:EvalActionName,decision:EvalDecision}'
- Debug order for
AccessDenied: read the policy type in the message → look for explicit denies (SCP, RCP, bucket or key policy, boundary, session policy) → confirm an allow exists on the right side (both sides if cross-account) → check conditions and the exact resource ARN.
Traps
- Trap: "An
Allowin the bucket policy overrides the explicit deny in my IAM policy." → Reality: an explicit deny anywhere wins. - Trap: "Attach an SCP to grant developers access." → Reality: SCPs and RCPs only limit; grants come from identity or resource policies.
- Trap: "A bucket policy allowing account A is enough for account A's users." → Reality: account A's users also need identity-policy allows.
- Trap: "An IAM allow is enough to use a KMS key." → Reality: the key policy must allow the principal or delegate to the account.
- Trap: "A permissions boundary adds permissions." → Reality: it is a ceiling; nothing passes the boundary unless an identity policy also allows it.
- Trap: "Implicit and explicit deny behave the same." → Reality: a missing allow can be supplied by another policy; an explicit deny can't be overridden.
Practice questions
- An EC2 instance role allows
s3:*on the bucketreports. The bucket policy denies all principals whenaws:SourceVpceisn't the company's VPC endpoint. The application on the instance reaches S3 through a NAT gateway and receivesAccessDenied. What is the reason?- A. Identity-based policies are ignored whenever the bucket has a bucket policy.
- B. The bucket policy must also contain an
Allowstatement that names the role. - C. The explicit deny in the bucket policy applies, and an explicit deny overrides the role's allow.
- D. S3 evaluates only the bucket policy for requests that arrive through a NAT gateway.
- A developer's role in account A must read objects encrypted with SSE-S3 from a bucket in account B. Which TWO policies must allow the access? (Select TWO.)
- A. A service control policy in account A that grants
s3:GetObject. - B. The bucket policy in account B, naming the developer's role or account A as principal.
- C. A permissions boundary on a role in account B.
- D. An identity-based policy on the developer's role that allows
s3:GetObjecton the bucket's objects. - E. A resource control policy in account A that grants
s3:GetObject.
- A. A service control policy in account A that grants
Answers
- C. Requests through the NAT gateway carry no matching
aws:SourceVpce, the deny condition matches, and an explicit deny beats any allow. A and D are false statements about evaluation; B: in the same account the role's identity policy already allows, so a missing bucket allow isn't the issue. - B, D. Cross-account access needs the resource-based policy in B and the identity policy in A. A and E: SCPs and RCPs never grant; C: boundaries in B don't apply to A's role. (With SSE-KMS the key policy would also have to allow.)
12. Dynamic policies and policy variables
Concept
- A policy variable
${…}is a placeholder that AWS replaces with a value from the request context while evaluating, so one policy can serve many principals: per-user home folders, per-tenant prefixes, team buckets. - Common variables:
${aws:username}(IAM users only),${aws:userid},${aws:PrincipalTag/<key>}(tags on users, roles or sessions),${cognito-identity.amazonaws.com:sub}(identity pool identity ID), service keys such as${s3:prefix}where the service provides them. - Variables need
"Version": "2012-10-17"; under the old2008-10-17language (or with noVersion),${aws:username}is read as literal text. - Where they work: in
Resource(only in the part after the fifth colon of the ARN) and inConditionvalues with string or ARN operators. Not inAction, and not with numeric, date, Boolean, IP orNulloperators.
Exam facts
${aws:username}is present only for IAM users; root, role sessions (including EC2, Lambda and ECS roles) and federated users don't have it, so statements built on it never match for them. For roles use${aws:PrincipalTag/…}, session tags or${aws:userid}.aws:useridvalues: IAM user → unique user ID; role session →<role-id>:<role-session-name>; EC2 instance role →<role-id>:<instance-id>; root → account ID.- Missing value:
StringEquals/StringLikedon't match;StringNotEquals/StringNotLikedo match; aResourcecontaining a missing variable matches nothing. Defaults:${aws:PrincipalTag/team, 'company-wide'}; or use…IfExistsoperators. Literal special characters:${*},${?},${$}; multivalued keys can't be used as variables. - S3 home folders need two statements:
s3:ListBucketon the bucket ARN with ans3:prefixcondition, and object actions onbucket/home/${aws:username}/*. - Identity pools:
${cognito-identity.amazonaws.com:sub}is the identity ID (<region>:<guid>), not the user poolsub. DynamoDB per-user items:dynamodb:LeadingKeyswithForAllValues:StringEqualsagainst that identity ID (detailsnotes/dynamodb-guide.md). - ABAC: principal or session tags (from identity pools §8, SAML or OIDC sessions, or role tags) in
${aws:PrincipalTag/<key>}scale better than writing a policy for each user or tenant; keep tag values free of*and?.
Developer angle
Per-user home folders for an IAM group:
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "ListOwnHomeFolder", "Effect": "Allow", "Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::team-files",
"Condition": { "StringLike": { "s3:prefix": ["home/${aws:username}/*"] } } },
{ "Sid": "ManageOwnObjects", "Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::team-files/home/${aws:username}/*" }
]
}
Per-identity items for an identity pool's authenticated role:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:eu-west-1:111122223333:table/UserNotes",
"Condition": { "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": ["${cognito-identity.amazonaws.com:sub}"] } }
}]
}
- Tenant isolation for roles and sessions:
"Resource": "arn:aws:s3:::tenant-data/${aws:PrincipalTag/tenant_id}/*", with the tag set on the role or passed as a session tag (identity pool attributes for access control, SAML, OIDC).
Traps
- Trap: "Use
${aws:username}in the ECS task role's policy." → Reality: the key is absent for role sessions; nothing matches. - Trap: "Leave out
Version, the variable still works." → Reality: without2012-10-17the variable is a literal string. - Trap: "Put a variable in
Action." → Reality: variables only work inResourceand string/ARN conditions. - Trap: "Use the user pool
subin the identity pool role's policy." → Reality: the variable resolves to the identity ID. - Trap: "
s3:ListBucketonbucket/home/${aws:username}/*." → Reality: listing acts on the bucket ARN with ans3:prefixcondition. - Trap: "Map any user attribute to a principal tag." → Reality: user-writable values allow privilege escalation, and
*or?behave as wildcards.
Practice questions
- An ECS task role has a policy that allows
s3:GetObjectonarn:aws:s3:::docs/home/${aws:username}/*. Every request from the task is denied. What is the reason?- A.
aws:usernameisn't present in requests made by role sessions, so the resource never matches. - B. Policy variables are allowed only in the
Conditionelement. - C. The policy needs
"Version": "2008-10-17"to support variables. - D. Amazon S3 doesn't support policy variables in identity-based policies.
- A.
- Each IAM user in the
analystsgroup must list and manage only the objects underhome/<user name>/in the bucketteam-files, using a single group policy. Which TWO statements should the policy contain? (Select TWO.)- A.
s3:ListBucketonarn:aws:s3:::team-fileswith aStringLikecondition ons3:prefixofhome/${aws:username}/*. - B.
s3:ListBucketonarn:aws:s3:::team-files/home/${aws:username}/*. - C.
s3:*onarn:aws:s3:::team-files/*with"Version": "2008-10-17". - D.
s3:GetObject,s3:PutObjectands3:DeleteObjectonarn:aws:s3:::team-files/home/${aws:username}/*. - E.
s3:GetObjectonarn:aws:s3:::team-files/home/*with aStringEqualscondition onaws:userid.
- A.
Answers
- A. Role sessions have no
aws:username, so the variable stays unresolved and the statement matches nothing; use principal or session tags. B: variables also work inResource; C: the old version disables variables; D: S3 supports them. - A, D. Listing is a bucket-level action filtered by
s3:prefix; object actions use the per-user object ARN. B targets the wrong resource for listing; C grants every object and disables the variable; E:aws:useridis an opaque ID, not the folder name, and the resource covers everyone's folders.
13. Managed vs inline policies and passing roles
Concept
- Three kinds of identity-based policy. AWS managed policies are created and updated by AWS (including job-function policies such as
AdministratorAccess,PowerUserAccessandReadOnlyAccess); you can attach them anywhere but not edit them. Customer managed policies are your own standalone, reusable policies with versions and a default version you can roll back to. Inline policies are embedded in a single user, group or role, live and die with it and have no versions. - AWS's guidance: start with AWS managed policies, then move to least-privilege customer managed policies for your workloads; keep inline policies for permissions that must never be attached to anything else.
- Passing a role: whenever you configure a service to act for you later — an EC2 instance profile, a Lambda execution role, an ECS task role, CodeBuild or CodePipeline service roles, a CloudFormation service role, identity pool roles — you hand it a role ARN. The service checks once, at configuration time, that you hold
iam:PassRoleon that role. This stops people from giving a service more power than they have themselves. - Separately, the receiving service must appear as a principal in the role's trust policy (
"Principal": {"Service": "lambda.amazonaws.com"}, …); PassRole on your side doesn't replace that trust.
Exam facts
- ⚠️ Changed: managed policies per role default to 20 (adjustable up to 25); per user 10 (adjustable up to 20); per group 10 (fixed). Role trust policies: 2,048 characters by default, adjustable up to 8,192. Older material may say 10 managed policies per role (maximum 20) and a trust-policy maximum of 4,096 characters.
- Size limits (whitespace not counted): inline policies per identity in total — user 2,048, group 5,120, role 10,240 characters; each managed policy 6,144 characters. Up to 1,500 customer managed policies per account (adjustable to 10,000); 1,000 roles per account (adjustable to 10,000).
- Versions: up to 5 per managed policy; the CLI/API must delete one before creating a sixth (the console prompts you); the default version can't be deleted. Roll back with
SetDefaultPolicyVersion. Anyone withiam:CreatePolicyVersionoriam:SetDefaultPolicyVersioncan change the effective permissions — deny both to freeze a policy. AWS sets the default version of its own managed policies; a new one reaches every principal using the policy immediately, with no action from you. iam:PassRoleis a permission, not an API call, so CloudTrail logs noPassRoleevent: audit the call that received the role (CreateFunction,RunInstances, …). Scope it byResource(role ARN or a path such asrole/lambda/*) and the conditionsiam:PassedToService(e.g.lambda.amazonaws.com) andiam:AssociatedResourceArn; don't control it withiam:ResourceTag(AWS warns the results are unreliable). Pair it withiam:GetRole(andiam:ListRolesfor console role pickers).- Typical failure:
User: arn:aws:sts::111122223333:assumed-role/Testrole/Diego is not authorized to perform: iam:PassRole on resource: arn:aws:iam::111122223333:role/…. PassRole works only within one account; to use a role from another account, the service's account needs its own role that can assume it. Naming a service-linked role explicitly also needs PassRole; letting the service use its default service-linked role doesn't. - One role per workload: each EC2 application (instance profile,
notes/ec2-mastery-guide.md§1), Lambda function (execution role), ECS task (task role) and CodeBuild project (service role) gets its own least-privilege role; generate starting policies from CloudTrail activity with IAM Access Analyzer. - ⚠️ Changed: current ECS-optimized AMIs support task roles out of the box;
ECS_ENABLE_TASK_IAM_ROLE=true(andECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=truefor host networking) is needed only on non-optimized AMIs and external instances, and Fargate tasks always use task roles (detailsnotes/containers-guide.md). Older material may say you must always set the variable (the course does). - ⚠️ Changed: humans should use federation with temporary credentials (IAM Identity Center), and workloads outside AWS should use IAM Roles Anywhere or OIDC/SAML federation; long-term access keys only where roles can't be used (same rule as the ec2 row in
notes/current-facts.md). Older material may say keys are acceptable on a personal computer or an on-premises server (the course does).
Developer angle
Customer managed policy lifecycle and an inline policy for comparison:
aws iam create-policy --policy-name orders-rw --policy-document file://orders-rw.json
aws iam attach-role-policy --role-name orders-fn-role --policy-arn arn:aws:iam::111122223333:policy/orders-rw
aws iam create-policy-version --policy-arn arn:aws:iam::111122223333:policy/orders-rw \
--policy-document file://orders-rw-v2.json --set-as-default
aws iam set-default-policy-version --policy-arn arn:aws:iam::111122223333:policy/orders-rw --version-id v1 # roll back
aws iam put-role-policy --role-name orders-fn-role --policy-name kms-decrypt \
--policy-document file://kms-decrypt.json # inline: lives and dies with the role
What a developer needs to deploy functions with pre-approved execution roles:
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "ManageOrderFunctions", "Effect": "Allow",
"Action": ["lambda:CreateFunction", "lambda:UpdateFunctionConfiguration"],
"Resource": "arn:aws:lambda:eu-west-1:111122223333:function:orders-*" },
{ "Sid": "PassOnlyLambdaRoles", "Effect": "Allow", "Action": "iam:PassRole",
"Resource": "arn:aws:iam::111122223333:role/lambda/orders-*",
"Condition": { "StringEquals": { "iam:PassedToService": "lambda.amazonaws.com" } } },
{ "Sid": "SeeTheRoles", "Effect": "Allow", "Action": "iam:GetRole",
"Resource": "arn:aws:iam::111122223333:role/lambda/orders-*" }
]
}
iam:GetRolesits in its own statement becauseiam:PassedToServiceexists only forPassRolerequests; a shared statement would never matchGetRole. The execution role itself trusts the service:"Principal": {"Service": "lambda.amazonaws.com"},"Action": "sts:AssumeRole".
Traps
- Trap: "Use an inline policy so 30 roles can share it." → Reality: inline is one-to-one; a customer managed policy is the reusable, versioned option.
- Trap: "Edit the AWS managed policy to add our bucket." → Reality: AWS managed policies are read-only; copy into a customer managed policy.
- Trap: "Grant
iam:PassRoleon*to unblock the pipeline." → Reality: scope to the role ARNs andiam:PassedToService, or anyone can hand an admin role to a service. - Trap: "Find who passed the role in CloudTrail
PassRoleevents." → Reality: there are none; look at the receiving API call. - Trap: "Fix a PassRole denial by editing the execution role's trust policy." → Reality: the missing permission belongs to the caller; the trust policy controls who may assume the role.
- Trap: "Share one broad role across all Lambda functions." → Reality: one least-privilege role per function or workload.
Practice questions
- A developer runs
aws lambda create-functionwith an existing execution role and getsAccessDenied … is not authorized to perform: iam:PassRole on resource: arn:aws:iam::111122223333:role/orders-fn-role. Which change fixes the error with least privilege?- A. Attach
AWSLambda_FullAccessto the execution role. - B. Add
lambda.amazonaws.comto the trust policy of the developer's own role. - C. Allow the developer
iam:PassRoleon that role's ARN withiam:PassedToServiceset tolambda.amazonaws.com. - D. Allow the developer
sts:AssumeRoleon the execution role.
- A. Attach
- A platform team maintains one set of permissions used by 30 IAM roles. The team needs a change history and a way to undo a bad change within minutes. Which TWO practices meet these requirements? (Select TWO.)
- A. Copy the permissions into an inline policy on each role.
- B. Store the permissions in one customer managed policy attached to all 30 roles.
- C. Edit the AWS managed
PowerUserAccesspolicy to add the permissions. - D. Keep a new policy version for every change, up to 10 versions per policy.
- E. Roll back by setting a previous policy version as the default version.
Answers
- C. The caller needs permission to pass exactly this role, narrowed to Lambda. A changes the role's own permissions, not the caller's; B confuses the developer's role with the execution role's trust; D lets the developer act as the role, which isn't what creating a function checks.
- B, E. A customer managed policy keeps versions, and switching the default version undoes a change for every attached role at once. A has no versions and must be edited 30 times; C: AWS managed policies can't be edited; D: the limit is 5 versions.
14. AWS Directory Service
Concept
- Microsoft Active Directory (AD) is the Windows directory service: domain controllers hold the users, groups and computers of a domain, authenticate them with Kerberos or LDAP and push group policies; domains can be nested into trees, and related trees make up a forest. Many Windows and enterprise workloads require it.
- AWS Directory Service brings AD to AWS in several shapes:
- AWS Managed Microsoft AD (Standard edition up to about 30,000 directory objects, Enterprise up to about 500,000): a real Windows Server 2019 AD that AWS runs as a highly available pair of domain controllers in two Availability Zones of your VPC. Users live in it; it can trust your on-premises or self-managed AD; it supports RADIUS-based MFA, LDAPS and schema extensions.
- AD Connector: a proxy that forwards authentication to your existing on-premises AD and keeps no directory data in AWS; users stay where they are; it supports RADIUS-based MFA and seamless domain join of EC2 instances to the on-premises domain.
- Simple AD: a low-cost, Samba 4–based AD-compatible standalone directory for basic needs (user accounts, groups, domain join, Kerberos SSO, group policies; Small ≈ 500 users, Large ≈ 5,000); no trusts, no MFA, no LDAPS, no schema extensions, no IAM Identity Center. ⚠️ Changed: closed to new customers since 30 July 2026; existing customers keep full functionality and can still create Simple AD directories, new customers choose AWS Managed Microsoft AD or AD Connector. Older material may say anyone can pick Simple AD as the cheap option.
- AWS Managed Microsoft AD (Hybrid Edition) (30 July 2025): extends an existing self-managed AD into AWS so the same credentials and group memberships work in both, without synchronising identities.
- Directory Service is for workforce and Windows-style workloads. Customer sign-in for your apps is Cognito; developer and admin access to AWS accounts is IAM Identity Center (which can use Managed Microsoft AD or AD Connector as its identity source).
Exam facts
| Need | Choose |
|---|---|
| Real AD in AWS for AD-aware apps, SharePoint, trusts with on-premises AD, or RDS for SQL Server Windows auth without running your own domain controllers | AWS Managed Microsoft AD |
| Keep using on-premises AD for WorkSpaces, console sign-in or EC2 domain join, with no directory data in AWS | AD Connector |
| Small, cheap standalone directory with basic AD features or LDAP for Linux apps | Simple AD (existing customers only); new customers: Managed Microsoft AD Standard |
| Extend a self-managed AD into AWS with the same identities | Managed Microsoft AD Hybrid Edition |
| Sign-up and sign-in for app customers, social logins | Amazon Cognito (not Directory Service) |
- ⚠️ Changed: besides the three classic types there is AWS Managed Microsoft AD (Hybrid Edition, 30 July 2025) to extend an existing self-managed AD into AWS, and Simple AD is no longer open to new customers (30 July 2026). Older material may say there are only Managed Microsoft AD, AD Connector and Simple AD, all open to everyone (the course does).
- MFA with RADIUS: AWS Managed Microsoft AD and AD Connector (Simple AD has none). Trusts: AWS Managed Microsoft AD only (Simple AD can't join or trust another domain). RDS for SQL Server with Windows authentication: AWS Managed Microsoft AD, or a self-managed AD (on premises, on EC2 or in another cloud) that the DB instance joins directly; AD Connector and Simple AD aren't supported.
- Seamless domain join of EC2 instances: all types (AD Connector joins them to the on-premises domain).
- AD Connector needs network connectivity to the on-premises domain controllers and a service account in your AD; it forwards every sign-in to them (no directory synchronisation, no copy in AWS).
- AWS handles monitoring, daily snapshots and recovery for Managed Microsoft AD and Simple AD.
Developer angle
Creating directories from the CLI (network prerequisites are out of scope):
# Real AD managed by AWS (Standard edition)
aws ds create-microsoft-ad --name corp.example.com --password 'Adm1n-Passw0rd-2026' --edition Standard \
--vpc-settings VpcId=vpc-0abc1234def567890,SubnetIds=subnet-0aaa1111bbb22222c,subnet-0ddd3333eee44444f
# Proxy to the on-premises domain (users stay on premises)
aws ds connect-directory --name corp.example.com --password 'Svc-Passw0rd-2026' --size Small \
--connect-settings VpcId=vpc-0abc1234def567890,SubnetIds=subnet-0aaa1111bbb22222c,subnet-0ddd3333eee44444f,CustomerDnsIps=10.0.0.10,CustomerUserName=svc-adconnector
aws ds describe-directories --query 'DirectoryDescriptions[].{Id:DirectoryId,Type:Type,Name:Name,Stage:Stage}'
- Applications usually reach AD through LDAP(S) or Kerberos, not through AWS APIs; your AWS-side work is choosing the directory type, wiring IAM Identity Center or WorkSpaces to it, and joining instances.
Traps
- Trap: "Simple AD with a trust to the on-premises forest." → Reality: Simple AD supports no trusts; use AWS Managed Microsoft AD.
- Trap: "AD Connector stores a copy of the users in AWS." → Reality: it only proxies requests to on-premises domain controllers.
- Trap: "Simple AD for RADIUS MFA." → Reality: Simple AD has no MFA; AD Connector and Managed Microsoft AD do.
- Trap: "Directory Service for a consumer mobile app's sign-up." → Reality: Cognito user pool.
- Trap: "AD Connector for RDS for SQL Server Windows authentication." → Reality: AWS Managed Microsoft AD.
- Trap: "A new AWS customer picks Simple AD as the cheapest directory." → Reality: Simple AD is closed to new customers; use Managed Microsoft AD (Standard) or AD Connector.
Practice questions
- A company wants Amazon WorkSpaces users to sign in with their existing on-premises Active Directory credentials and wants EC2 Windows instances to join the on-premises domain. No directory data may be stored in AWS, and operational overhead must be minimal. Which option meets these requirements?
- A. Simple AD with a trust relationship to the on-premises domain.
- B. AWS Managed Microsoft AD with all users re-created in the new domain.
- C. An Amazon Cognito user pool federated with the on-premises AD through SAML.
- D. AD Connector pointing at the on-premises domain controllers.
- A company has run Simple AD for years for its Windows and Linux EC2 instances. Which TWO new requirements would force a move to AWS Managed Microsoft AD? (Select TWO.)
- A. A trust relationship with a partner's existing Active Directory.
- B. Joining Linux EC2 instances to the domain.
- C. RADIUS-based multi-factor authentication for directory users.
- D. Kerberos-based single sign-on for domain users.
- E. Group policies for the Windows instances.
Answers
- D. AD Connector proxies authentication and domain join to the existing AD and stores no directory data. A: Simple AD can't trust other domains; B duplicates the directory in AWS; C: WorkSpaces and domain join need AD, not a user pool.
- A, C. Trusts and RADIUS MFA are beyond Simple AD. B, D and E are basic AD features that Simple AD already provides.
Quick decision rules
- If you see app users who need sign-up, sign-in or social/SAML login, think Cognito user pool.
- If you see temporary AWS credentials for app users or guests (direct S3 or DynamoDB access from the device), think Cognito identity pool.
- If you see a backend that must prove a Cognito JWT is genuine, think signature check with the pool's JWKS plus
exp,iss, audience andtoken_use(aws-jwt-verify). - If you see API authorization based on scopes, think access token; identity claims, think ID token.
- If you see "Unable to verify secret hash for client", think missing or wrong
SECRET_HASH(client has a secret). - If you see users who must sign in without a confirmation code, think pre sign-up trigger with
autoConfirmUser. - If you see extra claims or scopes in access tokens, think pre token generation trigger
V2_0(Essentials or Plus). - If you see moving users from a legacy store without password resets, think migrate user trigger +
USER_PASSWORD_AUTH. - If you see a trigger that works in the console but not after a CloudFormation deployment, think missing Lambda resource-based permission for
cognito-idp.amazonaws.com. - If you see a custom login domain, think ACM certificate in us-east-1 plus a DNS alias to the Cognito-managed CloudFront distribution.
- If you see a browser app that needs refresh tokens securely, think authorization code grant with PKCE.
- If you see service-to-service tokens from Cognito, think client credentials grant (secret, custom scopes, access token only).
- If you see risk-based MFA or blocking breached passwords, think Plus plan threat protection (audit first, password flows for breach checks).
- If you see a web app behind an ALB that must require sign-in without app code, think
authenticate-cognito/authenticate-oidcon an HTTPS listener. - If you see a target that must trust the user identity from the ALB, think verify the ES256-signed
x-amzn-oidc-dataheader and itssigner. - If you see guests failing to call a service their role allows, think enhanced-flow scope-down policy.
- If you see role choice per user pool group, think group role + identity pool "choose role from token".
- If you see access to another account's resources without long-term keys, think role in that account + trust policy +
sts:AssumeRolepermission for the caller. - If you see a vendor that assumes roles in many customer accounts, think
sts:ExternalIdcondition. - If you see a session longer than 1 hour failing, think the role's
MaxSessionDuration(1–12 h) or role chaining (1 h cap). - If you see MFA-protected CLI work with an IAM user, think
aws sts get-session-token --serial-number --token-code. - If you see a Deny-without-MFA policy that misses access-key calls, think
BoolIfExistsinstead ofBool. - If you see an explicit deny anywhere in the evaluated policies, think the request is denied, whatever else allows it.
- If you see cross-account access failing, think both the identity policy and the resource-based policy (or trust policy) must allow.
- If you see one policy for many users' folders or tenants, think policy variables (
${aws:username}for IAM users, principal tags for roles). - If you see
iam:PassRolein anAccessDenied, think grant PassRole on that role ARN to the caller, scoped withiam:PassedToService. - If you see reusable permissions with history and rollback, think customer managed policy and its default version.
- If you see on-premises AD users needing AWS app access with no data in AWS, think AD Connector; trusts or RDS for SQL Server, think AWS Managed Microsoft AD.
Changed vs older material
Learn the Current column; older practice material may still show the Older material may say value. After you confirm them, these rows (with verification dates) also live in notes/current-facts.md.
| Topic | Older material may say | Current (verified 2026-10-12) | Source |
|---|---|---|---|
| §1 · Cognito Sync | Use Cognito Sync to sync user data across devices | No longer open to new customers; use AWS AppSync or DynamoDB | AWS docs |
| §3 · Refresh tokens | Refresh tokens are never rotated; refresh with REFRESH_TOKEN_AUTH |
Optional rotation per app client (RefreshTokenRotation, grace ≤ 60 s) through GetTokensFromRefreshToken or the token endpoint; REFRESH_TOKEN_AUTH not allowed with rotation; expiry window unchanged |
AWS docs |
| §4 · Pre token generation trigger | Adds or removes claims in ID tokens only | V1_0 ID token (+ group override); V2_0 ID and access tokens (claims, scopes); V3_0 adds M2M tokens; V2/V3 need Essentials or Plus |
AWS docs |
| §5 · Hosted UI | Hosted sign-in pages customisable only with a logo and a CSS file | Managed login (22 Nov 2024): branding editor, passkeys, localisation, Essentials/Plus; classic hosted UI still in all plans and the only option in Lite; one branding version per domain | AWS docs |
| §5 · Custom domain setup | Defined in the console's "App integration" section | Console Branding → Domain; API CreateUserPoolDomain with CustomDomainConfig; certificate still in us-east-1 |
AWS docs |
| §6 · User pool pricing model | One per-user price plus an optional advanced security features add-on | Feature plans Lite / Essentials / Plus since 22 Nov 2024; Essentials is the default for new pools | AWS docs |
| §6 · Advanced security features | Separately priced add-on; compromised-credential blocking shown as a basic user pool feature | Now in Essentials or Plus; threat protection (adaptive auth, compromised credentials, log export) is Plus only; AUDIT/ENFORCED requires tier PLUS |
AWS docs |
| §7 · ALB user-claims headers | x-amzn-oidc-accesstoken, x-amzn-oidc-identity and x-amzn-oidc-data are equally usable |
Only x-amzn-oidc-data is signed (ES256; check signer); the other two are legacy unsigned headers |
AWS docs |
| §7 · Token validation at the ALB | The ALB only authenticates browser users; APIs validate JWTs in code or API Gateway | jwt-validation rule action (12 Nov 2025) verifies RS256 JWTs (iss, exp, up to 10 claims) on HTTPS listeners |
AWS docs |
| §8 · STS call behind identity pools | "AssumeRoleForWebIdentity" | sts:AssumeRoleWithWebIdentity; Cognito makes the call itself in the enhanced flow (GetCredentialsForIdentity) |
AWS docs |
| §9 · STS credential lifetime | Temporary credentials last 15 minutes to 1 hour | AssumeRole*: default 1 h, 900 s up to the role's maximum (1–12 h); role chaining ≤ 1 h; GetSessionToken / GetFederationToken: 15 min–36 h, default 12 h (root ≤ 1 h) |
AWS docs |
§9 · AssumeRoleWithWebIdentity guidance |
AWS recommends against calling it; use Cognito identity pools instead | Cognito recommended for mobile apps; calling it directly is a documented best-practice path for workloads with an OIDC IdP (e.g. CI/CD) | AWS docs |
| §9 · STS API set | Seven operations (AssumeRole, AssumeRoleWithSAML, AssumeRoleWithWebIdentity, GetSessionToken, GetFederationToken, GetCallerIdentity, DecodeAuthorizationMessage) | Also AssumeRoot (privileged root task in a member account, ≤ 900 s, Regional endpoint) and GetAccessKeyInfo |
AWS docs |
| §9 · STS global endpoint | sts.amazonaws.com is always served from us-east-1 |
Served in the originating Region for Regions enabled by default (Amazon DNS); opt-in Regions and other resolvers still go to us-east-1; Regional endpoints recommended | AWS docs |
| §11 · Policy types in evaluation | Explicit deny → allow → implicit deny over identity and resource policies (plus SCPs, boundaries, session policies) | Adds resource control policies (13 Nov 2024; launched for 5 services, now dozens): explicit deny → RCPs → SCPs → resource-based → identity-based → permissions boundary → session policy | AWS docs |
| §13 · IAM managed-policy and trust-policy quotas | 10 managed policies per role (max 20); trust policy max 4,096 characters | 20 per role (adjustable to 25); users 10 (to 20); groups 10 (fixed); trust policy 2,048 by default (adjustable to 8,192) | AWS docs |
| §13 · ECS task roles | Always set ECS_ENABLE_TASK_IAM_ROLE=true |
Current ECS-optimized AMIs support task roles out of the box; the variable is for non-optimized AMIs and external instances; Fargate always uses task roles | AWS docs |
| §13 · Long-term access keys | Acceptable on a personal computer or an on-premises server | Temporary credentials for humans (IAM Identity Center, federation) and workloads (roles, IAM Roles Anywhere, OIDC/SAML); keys only where roles are impossible | AWS docs |
| §14 · Directory types | Managed Microsoft AD, AD Connector, Simple AD | Plus AWS Managed Microsoft AD (Hybrid Edition, 30 Jul 2025) to extend a self-managed AD into AWS | AWS docs |
| §14 · Simple AD availability | Simple AD is the low-cost choice for any account | Closed to new customers since 30 Jul 2026; existing customers keep full functionality and can still create directories; new customers use Managed Microsoft AD or AD Connector | AWS docs |