Docs / Cognito JWT

Local Cognito emulator: API reference

LocalEmu generates real RS256-signed JWT tokens for Cognito user pools. Tokens are verifiable with standard libraries, JWKS endpoints work, and OIDC discovery is fully functional.

Want a runnable walkthrough? See Cognito: demo & walkthrough.

Token Overview

1. When you call CreateUserPool, LocalEmu generates a unique RSA key pair for that pool
2. Authentication calls (AdminInitiateAuth, InitiateAuth) return RS256-signed JWTs
3. The public key is available at the JWKS endpoint, just like real AWS Cognito
4. Your application's token verification code works unchanged against LocalEmu

Endpoints

Endpoint URL
JWKS <base>/<pool-id>/.well-known/jwks.json
OIDC discovery <base>/<pool-id>/.well-known/openid-configuration
Token issuer (iss claim) <base>/<pool-id>
OAuth2 authorize <base>/oauth2/authorize
OAuth2 token <base>/oauth2/token
OAuth2 userInfo <base>/oauth2/userInfo

<base> is whatever external_service_url() returns for this LocalEmu instance: http://localhost:4566 on a default install, or the value of LOCALEMU_HOST / LOCALEMU_PUBLISH_URL when set. The iss claim in every issued JWT uses the same base, so a JWT verifier configured against the discovery document will accept the tokens this LocalEmu instance signs without an issuer-mismatch error. Only the per-pool URLs (jwks.json, openid-configuration, the iss URL itself) carry the <pool-id> path segment; the OAuth2 endpoints do not.

Token Claims

ID Token

Claim Description
subUnique user identifier (UUID)
emailUser's email address
cognito:usernameThe username used for authentication
cognito:groupsList of groups the user belongs to
token_useAlways id

Access Token

Claim Description
usernameThe authenticated username
client_idThe app client ID used for authentication
scopeOAuth2 scopes granted to the token
token_useAlways access

Refresh Token

Refresh tokens are opaque strings, not JWTs. They can be used with AdminInitiateAuth using the REFRESH_TOKEN_AUTH flow to obtain new ID and access tokens.

Example: Full Authentication Flow

Step 1: Create a user pool

Terminal
$ awsemu cognito-idp create-user-pool \
    --pool-name my-app-pool

UserPool:
  Id: us-east-1_abc123XYZ
  Name: my-app-pool

Step 2: Create an app client

Terminal
$ awsemu cognito-idp create-user-pool-client \
    --user-pool-id us-east-1_abc123XYZ \
    --client-name my-app \
    --explicit-auth-flows ALLOW_ADMIN_USER_PASSWORD_AUTH

UserPoolClient:
  ClientId: 3abcdef12345
  ClientName: my-app

Step 3: Create a user and set their password

Terminal
$ awsemu cognito-idp admin-create-user \
    --user-pool-id us-east-1_abc123XYZ \
    --username testuser \
    --user-attributes Name=email,Value=test@example.com

$ awsemu cognito-idp admin-set-user-password \
    --user-pool-id us-east-1_abc123XYZ \
    --username testuser \
    --password 'Str0ng!Pass' \
    --permanent

Step 4: Authenticate and get tokens

Terminal
$ awsemu cognito-idp admin-initiate-auth \
    --user-pool-id us-east-1_abc123XYZ \
    --client-id 3abcdef12345 \
    --auth-flow ADMIN_USER_PASSWORD_AUTH \
    --auth-parameters USERNAME=testuser,PASSWORD='Str0ng!Pass'

AuthenticationResult:
  AccessToken: eyJraWQiOiJhYmMx...  (truncated)
  IdToken: eyJraWQiOiJhYmMx...      (truncated)
  RefreshToken: a1b2c3d4e5...       (opaque)
  ExpiresIn: 3600
  TokenType: Bearer

Step 5: Fetch the JWKS public key

Terminal
$ curl -s http://localhost:4566/us-east-1_abc123XYZ/.well-known/jwks.json | python3 -m json.tool

{
    "keys": [
        {
            "kty": "RSA",
            "kid": "abc123",
            "use": "sig",
            "alg": "RS256",
            "n": "0vx7agoebGcQ...",
            "e": "AQAB"
        }
    ]
}

Step 6: Check OIDC discovery

Terminal
$ curl -s http://localhost:4566/us-east-1_abc123XYZ/.well-known/openid-configuration | python3 -m json.tool

{
    "issuer": "http://localhost:4566/us-east-1_abc123XYZ",
    "jwks_uri": "http://localhost:4566/us-east-1_abc123XYZ/.well-known/jwks.json",
    "authorization_endpoint": "http://localhost:4566/oauth2/authorize",
    "token_endpoint": "http://localhost:4566/oauth2/token",
    "userinfo_endpoint": "http://localhost:4566/oauth2/userInfo",
    "id_token_signing_alg_values_supported": ["RS256"],
    "response_types_supported": ["code", "token"],
    "scopes_supported": ["openid", "email", "phone", "profile"],
    "subject_types_supported": ["public"],
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post"
    ],
    "claims_supported": [
        "sub", "iss", "auth_time",
        "email", "email_verified",
        "phone_number", "phone_number_verified",
        "cognito:username", "cognito:groups"
    ]
}

Python Token Verification

Verify tokens using PyJWT and requests. Install with: pip install PyJWT cryptography requests

verify_token.py
import jwt
import requests

POOL_ID = "us-east-1_abc123XYZ"
JWKS_URL = f"http://localhost:4566/{POOL_ID}/.well-known/jwks.json"

# Fetch the public keys
jwks = requests.get(JWKS_URL).json()
jwk_key = jwks["keys"][0]
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(jwk_key)

# Verify the ID token
id_token = "eyJraWQiOiJhYmMx..."  # from AuthenticationResult

decoded = jwt.decode(
    id_token,
    key=public_key,
    algorithms=["RS256"],
    issuer=f"http://localhost:4566/{POOL_ID}",
    options={"verify_aud": False},
)

print(f"Subject:  {decoded['sub']}")
print(f"Email:    {decoded['email']}")
print(f"Username: {decoded['cognito:username']}")
print(f"Groups:   {decoded.get('cognito:groups', [])}")

API Gateway V2 JWT Authorizers

LocalEmu's Cognito tokens work with API Gateway V2 (HTTP API) JWT authorizers. The authorizer fetches the JWKS from the issuer URL and validates the token signature, expiration, and audience automatically.

1. Set the authorizer's issuer to http://localhost:4566/<pool-id>
2. Set the audience to your app client ID
3. API Gateway fetches JWKS, verifies the RS256 signature, and extracts claims
4. The same authorizer configuration works against both LocalEmu and real AWS

Lambda Triggers

The nine Lambda triggers configurable via LambdaConfig on CreateUserPool / UpdateUserPool actually fire, at the same phase as real AWS. Set the ARN, and the Lambda is invoked with an AWS-shaped event carrying a triggerSource that matches the real per-flow naming (PreAuthentication_Authentication, CustomMessage_SignUp, and so on).

Terminal
$ awsemu cognito-idp create-user-pool \
    --pool-name my-app-pool \
    --lambda-config '{
        "PreAuthentication": "arn:aws:lambda:us-east-1:000000000000:function:pre-auth",
        "PostAuthentication": "arn:aws:lambda:us-east-1:000000000000:function:post-auth",
        "CustomMessage": "arn:aws:lambda:us-east-1:000000000000:function:custom-msg"
    }'

UserPool:
  Id: us-east-1_abc123XYZ
  Name: my-app-pool
Trigger Fires when
PreAuthenticationBefore the password check in InitiateAuth / AdminInitiateAuth. Raising in the Lambda returns NotAuthorizedException to the caller, matching the AWS contract.
PostAuthenticationAfter a successful AuthenticationResult in all four auth handlers. Side-effect only; the result isn't affected.
CustomMessageOn every code-send path (SignUp, ResendConfirmationCode, ForgotPassword, AdminCreateUser, attribute verification). The Lambda's smsMessage / emailMessage / emailSubject override lands in the per-user message buffer below.
DefineAuthChallengeDrives the CUSTOM_AUTH flow end to end, together with CreateAuthChallenge and VerifyAuthChallengeResponse below. A server-side session (5-minute idle TTL) carries private challenge parameters from InitiateAuth through to RespondToAuthChallenge, cleared on token mint or auth failure.
CreateAuthChallengeReturns the challenge's public and private parameters for the current step of the CUSTOM_AUTH flow.
VerifyAuthChallengeResponseJudges whether the caller's answer to the current CUSTOM_AUTH challenge is correct.
UserMigrationOn InitiateAuth / AdminInitiateAuth when the user is unknown and the auth flow is USER_PASSWORD_AUTH, USER_SRP_AUTH, or ADMIN_USER_PASSWORD_AUTH. The Lambda's response synthesizes a confirmed user with the password from the request; auth is then retried and the real AuthenticationResult is returned.
CustomEmailSender
CustomSMSSender
When configured, replace the in-memory message with a Lambda invocation carrying the code as a base64-encoded blob (a placeholder for AWS's KMS-encrypted shape -- there is no real KMS envelope locally).

None of these triggers send a real email or SMS. The plain code Cognito would have delivered -- and the sender-Lambda's message, when one is configured -- is stored in an in-memory buffer, queryable through a dashboard endpoint. Tests and tutorials can read the buffer to recover the code and complete a flow (sign-up confirmation, password reset, ...) without wiring up real delivery.

Terminal
$ curl -s "http://localhost:4566/_localemu/api/cognito-messages/us-east-1_abc123XYZ/alice" | python3 -m json.tool

{
    "pool_id": "us-east-1_abc123XYZ",
    "username": "alice",
    "account_id": "000000000000",
    "region": "us-east-1",
    "count": 1,
    "messages": [
        {
            "trigger_source": "CustomMessage_SignUp",
            "delivery_medium": "EMAIL",
            "code_plain": "123456",
            "code_encrypted_b64": null,
            "sms_message": null,
            "email_message": "Welcome alice, your code is 123456",
            "email_subject": "Verify your account",
            "sent_via_sender_lambda": false,
            "timestamp": "2026-08-01T12:00:00Z"
        }
    ]
}