Amazon Cognito is AWS's managed customer identity service, split into two purpose-built halves that the exam constantly tests as a pair: User Pools answer "who is this user?" (authentication), and Identity Pools answer "what can this user do in AWS?" (authorization via temporary credentials). This module builds the mental model for how tokens flow between them, then layers on the exam-reasoning patterns DVA-C02 tests around JWTs, federation, and API Gateway integration.
Almost every Cognito question tests one of three things: (1) can you tell whether a scenario needs authentication (User Pool) or AWS authorization/temporary credentials (Identity Pool) — often both together, (2) do you know the difference between the ID token, access token, and refresh token well enough to spot the one being used wrong, or (3) can you identify the right integration point — API Gateway User Pool authorizer for validating a JWT, or Identity Pool + STS for direct AWS service calls.
| Exam Domain | Where Cognito Shows Up |
|---|---|
| Domain 2 — Security | The centerpiece — implementing authentication/authorization for applications, choosing User Pools vs Identity Pools, JWT validation, IAM role mapping for federated identities |
| Domain 1 — Development with AWS Services | Integrating the Cognito SDK/Amplify into application code, calling User Pool APIs (sign-up, sign-in, token refresh), handling Hosted UI redirects |
| Domain 3 — Deployment | Configuring app clients, callback/redirect URLs, and OAuth scopes correctly per environment (dev/test/prod) as part of a deployment pipeline |
| Domain 4 — Troubleshooting and Optimization | Diagnosing 401s from an expired or wrong-type token, misconfigured API Gateway authorizers, or IAM role-mapping failures on an Identity Pool |
Cognito is one of the most heavily-tested services on DVA-C02 because it sits squarely in the "developer implements security" theme the exam is built around — prioritize it accordingly.
User submits credentials (or federates via a social/SAML/OIDC IdP) through the User Pool's Hosted UI or SDK. The User Pool validates them, runs any configured Lambda triggers, and issues three JWTs: ID token, Access token, Refresh token.
The client app stores the tokens (securely — never in plain localStorage for production-grade apps) and uses the ID token to know who the user is, or the access token to call authorized backend/resource-server APIs.
If the app needs to call AWS services directly, it presents the User Pool token to an Identity Pool (GetId → GetCredentialsForIdentity), which internally calls sts:AssumeRoleWithWebIdentity against the mapped IAM role.
The app uses the short-lived temporary AWS credentials to call AWS services directly (e.g. s3:PutObject) — no backend proxy required, keeping operational overhead low.
For calls to your own backend APIs (not raw AWS APIs), API Gateway's Cognito User Pool authorizer validates the JWT's signature, issuer, audience, and expiration before allowing the request through — no custom Lambda authorizer code needed.
When the ID/access token expires (default 1 hour), the app uses the still-valid refresh token to silently obtain a new pair without forcing the user to log in again.
Exam appearance probability: HIGH
The building blocks the exam actually asks about: what each piece stores, what it issues, and where developers most often trip over the details.
custom:token_use=idtoken_use=accessheader.payload.signature) signed with the User Pool's private key; you validate them against the pool's public JWKS endpoint.GetId then GetCredentialsForIdentity against the Identity Pool, which internally performs sts:AssumeRoleWithWebIdentity against the mapped role's trust policy — the credentials returned are temporary and scoped exactly to that role's permissions.GetOpenIdTokenForDeveloperIdentity directly — bypassing User Pools and social/SAML IdPs entirely. Rarely the primary answer, but a recognizable distractor/edge case on the exam.email attribute) into standard or custom User Pool attributes on first federation.custom:department) — useful input for Pre Token Generation trigger logic or Identity Pool role-mapping rules.Requirement → Keywords → Expected Answer → why every distractor fails. Written in AWS scenario style ("A company needs to...", "What is the MOST appropriate solution?").
Amazon Cognito User Pool
| Distractor | Why it's wrong |
|---|---|
| IAM Identity Center | Built for workforce/employee access to AWS accounts and business apps, not millions of customer-facing app end users |
| IAM users | Not designed for self-service sign-up at scale, no built-in hosted sign-in UI, and each IAM user is a security-sensitive AWS principal |
| Cognito Identity Pool | Doesn't authenticate anyone — it only exchanges an already-trusted identity for AWS credentials |
Cognito Identity Pool federated with a User Pool → temporary credentials via STS
| Distractor | Why it's wrong |
|---|---|
| Embed IAM user access keys in the app | Long-lived credentials shipped inside a mobile app are a severe security anti-pattern — never the correct answer |
| API Gateway + Lambda proxy for every upload | Adds a backend hop and operational overhead (scaling, monitoring, cost) that direct-to-S3 credentials avoid entirely |
| Backend-generated pre-signed URLs per upload | Works, but still requires a backend call for every single upload — Identity Pool credentials remove that dependency, giving the lowest operational overhead |
API Gateway Cognito User Pool authorizer (REST API) / JWT authorizer (HTTP API)
| Distractor | Why it's wrong |
|---|---|
| Lambda authorizer | Works, but requires you to write and maintain custom validation code — more operational overhead than the native, managed authorizer |
| IAM authorization (SigV4) | Designed for AWS-principal callers signing requests with AWS credentials, not for end users who authenticated via Cognito |
| API keys | Identify and throttle a calling application/plan — they do not authenticate an individual user |
Cognito User Pool with a SAML identity provider configured, exposed via Hosted UI
| Distractor | Why it's wrong |
|---|---|
| Social IdP (Google/Facebook) | Wrong protocol entirely for corporate AD/ADFS federation |
| Identity Pool alone, no User Pool | Possible in theory, but doesn't provide the managed interactive sign-in/redirect experience the Hosted UI gives you with a User Pool |
| IAM Identity Center | Valid for federating workforce access into the AWS Console/CLI, but not the mechanism for authenticating end users into a custom application via Cognito |
Pre Token Generation Lambda trigger
| Distractor | Why it's wrong |
|---|---|
| Post Authentication trigger | Fires after authentication succeeds, but cannot modify the contents of the tokens about to be issued |
| Custom attributes alone | Custom attributes are stored user data — they aren't automatically injected as token claims without a Pre Token Generation trigger doing the mapping |
| Pre Sign-up trigger | Runs before the account even exists, far too early to influence a specific sign-in's token contents |
Migrate User (User Migration) Lambda trigger
| Distractor | Why it's wrong |
|---|---|
| Bulk import via CSV | Requires an upfront batch job for every user — explicitly what the requirement says to avoid |
| Federation via SAML/OIDC | Would require the legacy directory to speak SAML/OIDC; overkill for a straightforward username/password validation migration |
| Post Confirmation trigger | Fires after confirmation of a Cognito-native account — not the trigger involved in validating credentials against the legacy store during migration |
Cognito Identity Pool unauthenticated (guest) role
| Distractor | Why it's wrong |
|---|---|
| User Pool with relaxed confirmation rules | Still requires a sign-up flow — not truly "unauthenticated" |
| Public S3 bucket policy / anonymous access | Grants blanket access with no per-session credential scoping, expiry, or ability to later differentiate guest vs authenticated permissions |
| Lambda authorizer that allows everything | Doesn't produce scoped, temporary AWS credentials usable for direct SDK calls the way an Identity Pool guest role does |
Send the Access token instead — it carries token_use=access and OAuth scopes; the ID token does not
| Distractor | Why it's wrong |
|---|---|
| Keep using the ID token | The ID token carries identity claims (who the user is), not authorization scopes — resource servers expecting scopes will reject or mishandle it |
| Send the refresh token to the API | Refresh tokens are never presented to resource servers — they exist solely to obtain new ID/Access tokens |
| Request a fresh token from the Identity Pool | Identity Pools don't issue application-level authorization tokens — they issue AWS credentials, an entirely different credential type |
Authorization header → API Gateway validates before invoking the backend integrationcognito-identity.amazonaws.com, conditioned on cognito-identity.amazonaws.com:aud = the Identity Pool ID (and :amr for authenticated vs unauthenticated)GetCredentialsForIdentity internally calls sts:AssumeRoleWithWebIdentity against the mapped IAM role${cognito-identity.amazonaws.com:sub}) — no backend proxy requiredA photo-sharing mobile app needs authenticated users to upload images straight to S3 without routing every upload through a backend server.
s3:PutObject/s3:GetObject restricted to a per-user S3 key prefix (using a policy variable tied to the Cognito identity ID).A web application needs a secured backend API, without managing servers or writing custom auth-validation code.
Ask: does the client need to call an AWS service API directly (S3, DynamoDB, Kinesis, etc.)? If yes → you need an Identity Pool to mint temporary AWS credentials. If the client only ever talks to your own backend/API Gateway, which then talks to AWS services using its own execution role, an Identity Pool is unnecessary — a User Pool alone (for authentication) plus an API Gateway authorizer (for authorization at the API layer) is sufficient.
| Aspect | User Pools | Identity Pools |
|---|---|---|
| Answers | "Who is this user?" | "What AWS resources can this user touch?" |
| Function | Authentication — user directory, sign-up/sign-in | Authorization — token-to-AWS-credential exchange |
| Output | JWTs: ID token, Access token, Refresh token | Temporary AWS credentials (access key, secret key, session token) via STS |
| Typical consumer | Your own backend / API Gateway authorizer | AWS SDK calls made directly from the client (S3, DynamoDB, etc.) |
| Supports guests? | No — every principal is a registered/confirmed user | Yes — built-in unauthenticated (guest) role |
| Needs the other? | Optional — can stand alone for pure app authentication | Optional — can federate directly with a social/SAML/OIDC IdP, but is most often layered on top of a User Pool |
| Misconception | Reality |
|---|---|
| "The ID token and Access token are interchangeable" | ID token = identity claims (who); Access token = authorization scopes (what they can do). Sending the wrong one to a resource server is a classic bug and exam trap |
| "An Identity Pool authenticates the user" | Identity Pools never authenticate anyone — they only exchange an already-trusted identity for temporary AWS credentials |
| "Cognito Identity Pools hand out permanent IAM access keys" | They always issue short-lived, temporary STS credentials tied to a mapped IAM role — never static long-lived keys |
| "You must always use both a User Pool and an Identity Pool together" | A User Pool can stand alone for pure application authentication; an Identity Pool is only needed when the client must call AWS service APIs directly |
| "API Gateway needs a Lambda authorizer to work with Cognito" | API Gateway has a native Cognito User Pool authorizer (REST) / JWT authorizer (HTTP) that validates tokens with zero custom code |
| "Custom attributes automatically show up as token claims" | They don't, by default — a Pre Token Generation Lambda trigger is required to inject custom claims into issued tokens |
| "Refresh tokens are sent to protected APIs like access tokens" | Refresh tokens are only ever exchanged with the User Pool token endpoint to mint new ID/Access tokens — never presented to a resource server |
"I need users to sign up and log in" → User Pool. Full stop.
"I need logged-in users to call AWS services directly" → Identity Pool, usually layered on a User Pool as the trusted IdP.
Three tokens, three distinct jobs — never assume they're interchangeable.
Two entirely different problems that both start with "how do I secure this call" — don't reach for an Identity Pool just to protect a backend API endpoint.
Three self-contained exercises that turn the token flow from the Overview tab into real AWS CLI v2 calls. Everything runs in eu-west-1. Work through them in order — Exercise 1 produces the tokens that Exercises 2 and 3 consume.
Create a User Pool and app client, provision a ready-to-use test user without an email/SMS confirmation round-trip, authenticate via the CLI, and inspect the raw claims inside the ID and Access tokens to see the difference the guide describes conceptually.
aws cognito-idp create-user-pool \
--pool-name dva-lab-pool \
--region eu-west-1 \
--auto-verified-attributes email \
--policies '{"PasswordPolicy":{"MinimumLength":8,"RequireUppercase":true,"RequireLowercase":true,"RequireNumbers":true,"RequireSymbols":false}}'
JSON output containing "UserPool": { "Id": "eu-west-1_AbCdEfGhI", ... }. Copy this pool ID — every later command needs it.
initiate-auth doesn't need a SECRET_HASH).
aws cognito-idp create-user-pool-client \ --user-pool-id eu-west-1_AbCdEfGhI \ --client-name dva-lab-client \ --no-generate-secret \ --explicit-auth-flows ALLOW_USER_PASSWORD_AUTH ALLOW_REFRESH_TOKEN_AUTH \ --region eu-west-1
"UserPoolClient": { "ClientId": "1a2b3c4d5e6f7g8h9i0j1k2l3m", ... }.
admin-create-user + admin-set-user-password --permanent skips the verification-code step entirely — the fastest way to get a confirmed user for a lab.
aws cognito-idp admin-create-user \ --user-pool-id eu-west-1_AbCdEfGhI \ --username dva-lab-user \ --user-attributes Name=email,Value=dva-lab-user@example.com Name=email_verified,Value=true \ --message-action SUPPRESS \ --region eu-west-1 aws cognito-idp admin-set-user-password \ --user-pool-id eu-west-1_AbCdEfGhI \ --username dva-lab-user \ --password 'DvaLab2026!' \ --permanent \ --region eu-west-1User status becomes
CONFIRMED (check with admin-get-user) — no verification email/SMS round-trip needed.
aws cognito-idp initiate-auth \ --auth-flow USER_PASSWORD_AUTH \ --client-id 1a2b3c4d5e6f7g8h9i0j1k2l3m \ --auth-parameters USERNAME=dva-lab-user,PASSWORD='DvaLab2026!' \ --region eu-west-1A JSON
AuthenticationResult block with IdToken, AccessToken, and RefreshToken — three long dot-separated strings. Save the ID and Access tokens as ID_TOKEN and ACCESS_TOKEN — later labs reuse them.
base64 -d one-liner can fail — pad it first, or use Python, which handles this cleanly.
ID_TOKEN="<paste IdToken here>"
ACCESS_TOKEN="<paste AccessToken here>"
decode_jwt() {
python3 -c "
import sys, json, base64
p = sys.argv[1].split('.')[1]
p += '=' * (-len(p) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(p)), indent=2))
" "$1"
}
decode_jwt "$ID_TOKEN"
decode_jwt "$ACCESS_TOKEN"
The ID token's JSON shows "token_use": "id" plus identity claims (email, email_verified, cognito:username). The Access token's JSON shows "token_use": "access", a "client_id" claim, and a "scope" claim — and notably has no email claim, because it isn't meant to carry identity data.
DVA-C02 loves the scenario "a resource server rejects a Cognito token — why?" Seeing with your own eyes that the ID token has no scope claim and the Access token has no email claim makes the "wrong token sent to the wrong consumer" trap (covered in Components 1.3 and Exam Logic) permanently obvious instead of a memorized rule.
Attach the User Pool as a native JWT authorizer on an API Gateway HTTP API route, then call the route with and without a valid Authorization header to see the 401 vs 200 behavior the Integrations tab describes.
aws apigatewayv2 create-api \ --name dva-lab-http-api \ --protocol-type HTTP \ --region eu-west-1
"ApiId": "abc123xyz4" and an "ApiEndpoint": "https://abc123xyz4.execute-api.eu-west-1.amazonaws.com". Save both.
aws apigatewayv2 create-integration \ --api-id abc123xyz4 \ --integration-type HTTP_PROXY \ --integration-method GET \ --integration-uri https://httpbin.org/anything \ --payload-format-version 1.0 \ --region eu-west-1
"IntegrationId": "i1j2k3l4".
aud claim (ID tokens) or the client_id claim (Access tokens).
aws apigatewayv2 create-authorizer \ --api-id abc123xyz4 \ --authorizer-type JWT \ --identity-source '$request.header.Authorization' \ --name dva-lab-cognito-jwt-authorizer \ --jwt-configuration Audience=1a2b3c4d5e6f7g8h9i0j1k2l3m,Issuer=https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_AbCdEfGhI \ --region eu-west-1
"AuthorizerId": "auth9z8y7x".
$default stage.
aws apigatewayv2 create-route \ --api-id abc123xyz4 \ --route-key "GET /secure" \ --target integrations/i1j2k3l4 \ --authorization-type JWT \ --authorizer-id auth9z8y7x \ --region eu-west-1 aws apigatewayv2 create-stage \ --api-id abc123xyz4 \ --stage-name '$default' \ --auto-deploy \ --region eu-west-1Route and stage created; the API endpoint from Step 1 is live at
/secure within a few seconds (auto-deploy).
curl -i https://abc123xyz4.execute-api.eu-west-1.amazonaws.com/secure
HTTP/2 401 with a body like {"message":"Unauthorized"} — the JWT authorizer rejects the request before the integration ever runs.
curl -i https://abc123xyz4.execute-api.eu-west-1.amazonaws.com/secure \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
HTTP/2 200 with the httpbin echo response body — the authorizer validated the token's signature, issuer, and client_id-as-audience, then let the request through with zero custom authorization code.
This is the "validate a JWT with no custom Lambda code" scenario from Exam Logic, made concrete. Also notice you authenticated with USER_PASSWORD_AUTH (a User Pool concern) and authorized the API call with a JWT authorizer (also a User Pool concern) — no Identity Pool was involved anywhere in this lab, because the client never called an AWS service API directly. That's the exact discriminator the Integrations tab's "Identity Pool Necessity Test" trains you to spot.
Create an Identity Pool that trusts the User Pool as an authentication provider, walk through GetId → GetCredentialsForIdentity by hand, and use the resulting temporary credentials to prove they are real, scoped, short-lived AWS credentials — not another JWT.
aws cognito-identity create-identity-pool \ --identity-pool-name dva_lab_identity_pool \ --no-allow-unauthenticated-identities \ --cognito-identity-providers ProviderName=cognito-idp.eu-west-1.amazonaws.com/eu-west-1_AbCdEfGhI,ClientId=1a2b3c4d5e6f7g8h9i0j1k2l3m,ServerSideTokenCheck=false \ --region eu-west-1
"IdentityPoolId": "eu-west-1:11111111-2222-3333-4444-555555555555".
cognito-identity.amazonaws.com, scoped to this specific Identity Pool via the aud condition.
cat > trust-policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "cognito-identity.amazonaws.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "cognito-identity.amazonaws.com:aud": "eu-west-1:11111111-2222-3333-4444-555555555555" },
"ForAnyValue:StringLike": { "cognito-identity.amazonaws.com:amr": "authenticated" }
}
}]
}
EOF
aws iam create-role \
--role-name dva-lab-cognito-authenticated-role \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name dva-lab-cognito-authenticated-role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
A role ARN like arn:aws:iam::123456789012:role/dva-lab-cognito-authenticated-role, scoped (for this lab) to read-only S3 access.
aws cognito-identity set-identity-pool-roles \ --identity-pool-id eu-west-1:11111111-2222-3333-4444-555555555555 \ --roles authenticated=arn:aws:iam::123456789012:role/dva-lab-cognito-authenticated-role \ --region eu-west-1No output on success — the mapping is now active.
GetId, presenting the ID token from Lab 1 as the login (an Identity Pool's --logins map is keyed by the User Pool's issuer path, and expects the ID token there — not the Access token).
aws cognito-identity get-id \
--identity-pool-id eu-west-1:11111111-2222-3333-4444-555555555555 \
--logins cognito-idp.eu-west-1.amazonaws.com/eu-west-1_AbCdEfGhI=${ID_TOKEN} \
--region eu-west-1
"IdentityId": "eu-west-1:99999999-aaaa-bbbb-cccc-dddddddddddd" — a Cognito identity, not an AWS credential yet.
GetCredentialsForIdentity with that identity ID and the same login map.
aws cognito-identity get-credentials-for-identity \
--identity-id "eu-west-1:99999999-aaaa-bbbb-cccc-dddddddddddd" \
--logins cognito-idp.eu-west-1.amazonaws.com/eu-west-1_AbCdEfGhI=${ID_TOKEN} \
--region eu-west-1
A Credentials block with AccessKeyId (starts ASIA...), SecretKey, SessionToken, and an Expiration roughly an hour out — temporary STS credentials, exactly as the guide describes, never a static IAM access key.
AWS_ACCESS_KEY_ID="ASIA..." \ AWS_SECRET_ACCESS_KEY="..." \ AWS_SESSION_TOKEN="..." \ aws sts get-caller-identity --region eu-west-1
"Arn" in the response reads arn:aws:sts::123456789012:assumed-role/dva-lab-cognito-authenticated-role/CognitoIdentityCredentials — proof this session was minted via sts:AssumeRoleWithWebIdentity against the role from Step 2, not via any User Pool mechanism.
This is the single most-tested distinction in the whole module, made physically visible: Lab 1 gave you JWTs that prove identity; this lab exchanges one of those JWTs for temporary AWS credentials that grant access. Same underlying user, two completely different credential types, two completely different AWS services (Cognito User Pools vs. Cognito Identity Pools/STS) doing the work. If you can trace GetId → GetCredentialsForIdentity → AssumeRoleWithWebIdentity from memory, the "does this scenario need an Identity Pool" question type stops being guesswork.
Click card to flip. Mark right or wrong to track score.
DVA-C02 scenario style, Easy → Specialty. Select an answer to reveal the explanation.