Docs / Use Cases / Cognito

Cognito: demo & walkthrough

Create user pools, sign in users, and get real RS256-signed JWTs that verify with any standard JWT library. Full OIDC discovery and JWKS endpoints included. No AWS account needed.

Looking for the API surface and CLI examples? See Cognito: API reference.

What the demo does. Creates a Cognito user pool and a public app client, creates a user with admin-create-user and sets a permanent password with admin-set-user-password, signs the user in via admin-initiate-auth with the ADMIN_USER_PASSWORD_AUTH flow and captures the returned IdToken, fetches the pool's /.well-known/openid-configuration + /.well-known/jwks.json over HTTP, then verifies the JWT signature with PyJWT against the JWKS (src/verify_token.py), and exercises group management: create-group, admin-add-user-to-group, admin-list-groups-for-user. One ./scripts/demo.sh runs all 10 steps; ./scripts/teardown.sh removes the pool. No special LocalEmu flags required. Source: 11-cognito-demo/ in the examples repo.
🔑

Real JWTs

RS256-signed tokens with proper claims, kid headers, and expiration. Verifiable with any JWT library.

🌐

JWKS Endpoint

Each pool gets its own RSA key pair. Public keys served at /.well-known/jwks.json like real Cognito.

📋

OIDC Discovery

Full openid-configuration with issuer, scopes, claims, and token endpoints. Standard-compliant.

👥

Group Management

Create groups, assign users, list memberships. Test RBAC flows without touching AWS.

Step-by-Step Walkthrough

Step 1: Start LocalEmu

Terminal
localemu start

Cognito is available out of the box. No special environment variables needed.

Step 2: Create a User Pool

Terminal
$ POOL_ID=$(awsemu cognito-idp create-user-pool \
    --pool-name my-app-pool \
    --query 'UserPool.Id' --output text)

$ echo "Pool ID: $POOL_ID"
Pool ID: us-east-1_72c878283811431980a7e118647f6190

# Inspect the full record:
$ awsemu cognito-idp describe-user-pool --user-pool-id "$POOL_ID" \
    --query 'UserPool.{Name:Name,Id:Id,Arn:Arn,Password:Policies.PasswordPolicy,Mfa:MfaConfiguration}'
{
  "Name": "my-app-pool",
  "Id":   "us-east-1_72c878283811431980a7e118647f6190",
  "Arn":  "arn:aws:cognito-idp:us-east-1:000000000000:userpool/us-east-1_72c878283811431980a7e118647f6190",
  "Password": {
    "MinimumLength": 8,
    "RequireUppercase": true,
    "RequireLowercase": true,
    "RequireNumbers": true,
    "RequireSymbols": true,
    "TemporaryPasswordValidityDays": 7
  },
  "Mfa": "OFF"
}

The pool is created with a full password policy (uppercase, lowercase, numbers, symbols, min 8 chars). LocalEmu generates a unique pool ID and registers JWKS/OIDC endpoints for it automatically. The $POOL_ID shell variable captured here is reused in every later step, so run the rest of the walkthrough in the same terminal session.

Step 3: Create a User Pool Client

Terminal
$ CLIENT_ID=$(awsemu cognito-idp create-user-pool-client \
    --user-pool-id $POOL_ID \
    --client-name my-app \
    --explicit-auth-flows ALLOW_ADMIN_USER_PASSWORD_AUTH ALLOW_USER_PASSWORD_AUTH ALLOW_REFRESH_TOKEN_AUTH \
    --query 'UserPoolClient.ClientId' --output text)

$ echo "Client ID: $CLIENT_ID"
Client ID: 5381380d005c437fb685b82aa7

The client is configured with ALLOW_ADMIN_USER_PASSWORD_AUTH, ALLOW_USER_PASSWORD_AUTH, and ALLOW_REFRESH_TOKEN_AUTH flows. The demo signs in with admin-initiate-auth. $CLIENT_ID is captured here and used together with $POOL_ID by the sign-in and verification steps below.

Step 4: Create user alice with email

Terminal
$ awsemu cognito-idp admin-create-user \
    --user-pool-id $POOL_ID \
    --username alice \
    --user-attributes Name=email,Value=alice@example.com \
    --temporary-password TempPass123!

User:
  Username: alice
  Attributes:
    - email: alice@example.com
    - sub: 9cf943c2-7bdd-4f4a-ba1e-fef296a40b46
  Enabled: true
  UserStatus: FORCE_CHANGE_PASSWORD

The user starts in FORCE_CHANGE_PASSWORD status, just like real AWS Cognito. A sub UUID is assigned automatically.

Step 5: Set permanent password

Terminal
$ awsemu cognito-idp admin-set-user-password \
    --user-pool-id $POOL_ID \
    --username alice \
    --password MySecret123! \
    --permanent

(no output - password set successfully)

The --permanent flag skips the forced password change. The user status moves to CONFIRMED.

Step 6: Sign in with admin-initiate-auth

Terminal
$ awsemu cognito-idp admin-initiate-auth \
    --user-pool-id $POOL_ID \
    --client-id $CLIENT_ID \
    --auth-flow ADMIN_USER_PASSWORD_AUTH \
    --auth-parameters USERNAME=alice,PASSWORD=MySecret123!

AuthenticationResult:
  TokenType: Bearer
  ExpiresIn: 3600
  AccessToken: eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc4ZjM1NTNkLTIwN2EtNGJjMC04
    Y2YwLThhNzM3OGRiNjA4OSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlMDg5
    MWE3Ni04Mjk3LTQxNzAtYTlmNi1lYmQyYjQ1ZWQ2YmYiLCJldmVudF9p
    ZCI6IjExNTQyZGRjLTFjMjQtNGNlZS05ZGZhLWFjNjBlY2I0NmNhZiIs
    ...
  IdToken: eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc4ZjM1NTNkLTIwN2EtNGJjMC04
    Y2YwLThhNzM3OGRiNjA4OSIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlMDg5
    MWE3Ni04Mjk3LTQxNzAtYTlmNi1lYmQyYjQ1ZWQ2YmYiLCJhdWQiOiI1
    MzgxMzgwZDAwNWM0MzdmYjY4NWI4MmFhNyIsImVtYWlsX3ZlcmlmaWVk
    ...
  RefreshToken: 38b88feb-4c53-4991-bf6e-628830b463ae-57cb38ac-dd9a-4f44-871e-cfbee79aff0b

Real RS256-signed JWTs. The AccessToken contains scopes and username. The IdToken contains user claims (email, sub, cognito:username). The RefreshToken can be used to get new tokens.

Step 7: JWKS endpoint - fetch the public key

Terminal
$ curl -s "http://localhost:4566/$POOL_ID/.well-known/jwks.json" | python3 -m json.tool

{
    "keys": [
        {
            "kty": "RSA",
            "kid": "78f3553d-207a-4bc0-8cf0-8a7378db6089",
            "use": "sig",
            "alg": "RS256",
            "n": "qTU-VHI-Qr7SsajRo4V0DA4KMKFaTQ-2aydeJ-VA-CjtBX5x4lp8g6yH
                 delXXz4jqBQzBaZ6c-sk2ovgsBLoFFVH4EyuDzRary2hXfRbkwaUVc3e
                 qiRonGv3l4rEAcRrLi7zNPKsjBEExHmXgxTMumgxC24NC5O8TW1a7Ybq
                 zWKJS0qJaD-LbLguXzM1fiaeCQgkTuvDhrus7jWKbllTkkvc68wTbWUb0
                 kyTFTm6anC42XLc7-o-BAt-ZVHle_IxnnTMghQIV_b2vfUd7WbAFlP9zT
                 JL-2WDaQDGS1Qqm7__bvKgIjzBRQIPlBl7A0n0t7ws43Fqn-vxvWgzYb
                 Xv9Q",
            "e": "AQAB"
        }
    ]
}

The JWKS endpoint returns the RSA public key for this pool. The kid matches the one in the JWT headers. Any JWT library can use this to verify tokens.

Step 8: OIDC discovery endpoint

Terminal
$ curl -s "http://localhost:4566/$POOL_ID/.well-known/openid-configuration" | python3 -m json.tool

{
    "issuer": "http://localhost:4566/us-east-1_72c878283811431980a7e118647f6190",
    "jwks_uri": "http://localhost:4566/us-east-1_72c878283811431980a7e118647f6190/.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"],
    "claims_supported": [
        "sub", "iss", "auth_time", "email", "email_verified",
        "phone_number", "phone_number_verified",
        "cognito:username", "cognito:groups"
    ]
}

Full OpenID Connect discovery document. Libraries like python-jose, jsonwebtoken, and Spring Security can auto-configure from this URL.

Step 9: Verify the JWT with Python

verify_cognito.py
import os
import sys

import boto3
import jwt as pyjwt
import requests
from jwt import PyJWK

pool_id = sys.argv[1]
client_id = sys.argv[2]

# Point boto3 at LocalEmu via AWS_ENDPOINT_URL; credentials come from
# the standard boto3 chain (env vars, shared config, instance profile).
endpoint = os.environ.get("AWS_ENDPOINT_URL", "http://localhost:4566")
client = boto3.client("cognito-idp", endpoint_url=endpoint, region_name="us-east-1")

result = client.admin_initiate_auth(
    UserPoolId=pool_id, ClientId=client_id,
    AuthFlow='ADMIN_USER_PASSWORD_AUTH',
    AuthParameters={'USERNAME': 'alice', 'PASSWORD': 'MySecret123!'})

token = result['AuthenticationResult']['IdToken']

# Fetch JWKS and verify the token
jwks = requests.get(f"http://localhost:4566/{pool_id}/.well-known/jwks.json").json()
print(f"JWKS keys: {len(jwks['keys'])}")

header = pyjwt.get_unverified_header(token)
print(f"Token kid: {header['kid']}")
print(f"Token alg: {header['alg']}")

for key_data in jwks['keys']:
    if key_data['kid'] == header['kid']:
        jwk_key = PyJWK.from_dict(key_data)
        decoded = pyjwt.decode(token, jwk_key, algorithms=['RS256'], audience=client_id)
        print(f"\nVERIFIED! Token claims:")
        print(f"  sub: {decoded['sub']}")
        print(f"  email: {decoded.get('email')}")
        print(f"  cognito:username: {decoded['cognito:username']}")
        print(f"  token_use: {decoded['token_use']}")
        print(f"  iss: {decoded['iss']}")
        break

oidc = requests.get(f"http://localhost:4566/{pool_id}/.well-known/openid-configuration").json()
print(f"\nOIDC issuer: {oidc['issuer']}")
print(f"Issuer matches token: {oidc['issuer'] == decoded['iss']}")
Terminal
$ python3 verify_cognito.py "$POOL_ID" "$CLIENT_ID"

JWKS keys: 1
Token kid: 78f3553d-207a-4bc0-8cf0-8a7378db6089
Token alg: RS256

VERIFIED! Token claims:
  sub: e0891a76-8297-4170-a9f6-ebd2b45ed6bf
  email: alice@example.com
  cognito:username: alice
  token_use: id
  iss: http://localhost:4566/us-east-1_72c878283811431980a7e118647f6190

OIDC issuer: http://localhost:4566/us-east-1_72c878283811431980a7e118647f6190
Issuer matches token: True

The token is cryptographically verified using the RSA public key from the JWKS endpoint. The claims match the user we created: email alice@example.com, username alice, token_use id. The issuer from the OIDC discovery matches the token issuer.

Step 10: Group management

Terminal
$ awsemu cognito-idp create-group \
    --user-pool-id $POOL_ID \
    --group-name admins \
    --description "Admin users"

Group:
  GroupName: admins
  UserPoolId: us-east-1_72c878283811431980a7e118647f6190
  Description: Admin users

$ awsemu cognito-idp admin-add-user-to-group \
    --user-pool-id $POOL_ID \
    --username alice \
    --group-name admins

$ awsemu cognito-idp admin-list-groups-for-user \
    --user-pool-id $POOL_ID \
    --username alice

Groups:
  - GroupName: admins
    Description: Admin users

Create groups, add users, and list memberships. Use groups for role-based access control in your application.

Step 11: List users

Terminal
$ awsemu cognito-idp list-users --user-pool-id $POOL_ID

Users:
  - Username: alice
    email: alice@example.com
    sub: 9cf943c2-7bdd-4f4a-ba1e-fef296a40b46
    Enabled: true
    UserStatus: CONFIRMED

Alice is now CONFIRMED after the permanent password was set.

Cleanup (separate script: teardown.sh)

Terminal
$ awsemu cognito-idp delete-user-pool --user-pool-id $POOL_ID

$ awsemu cognito-idp list-user-pools --max-results 10

{
    "UserPools": []
}
(empty - pool deleted)

The 10-step demo.sh leaves the pool in place so you can poke at it. Run scripts/teardown.sh to drop the pool, its users, clients, and groups.

Python Integration

A complete example showing how an application verifies Cognito tokens. This pattern works identically against LocalEmu and real AWS Cognito. Just change the endpoint URL for production.

cognito_auth.py
import os

import boto3
import jwt as pyjwt
import requests
from jwt import PyJWK

# Set AWS_ENDPOINT_URL=http://localhost:4566 in your shell and the same
# code talks to LocalEmu; unset it and it talks to real AWS Cognito.
ENDPOINT = os.environ.get("AWS_ENDPOINT_URL", "http://localhost:4566")

def get_cognito_client():
    return boto3.client("cognito-idp", endpoint_url=ENDPOINT, region_name="us-east-1")

def verify_token(pool_id, client_id, token):
    """Verify a Cognito JWT using the JWKS endpoint.
    Works identically against LocalEmu and real AWS Cognito.
    """
    # Fetch the public keys from JWKS endpoint
    jwks_url = f"{ENDPOINT}/{pool_id}/.well-known/jwks.json"
    jwks = requests.get(jwks_url).json()

    # Match the token's key ID to the right public key
    header = pyjwt.get_unverified_header(token)
    for key_data in jwks['keys']:
        if key_data['kid'] == header['kid']:
            jwk_key = PyJWK.from_dict(key_data)
            return pyjwt.decode(
                token, jwk_key,
                algorithms=['RS256'],
                audience=client_id
            )
    raise ValueError("No matching key found in JWKS")

# Example: authenticate and verify
client = get_cognito_client()
result = client.admin_initiate_auth(
    UserPoolId="us-east-1_72c878283811431980a7e118647f6190",
    ClientId="5381380d005c437fb685b82aa7",
    AuthFlow='ADMIN_USER_PASSWORD_AUTH',
    AuthParameters={'USERNAME': 'alice', 'PASSWORD': 'MySecret123!'})

id_token = result['AuthenticationResult']['IdToken']
claims = verify_token(
    "us-east-1_72c878283811431980a7e118647f6190",
    "5381380d005c437fb685b82aa7",
    id_token
)

print(f"Authenticated: {claims['cognito:username']}")
print(f"Email: {claims['email']}")
print(f"Token use: {claims['token_use']}")

How It Works

1. Pool creation generates an RSA key pair - each user pool gets its own 2048-bit RSA private/public key pair, stored in memory
2. Sign-in produces real JWTs - admin-initiate-auth signs AccessToken and IdToken with the pool's RSA private key using RS256
3. JWKS endpoint serves public keys - GET /pool-id/.well-known/jwks.json returns the RSA public key in JWK format with the matching kid
4. OIDC discovery is auto-configured - GET /pool-id/.well-known/openid-configuration returns issuer, jwks_uri, scopes, and supported claims
5. Any JWT library can verify - fetch the public key from JWKS, match the kid from the token header, verify the RS256 signature
6. Token claims match real Cognito - sub, email, cognito:username, token_use, iss, aud, exp, iat are all present and correct

Token shape. The tokens are RS256-signed JWTs with the standard JOSE headers (alg, kid, typ) and the claim set Cognito issues in production: sub, iss, aud, exp, iat, cognito:username, cognito:groups, token_use, email. Signature is per-pool: each user pool gets its own RSA key pair and serves the matching public key from /<pool-id>/.well-known/jwks.json, so a verifier wired up for real Cognito Just Works against LocalEmu.