The Three Cognito Tokens
After a successful sign-in against a Cognito User Pool, AWS returns three tokens. Each has a distinct purpose — mixing them up is the #1 source of Cognito integration bugs.
ID Token — "Who is this user?"
- OIDC-compliant JWT signed with RS256.
- Contains
email,email_verified,phone_number,name,preferred_username,cognito:username, and every custom attribute (prefixedcustom:). token_useis always"id".audis your Cognito App Client ID.- Send to services that need to display or personalise based on user identity — never to APIs for authorization.
Access Token — "What can this user do?"
- OAuth 2.0 access token as a JWT signed with RS256.
- Contains
cognito:groups(array of group names),scope(space-separated OAuth scopes granted),client_id,username. token_useis always"access".- Send to your APIs (API Gateway, ALB, or custom services) for authorization.
- Contains NO email or profile info — do not use for user display.
Refresh Token — Opaque, Not a JWT
- Random opaque string. Do not try to decode it — it's not base64, not JSON, not a JWT.
- Only Cognito knows the mapping from this string to a user pool session.
- Used at
POST /oauth2/tokenwithgrant_type=refresh_tokento get new access and ID tokens. - Default 30-day validity; configurable 60 minutes to 3650 days per app client.
Complete Cognito ID Token Example
{
"at_hash": "abc123def456...",
"sub": "a1b2c3d4-1234-5678-90ab-cdef01234567",
"cognito:groups": ["premium-users", "beta-testers"],
"email_verified": true,
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABCDE1234",
"cognito:username": "jane.doe",
"origin_jti": "aabbccdd-...",
"aud": "12345abcde67890fghij",
"identities": [
{ "userId": "1234567890",
"providerName": "Google",
"providerType": "Google",
"issuer": null,
"primary": "true",
"dateCreated": "1727654321000" }
],
"token_use": "id",
"auth_time": 1727654321,
"exp": 1727657921,
"iat": 1727654321,
"jti": "aabbccdd-1234-5678-90ab-cdef01234567",
"email": "[email protected]",
"custom:organization": "Acme Corp",
"custom:plan": "enterprise"
}Complete Cognito Access Token Example
{
"sub": "a1b2c3d4-1234-5678-90ab-cdef01234567",
"cognito:groups": ["premium-users", "beta-testers"],
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABCDE1234",
"version": 2,
"client_id": "12345abcde67890fghij",
"origin_jti": "aabbccdd-...",
"token_use": "access",
"scope": "aws.cognito.signin.user.admin openid email profile",
"auth_time": 1727654321,
"exp": 1727657921,
"iat": 1727654321,
"jti": "1234abcd-...",
"username": "jane.doe"
}Verification With aws-jwt-verify (Node.js — Official)
import { CognitoJwtVerifier } from 'aws-jwt-verify';
// One verifier per token type, per user pool
const idTokenVerifier = CognitoJwtVerifier.create({
userPoolId: 'us-east-1_ABCDE1234',
tokenUse: 'id',
clientId: 'YOUR_APP_CLIENT_ID',
});
const accessTokenVerifier = CognitoJwtVerifier.create({
userPoolId: 'us-east-1_ABCDE1234',
tokenUse: 'access',
clientId: 'YOUR_APP_CLIENT_ID',
scope: ['aws.cognito.signin.user.admin'], // optional scope check
});
// Express middleware
app.use(async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).end();
try {
const payload = await accessTokenVerifier.verify(token);
req.userId = payload.sub;
req.groups = payload['cognito:groups'] || [];
next();
} catch (err) {
res.status(401).json({ error: err.message });
}
});Verification In Python
import jwt
import requests
from functools import lru_cache
REGION = "us-east-1"
USER_POOL_ID = "us-east-1_ABCDE1234"
APP_CLIENT_ID = "YOUR_APP_CLIENT_ID"
ISSUER = f"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}"
@lru_cache(maxsize=1)
def _jwks():
r = requests.get(f"{ISSUER}/.well-known/jwks.json", timeout=5)
return {k["kid"]: k for k in r.json()["keys"]}
def verify_cognito_token(token: str, token_use: str = "access"):
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header["kid"]
jwks = _jwks()
if kid not in jwks:
_jwks.cache_clear()
jwks = _jwks()
key = jwt.algorithms.RSAAlgorithm.from_jwk(jwks[kid])
payload = jwt.decode(
token, key,
algorithms=["RS256"],
issuer=ISSUER,
audience=APP_CLIENT_ID if token_use == "id" else None,
options={"verify_aud": token_use == "id"},
)
if payload["token_use"] != token_use:
raise jwt.InvalidTokenError(f"expected {token_use}, got {payload['token_use']}")
if token_use == "access" and payload.get("client_id") != APP_CLIENT_ID:
raise jwt.InvalidTokenError("client_id mismatch")
return payloadCognito-Specific Gotchas
- ID token has aud, access token has client_id. Both are set to your App Client ID, but the claim name is different. Check the right one for the right token type.
- Access tokens have no email. If you need the user's email server-side, either verify the ID token instead, or call
GetUseragainst the Cognito API with the access token. - Groups may be missing entirely. If a user is in no groups, the
cognito:groupsclaim is absent — not an empty array. Always guard withpayload['cognito:groups']?.includes(...). - Custom attributes require the
custom:prefix. A custom attribute namedorganizationappears in the token ascustom:organization. Amplify does not strip the prefix — reference it exactly. - Federated identities show up in
identities. If a user signed in via Google/Facebook/SAML, theidentitiesarray in the ID token has details. Thesubremains the Cognito UUID, not the federated provider's sub. - Do NOT accept tokens from any Cognito user pool. Always verify
issmatches YOUR pool exactly. Otherwise, an attacker with any Cognito pool could mint tokens your app trusts.
Cognito JWKS URL
Always fetch keys from this exact URL, cache for 6–12 hours, refresh on kid miss:
https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/jwks.json
# Example (us-east-1, pool ABCDE1234):
https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABCDE1234/.well-known/jwks.jsonRelated JWT Tools
- Firebase JWT Decoder — the Firebase equivalent
- JWT RS256 Decoder — the algorithm Cognito uses
- Verify JWT Signature — full signature verification with JWKS
- JWT Expiration Checker — check exp/iat/auth_time
- JWT Refresh Token Example — refresh flow patterns