Signing in to an Amazon Cognito user pool returns three tokens. Name them, and say which one your own backend API should accept as proof that the caller is authorized.
answer
- three tokens, three different jobs
- one says who, one says what
- audience decides who may consume it
- scopes and groups live on one of them
- the third one only talks to Cognito
basics
~20 sA Cognito user pool sign-in returns an ID token, an access token and a refresh token. Your API should accept the access token, which carries scopes and group claims; the ID token describes the user to the client, and the refresh token only obtains new tokens.
solid answer
~50 sCognito user pools issue three JWTs. The **ID token** answers "who is this person" — it carries profile claims such as `email` and `cognito:username`, and its audience (`aud`) is the app client, so it is meant for the client application itself. The **access token** answers "what may this caller do" — it carries `scope`, `client_id` and `cognito:groups`, and it is the token a resource server should accept on the `Authorization` header. The **refresh token** is not a JWT you inspect; it is presented back to Cognito to mint fresh ID and access tokens without re-prompting for a password, and it should never be sent to your API. The common mistake is authorizing on the ID token because it conveniently contains the email address — that token was minted for a different audience, and it says nothing about permissions.
code
json · 20 lines{
"id_token_claims": {
"sub": "a1b2c3d4-1111-2222-3333-444455556666",
"aud": "7example12345clientid",
"token_use": "id",
"email": "[email protected]",
"cognito:username": "dana",
"cognito:groups": ["admins"],
"exp": 1735689600
},
"access_token_claims": {
"sub": "a1b2c3d4-1111-2222-3333-444455556666",
"client_id": "7example12345clientid",
"token_use": "access",
"scope": "openid orders/read",
"username": "dana",
"cognito:groups": ["admins"],
"exp": 1735689600
}
}go deeper
Be able to name all three tokens and say in one sentence what each is for, and state plainly that the access token is the one your API accepts.
Explain why audience separates the ID token from the access token, and point to scope, client_id and token_use as the claims that make the access token the authorization credential.
Show where you terminate tokens in a real architecture — SPA versus backend-for-frontend — and account for token lifetime, refresh storage and the staleness window between a permission change and the next token.
Own the decision about who holds the refresh token across every client your product ships, and be ready to argue when a Cognito-issued JWT is enough authorization signal versus when entitlements belong in a service the token merely identifies the caller to.
## The three tokens When a client completes authentication against an Amazon Cognito user pool — through `InitiateAuth`, through the hosted UI's OAuth code exchange at `/oauth2/token`, or through a federated sign-in — Cognito returns a token set. Two of the three are JSON Web Tokens signed with RS256 by the user pool; the third is an opaque credential. **ID token.** An identity assertion about the signed-in user. Its claims describe the person: `sub` (the immutable user identifier inside the pool), `email`, `phone_number`, any custom attributes you configured, `cognito:username`, `cognito:groups`, and `token_use` set to `"id"`. Its `aud` claim is the app client ID. The intended consumer is the application that requested sign-in — a SPA reading it to render the user's name, or a server-side web app establishing a session. **Access token.** An authorization assertion. It carries `scope` (the OAuth scopes granted to the app client, plus any custom scopes from a Cognito resource server), `client_id` instead of `aud`, `username`, `cognito:groups`, and `token_use` set to `"access"`. This is the credential a resource server should demand, because it states what the bearer was granted rather than who the bearer is. **Refresh token.** A long-lived opaque string. It is presented back to Cognito — never to your own service — to obtain a fresh ID and access token pair. By default a user pool's ID and access tokens are short-lived (one hour) while the refresh token lasts far longer, and both windows are configured per app client. ## Why the distinction matters It is tempting to send the ID token to your API: it contains the email address, which is exactly the field the API wants for a lookup. Two things are wrong with that. First, audience. The ID token's `aud` names the app client that requested it; a resource server accepting it is accepting a token minted for somebody else, which is precisely the ambiguity audience checking exists to prevent. Second, semantics. The ID token asserts identity, not entitlement. If you later introduce scopes — a read-only mobile client versus a full-privilege web client — the ID token cannot express the difference, because it is not the token scopes live on. Code that authorizes on identity claims silently ignores every scope decision you make afterwards. The symmetric mistake is putting the refresh token anywhere near a resource server. A refresh token is closer to a password than to a session token: it mints new credentials on demand. It belongs in the client's most protected storage, is presented only to Cognito's token endpoint, and can be revoked with the `RevokeToken` API when a user signs out globally. ## What your API actually does with the access token A resource server extracts the bearer token, verifies its signature against the pool's published JWKS, and then reads claims: ```javascript // after signature and expiry have been verified if (claims.token_use !== "access") throw new Error("wrong token type"); if (claims.client_id !== EXPECTED_APP_CLIENT_ID) throw new Error("wrong client"); const groups = claims["cognito:groups"] ?? []; ``` Note `cognito:groups` appears on both tokens. Groups in a Cognito user pool are the usual coarse authorization handle — you assign users to `admins` or `support`, and the claim rides along. It is a claim like any other: trustworthy only because the signature was checked, and stale the moment group membership changes, until the token expires. ## Practical consequences of the split - A single-page app typically keeps the ID token to render the UI, sends the access token on every API call, and lets the SDK use the refresh token in the background. - A backend-for-frontend pattern trades the token set for its own session cookie at the edge and never exposes the refresh token to browser JavaScript at all. - Machine-to-machine callers use the client-credentials grant, which returns **only** an access token — there is no user, so there is no ID token. That alone shows which token authorization is supposed to hang off. - Shortening token lifetime tightens the blast radius of a leaked access token but increases traffic to Cognito's token endpoint; it is a per-app-client dial, not a pool-wide one.
- Your machine-to-machine integration authenticates with the client-credentials grant and the team complains there is no ID token. What do you tell them?That is expected. Client credentials authenticate an application, not a person, so there is no identity to assert and Cognito returns only an access token carrying the app client's custom scopes. Authorize on those scopes. If the integration needs a user identity, it is using the wrong grant — it should be acting on behalf of a signed-in user instead.
- A colleague wants to shorten access token validity to five minutes for security. What is the tradeoff?Shorter validity narrows the window in which a stolen access token is useful, and it makes group or attribute changes take effect sooner, because a resource server verifying offline only re-reads claims when a new token arrives. The cost is more refresh round-trips to Cognito's token endpoint, more latency on the first call after each expiry, and more visible breakage if refresh handling is buggy.
- Where should a browser SPA keep the refresh token?Nowhere a cross-site script can read it if you can avoid it. The safest shape is not to give the browser a refresh token at all: terminate the OAuth code exchange in a backend-for-frontend, keep the refresh token server-side, and hand the browser an httpOnly session cookie. If the SPA must hold tokens, keep them in memory rather than localStorage and accept that an XSS bug is a full account compromise.
The ID token is a passport that proves who you are; the access token is the boarding pass that says which flight you may board. The gate agent wants the boarding pass.
saying these in an interview costs you the question
- Sends the ID token to the API because it carries the email
- Thinks the refresh token is a bearer credential for resource servers
- Believes the access token must be checked with Cognito on every call
- Assumes signing out invalidates already-issued access tokens
- Reads cognito:groups without verifying the token signature