JWT Token Management
Overview
ASCEND implements enterprise-grade JWT (JSON Web Token) management with RS256 asymmetric cryptographic signatures, comprehensive claims validation, token revocation support, and complete audit logging. All JWT operations are designed to fail-secure - any validation error results in authentication denial.
Why It Matters
JWT tokens are the primary authentication mechanism for interactive users. Proper JWT management ensures:
- Tamper Prevention: RS256 signatures prevent token modification
- Identity Verification: Claims validation confirms user identity and permissions
- Session Control: Revocation support enables immediate access termination
- Audit Compliance: Token tracking supports forensic investigation
- Multi-Tenancy: Organization claims enable tenant isolation
Architecture
Token Flow
+------------------+ +------------------+ +------------------+
| Token Issuance | | Token Storage | | Token Usage |
| (AWS Cognito) | | (Client/Cookie) | | (API Request) |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
| 1. User authenticates | |
+----------------------->| |
| | |
| 2. Tokens issued | |
| (ID, Access, Refresh)| |
+----------------------->| |
| | |
| | 3. Client stores |
| | tokens securely |
| | |
| | 4. Request with |
| | Bearer token |
| +----------------------->|
| | |
| | +------+------+
| | | Validation |
| | +------+------+
| | |
| | 5. Response |
| |<-----------------------+
Validation Pipeline
+----------------+
| Extract Token |
| from Header |
+-------+--------+
|
v
+-------+--------+
| Decode Header |
| (get kid) |
+-------+--------+
|
v
+-------+--------+
| Fetch/Cache |
| JWKS Keys |
+-------+--------+
|
v
+-------+--------+
| Verify RS256 |
| Signature |
+-------+--------+
|
v
+-------+--------+
| Validate |
| Standard Claims|
| (iss, aud, exp)|
+-------+--------+
|
v
+-------+--------+
| Validate |
| Custom Claims |
| (org_id, role) |
+-------+--------+
|
v
+-------+--------+
| Check Token |
| Revocation |
+-------+--------+
|
v
+-------+--------+
| Return User |
| Context |
+----------------+
JWT Structure
Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "abc123def456..."
}
| Field | Description | Validation |
|---|---|---|
alg | Algorithm | Must be "RS256" |
typ | Token type | Must be "JWT" |
kid | Key ID | Used to select JWKS key |
Payload (Claims)
{
"sub": "12345678-1234-1234-1234-123456789012",
"aud": "your-app-client-id",
"email_verified": true,
"token_use": "id",
"auth_time": 1705766400,
"iss": "https://cognito-idp.us-east-2.amazonaws.com/us-east-2_xxxxxxxx",
"cognito:username": "user@example.com",
"exp": 1705770000,
"iat": 1705766400,
"jti": "unique-token-id",
"email": "user@example.com",
"custom:organization_id": "123",
"custom:organization_slug": "acme-corp",
"custom:role": "admin",
"custom:is_org_admin": "true"
}
Standard Claims
| Claim | Full Name | Description | Validation |
|---|---|---|---|
sub | Subject | Unique user identifier (Cognito UUID) | Required, non-empty |
iss | Issuer | Token issuer URL | Must match Cognito pool URL |
aud | Audience | Intended recipient | Must match app client ID |
exp | Expiration | Token expiry timestamp | Must be in future |
iat | Issued At | Token issue timestamp | Must be in past |
nbf | Not Before | Token valid from | Must be in past (if present) |
jti | JWT ID | Unique token identifier | Used for revocation tracking |
Custom Claims (ASCEND-Specific)
| Claim | Type | Required | Description |
|---|---|---|---|
custom:organization_id | Number | Yes | Tenant isolation identifier |
custom:organization_slug | String | Yes | URL-safe organization name |
custom:role | String | Yes | RBAC role (admin, user, etc.) |
custom:is_org_admin | String | No | "true" if organization admin |
token_use | String | Yes | Must be "id" for ID tokens |
Configuration
Environment Variables
# Cognito JWT Configuration
COGNITO_REGION=us-east-2
COGNITO_USER_POOL_ID=us-east-2_xxxxxxxxx
COGNITO_APP_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
# Derived URLs (computed automatically)
# COGNITO_ISSUER=https://cognito-idp.{region}.amazonaws.com/{pool_id}
# COGNITO_JWKS_URL={issuer}/.well-known/jwks.json
# Token Expiration (configured in Cognito)
# Access Token: 1 hour
# ID Token: 1 hour
# Refresh Token: 30 days
# Validation Options
JWT_VALIDATION_TIMEOUT_MS=3000
JWT_CLOCK_SKEW_SECONDS=60
Validation Options
# JWT decode options
options = {
"verify_signature": True, # Always verify RS256 signature
"verify_aud": True, # Verify audience claim
"verify_iat": True, # Verify issued-at claim
"verify_exp": True, # Verify expiration claim
"verify_nbf": True, # Verify not-before claim
"verify_iss": True, # Verify issuer claim
"require_aud": True, # Audience must be present
"require_exp": True, # Expiration must be present
"require_iat": True # Issued-at must be present
}
RS256 Signature Verification
Algorithm Details
| Property | Value |
|---|---|
| Algorithm | RS256 (RSASSA-PKCS1-v1_5 with SHA-256) |
| Key Type | RSA public key |
| Key Size | 2048+ bits (Cognito default) |
| Key Source | JWKS endpoint |
JWKS Key Caching
@lru_cache(maxsize=1)
def get_cognito_public_keys() -> Dict[str, RSAKey]:
"""
Fetch and cache AWS Cognito public keys for JWT signature validation.
Performance:
- Cached to reduce latency (keys rarely change)
- Auto-refreshes on signature validation failure
Security:
- Fetches from official Cognito JWKS endpoint
- Verifies RS256 algorithm
"""
response = requests.get(COGNITO_JWKS_URL, timeout=10)
response.raise_for_status()
keys = response.json()["keys"]
public_keys = {}
for key_data in keys:
kid = key_data["kid"]
public_keys[kid] = jwk.construct(key_data)
return public_keys
Key Rotation Handling
def validate_cognito_token(token: str, db: Session) -> dict:
# Decode header to get key ID
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header.get("kid")
# Get cached public keys
public_keys = get_cognito_public_keys()
# If key not found, refresh cache (handles Cognito key rotation)
if kid not in public_keys:
public_keys = refresh_cognito_keys()
if kid not in public_keys:
raise JWTError(f"Public key not found for kid: {kid}")
public_key = public_keys[kid]
# Verify signature and validate claims
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience=COGNITO_APP_CLIENT_ID,
issuer=COGNITO_ISSUER,
options={...}
)
return payload
Token Types
ID Token
Purpose: Identity assertion and custom claims access.
| Property | Value |
|---|---|
| Use | API authentication |
| Expiration | 1 hour (default) |
| Contains | User identity, custom claims |
| Validation | Full signature + claims validation |
Access Token
Purpose: Resource access authorization (Cognito APIs).
| Property | Value |
|---|---|
| Use | Cognito API calls |
| Expiration | 1 hour (default) |
| Contains | Scopes, resource permissions |
| Validation | Usually handled by Cognito |
Refresh Token
Purpose: Obtain new ID and Access tokens without re-authentication.
| Property | Value |
|---|---|
| Use | Token refresh |
| Expiration | 30 days (default) |
| Contains | Encrypted session data |
| Storage | Secure, HttpOnly cookie or secure storage |
Token Revocation
Revocation Database Schema
CREATE TABLE cognito_tokens (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
cognito_user_id VARCHAR(255) NOT NULL,
organization_id INTEGER NOT NULL,
token_jti VARCHAR(255) UNIQUE NOT NULL,
token_type VARCHAR(50) NOT NULL,
issued_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
is_revoked BOOLEAN DEFAULT FALSE,
revoked_at TIMESTAMP,
revocation_reason VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_cognito_tokens_jti ON cognito_tokens(token_jti);
CREATE INDEX idx_cognito_tokens_user ON cognito_tokens(user_id);
Revocation Check
async def check_token_revoked(token_jti: str, db: Session) -> bool:
"""
Check if token has been revoked.
Returns True if revoked, False if valid.
"""
token_record = db.query(CognitoToken).filter(
CognitoToken.token_jti == token_jti
).first()
if token_record and token_record.is_revoked:
logger.warning(f"Revoked token used: {token_jti}")
return True
return False
Revocation API
# Revoke specific token
curl -X POST https://api.ascend.io/v1/auth/revoke-token \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"token_jti": "unique-token-id",
"reason": "Security incident - credential compromise"
}'
# Revoke all tokens for user
curl -X POST https://api.ascend.io/v1/auth/revoke-user-tokens \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user_id": 123,
"reason": "Account compromised"
}'
User Context Extraction
Extracted Fields
user_context = {
"user_id": user.id, # Database user ID
"cognito_user_id": payload["sub"], # Cognito UUID
"email": payload["email"], # User email
"organization_id": int(payload["custom:organization_id"]),
"organization_slug": payload["custom:organization_slug"],
"organization_name": org.name, # From database
"role": payload.get("custom:role", "user"),
"is_org_admin": payload.get("custom:is_org_admin") == "true",
"auth_method": "cognito",
"subscription_tier": org.subscription_tier, # From database
"jti": payload.get("jti"), # For revocation
"exp": payload.get("exp"), # Expiration
"iat": payload.get("iat") # Issued at
}
RLS Context Setting
# Set PostgreSQL Row-Level Security context for multi-tenancy
db.execute(text(f"SET LOCAL app.current_organization_id = {organization_id}"))
# All subsequent queries in this transaction are now tenant-isolated
Fail-Secure Behavior
| Validation Step | Failure Scenario | Response |
|---|---|---|
| Token extraction | Missing/malformed header | 401 - Missing Authorization header |
| Header decode | Invalid JWT format | 401 - Invalid token format |
| JWKS fetch | Network error | 503 - Auth service unavailable |
| Key lookup | Unknown kid | 401 - Invalid token (after refresh) |
| Signature verification | Invalid signature | 401 - Invalid authentication token |
| Issuer validation | Wrong issuer | 401 - Invalid token claims |
| Audience validation | Wrong audience | 401 - Invalid token claims |
| Expiration check | Token expired | 401 - Token has expired |
| Custom claims | Missing org_id | 401 - Token missing required attribute |
| Revocation check | Token revoked | 401 - Token has been revoked |
| Organization lookup | Org not found | 403 - Organization not found |
| Subscription check | Org suspended | 403 - Organization suspended |
Compliance Mapping
| Framework | Control | Implementation |
|---|---|---|
| SOC 2 | CC6.1 | RS256 cryptographic validation |
| HIPAA | 164.312(d) | Token-based person authentication |
| PCI-DSS | Req 8.2 | Strong cryptographic authentication |
| NIST 800-63 | AAL2 | Multi-factor + cryptographic token |
| NIST 800-63 | AAL3 | Hardware-backed cryptographic verification |
| OWASP ASVS | V3.5 | Token-based session management |
Verification
Decode Token (for debugging)
# Decode JWT without verification (debugging only)
echo "$JWT_TOKEN" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
# Output
{
"sub": "12345678-1234-1234-1234-123456789012",
"email": "user@example.com",
"custom:organization_id": "123",
"custom:role": "admin",
"exp": 1705770000,
...
}
Verify Token via API
curl -X GET https://api.ascend.io/v1/auth/verify-token \
-H "Authorization: Bearer $JWT_TOKEN"
# Response
{
"valid": true,
"expires_at": "2026-01-20T11:00:00Z",
"user_id": 123,
"organization_id": 1,
"role": "admin",
"token_jti": "unique-token-id"
}
Check Token Revocation Status
curl -X GET https://api.ascend.io/v1/auth/token-status \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"token_jti": "unique-token-id"
}'
# Response
{
"token_jti": "unique-token-id",
"is_revoked": false,
"user_id": 123,
"issued_at": "2026-01-20T10:00:00Z",
"expires_at": "2026-01-20T11:00:00Z"
}
Next Steps
- API Key Authentication - SDK and CI/CD authentication
- Authorization Overview - RBAC configuration
- RBAC Reference - Role hierarchy details