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.
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
localemu start Cognito is available out of the box. No special environment variables needed.
Step 2: Create a User Pool
$ 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
$ 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
$ 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
$ 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
$ 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
$ 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
$ 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
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']}") $ 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
$ 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
$ 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)
$ 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.
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
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.