Skip to content

Amazon Cognito

Amazon Cognito is a fully managed AWS service that handles authentication, authorization, and user management for web, mobile, and serverless applications, removing the need to build and maintain custom user directories, password hashing, token logic and session management.

Users can sign in directly with credentials they create in your app (username/password), or federate through third-party identity providers (IdPs) such as:

  • Google
  • Facebook
  • Login with Amazon
  • Sign in with Apple
  • SAML 2.0 providers (e.g., Microsoft Entra ID / Active Directory)
  • OpenID Connect (OIDC) providers

Cognito supports secure sign-up, sign-in, MFA (SMS/email/TOTP), password policies, custom attributes, user groups, token issuance, and session revocation — all with built-in security and compliance features.

Cognito has two main services that solve complementary problems:

ComponentPurposeKey OutputsTypical Use CasesCan Be Used Alone?
User PoolsAuthentication + user directoryID Token, Access Token, Refresh Token (JWTs)User sign-up/sign-in, federated login, API protectionYes
Identity PoolsAuthorization — temporary AWS credentialsTemporary IAM credentials (access key, secret key, session token)Direct client access to S3, DynamoDB, Lambda, etc.Yes (but usually paired)
  • User Pools act as your user directory and identity provider (IdP), issuing JSON Web Tokens (JWTs) after successful authentication.
  • Identity Pools (formerly Federated Identities) map authenticated (or guest) users to temporary AWS IAM credentials for fine-grained access to AWS resources.
  • Recommended pattern: use User Pools for login + API auth. Add Identity Pools only when clients need direct AWS service access (e.g., browser uploads to S3).

User Pools and Identity Pools can be used separately or together — the most common modern pattern is User Pools → Identity Pools for authenticated flows.

After successful authentication in a User Pool, Cognito issues three JWTs (or opaque tokens):

  1. ID Token

    • Purpose: Proves who the user is (authentication / OpenID Connect identity).
    • Contains: User claims (sub/user ID, email, name, phone, custom attributes, groups).
    • Best used for: Frontend UI (display name/email), identity-based decisions in your app.
    • Not primarily for API authorization (contains sensitive PII — avoid sending unnecessarily).
    • Lifetime: Default 1 hour (configurable).
  2. Access Token

    • Purpose: Authorizes actions/scopes the user can perform (OAuth 2.0 authorization).
    • Contains: Scopes (e.g., openid, profile, custom scopes), groups, client_id.
    • Best used for: Calling your APIs (via API Gateway JWT authorizer), user self-service operations (e.g., UpdateUserAttributes — requires aws.cognito.signin.user.admin scope).
    • Preferred for backend API authorization, since scopes keep the grant least-privilege and separate authorization from identity.
  3. Refresh Token

    • Purpose: Obtain new ID + Access tokens without re-login.
    • Opaque/encrypted — readable only by Cognito.
    • Lifetime: Default 30 days (configurable up to 10 years).
    • Security: Store securely (HttpOnly cookie or secure storage), never expose client-side. Revoke on logout.

Token handling:

  • Use Access Token for API calls when possible (scopes + least privilege).
  • Use ID Token safely with Cognito’s native API Gateway authorizer (AWS verifies it securely).
  • Never store tokens in localStorage (XSS risk) — use secure cookies or Amplify Auth helpers.
  • Enable token revocation to invalidate tokens on logout, compromise, or account disable.

ID Token (decoded JWT payload):

{
"sub": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111",
"iss": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_EXAMPLE",
"aud": "3q1example4client8id0abc",
"token_use": "id",
"auth_time": 1708300800,
"exp": 1708304400,
"iat": 1708300800,
"email": "jane.doe@example.com",
"email_verified": true,
"name": "Jane Doe",
"phone_number": "+44712345678",
"cognito:username": "jane.doe",
"cognito:groups": ["admin", "developers"],
"custom:department": "Engineering",
"custom:tenant_id": "tenant-42"
}

Contains user identity claims (PII). Use for displaying user info in the UI or passing identity to Identity Pools. token_use is always "id".

Access Token (decoded JWT payload):

{
"sub": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111",
"iss": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_EXAMPLE",
"client_id": "3q1example4client8id0abc",
"token_use": "access",
"scope": "openid profile aws.cognito.signin.user.admin custom-api/read:orders custom-api/write:orders",
"auth_time": 1708300800,
"exp": 1708304400,
"iat": 1708300800,
"jti": "EXAMPLE-jwt-id-12345",
"username": "jane.doe",
"cognito:groups": ["admin", "developers"],
"origin_jti": "EXAMPLE-origin-jwt-id-67890",
"event_id": "abcdef12-3456-7890-abcd-ef1234567890",
"version": 2
}

Contains scopes and groups but no PII (no email, name, or phone). Use for API authorization. token_use is always "access". Note: client_id replaces aud.

Refresh Token:

eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ...
(opaque encrypted blob — not a standard JWT)

The Refresh Token is encrypted and opaque — it cannot be decoded or inspected. Only Cognito can read it. It contains no user-readable claims. Your application stores it securely and sends it back to Cognito to obtain new ID and Access Tokens.

Key differences at a glance:

FieldID TokenAccess TokenRefresh Token
token_use"id""access"N/A (opaque)
User PII (email, name, phone)YesNoN/A
cognito:groupsYesYesN/A
scopeNoYesN/A
Audience fieldaud (client ID)client_idN/A
Custom attributesYes (custom:*)NoN/A
DecodableYes (JWT)Yes (JWT)No (encrypted)
Default lifetime1 hour1 hour30 days
sequenceDiagram autonumber actor User participant Frontend as Client App (Browser/Mobile) participant Cognito as AWS Cognito User Pool participant Backend as Your API / Lambda participant Resource as Protected Resource / AWS Service User->>Frontend: Open app & initiate sign-in Frontend->>Cognito: Redirect to Hosted UI / SDK sign-in (username/password or federated) User->>Cognito: Authenticate Cognito-->>Frontend: ID Token + Access Token + Refresh Token Frontend->>Frontend: Store tokens securely (memory + HttpOnly cookie for refresh) User->>Frontend: Request protected data Frontend->>Backend: API request + Authorization: Bearer <Access Token> Backend->>Cognito: Validate JWT (native authorizer) Backend-->>Frontend: Return protected data Note over Frontend,Cognito: Periodic refresh Frontend->>Cognito: Refresh request (with Refresh Token) Cognito-->>Frontend: New ID + Access Tokens alt Optional: Direct AWS access (Identity Pool) Frontend->>Cognito: Exchange tokens via Identity Pool SDK Cognito-->>Frontend: Temporary AWS IAM Credentials Frontend->>Resource: Call S3 / DynamoDB / etc. directly end

Cognito User Pool Authorizer (API Gateway)

Section titled “Cognito User Pool Authorizer (API Gateway)”

A Cognito User Pool Authorizer is a built-in API Gateway feature that validates Cognito-issued JWTs before your backend code runs. It offloads authentication entirely to the API Gateway layer, so your Lambda or backend never receives unauthenticated requests.

Client → API Gateway → [Cognito User Pool Authorizer] → Lambda / Backend
Validates JWT signature,
expiration, issuer, audience
→ 401 if invalid
  1. The client sends a request with Authorization: Bearer <token> (ID Token or Access Token).
  2. API Gateway passes the token to the Cognito User Pool Authorizer.
  3. The authorizer validates the token against the configured User Pool (checks signature, expiration, issuer iss, and audience aud/client_id).
  4. If valid, the request proceeds to the backend with the decoded claims available in event.requestContext.authorizer.claims (REST API) or event.requestContext.authorizer.jwt.claims (HTTP API).
  5. If invalid or expired, API Gateway returns 401 Unauthorized immediately — your backend is never invoked.
ScenarioUse Cognito User Pool Authorizer?
Protect APIs behind Cognito sign-inYes — this is the primary use case
Simple group-based access (e.g., admins vs users)Yes — check cognito:groups claim in Lambda
OAuth2 scope-based authorization (e.g., read:orders)Yes — use Access Token with custom scopes
Public + authenticated mixed endpointsYes — set authorizer per route/method
Fine-grained per-resource authorization (e.g., user can only edit own records)Partially — authorizer handles authentication, your backend handles resource-level authorization using claims
Non-Cognito identity providers onlyNo — use a Lambda authorizer or JWT authorizer with your IdP
Machine-to-machine (M2M) with client credentialsYes — use Cognito client credentials grant + Access Token with custom scopes
  • Custom token validation logic — where tokens must be checked against a database, external services called, or business rules applied during authentication, use a Lambda Authorizer instead.
  • Non-JWT authentication — API keys, basic auth, or custom headers require a Lambda Authorizer.
  • Third-party IdP without Cognito — where Auth0, Okta or another IdP is used directly rather than federated through Cognito, use the JWT Authorizer (HTTP API) or a Lambda Authorizer (REST API).
  1. Create the Authorizer in API Gateway Console:

    • Go to your REST API → AuthorizersCreate New Authorizer
    • Type: Cognito
    • Cognito User Pool: select your pool
    • Token Source: Authorization (the header name)
  2. Attach to Methods:

    • Go to Resources → select a method (e.g., GET /orders)
    • Method Request → Authorization → select your Cognito authorizer
  3. Deploy the API to a stage.

CloudFormation / SAM example (REST API):

Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Auth:
DefaultAuthorizer: MyCognitoAuthorizer
Authorizers:
MyCognitoAuthorizer:
UserPoolArn: !GetAtt MyUserPool.Arn
GetOrdersFunction:
Type: AWS::Serverless::Function
Properties:
Handler: orders.handler
Runtime: nodejs22.x
Events:
GetOrders:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /orders
Method: GET
# Inherits MyCognitoAuthorizer from API default

HTTP APIs use a JWT Authorizer that works natively with Cognito User Pools:

Resources:
MyHttpApi:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: my-http-api
ProtocolType: HTTP
CognitoAuthorizer:
Type: AWS::ApiGatewayV2::Authorizer
Properties:
ApiId: !Ref MyHttpApi
AuthorizerType: JWT
Name: CognitoJWTAuthorizer
IdentitySource: "$request.header.Authorization"
JwtConfiguration:
Audience:
- !Ref MyUserPoolClient
Issuer: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${MyUserPool}"

Once the authorizer validates the token, your Lambda receives the decoded claims:

// REST API (v1) — claims are strings
exports.handler = async (event) => {
const claims = event.requestContext.authorizer.claims;
const userId = claims.sub;
const email = claims.email;
const groups = claims['cognito:groups']; // comma-separated string
// Use claims for resource-level authorization
const orders = await getOrdersByUser(userId);
return { statusCode: 200, body: JSON.stringify(orders) };
};
// HTTP API (v2) — claims are in jwt.claims
exports.handler = async (event) => {
const claims = event.requestContext.authorizer.jwt.claims;
const userId = claims.sub;
const scopes = event.requestContext.authorizer.jwt.scopes;
return { statusCode: 200, body: JSON.stringify({ userId, scopes }) };
};

ID Token vs Access Token — Which to Send?

Section titled “ID Token vs Access Token — Which to Send?”
AspectID TokenAccess Token
ContainsUser identity claims (email, name, sub, groups)Scopes, groups, client_id
REST API Cognito AuthorizerSupported (validates aud = client ID)Supported (validates client_id)
HTTP API JWT AuthorizerSupportedPreferred — supports scope-based authorization
Scope enforcementNot applicableAuthorizer can require specific scopes per route
Best practiceUse when backend needs identity claimsUse for API authorization (least privilege)

Recommendation: Send the Access Token for API calls. If your backend needs user attributes (email, name), either:

  • Include them as custom claims via a Pre Token Generation Lambda Trigger, or
  • Look up the user by sub from the Access Token.
exports.handler = async (event) => {
const claims = event.requestContext.authorizer.claims;
const groups = (claims['cognito:groups'] || '').split(',');
if (!groups.includes('admin')) {
return { statusCode: 403, body: JSON.stringify({ message: 'Forbidden' }) };
}
// Admin-only logic here
};

Cognito User Pool Authorizer vs Lambda Authorizer vs IAM Authorization

Section titled “Cognito User Pool Authorizer vs Lambda Authorizer vs IAM Authorization”
FeatureCognito User Pool AuthorizerLambda AuthorizerIAM Authorization
Auth mechanismCognito JWT validationCustom code (any token/header)AWS Sig v4 signed requests
Setup complexityLow (configuration only)Medium (write + deploy Lambda)Low (IAM policies)
LatencyLow (built-in validation)Higher (Lambda cold start possible)Low
CachingBuilt-in (configurable TTL)Optional (configurable TTL)N/A
CostNo extra costLambda invocation costNo extra cost
Custom logicNo (validation only)Yes (database lookups, business rules)No
Best forCognito-based appsCustom IdPs, complex auth rulesAWS service-to-service, SDK clients