Skip to main content

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

{
"alg": "RS256",
"typ": "JWT",
"kid": "abc123def456..."
}
FieldDescriptionValidation
algAlgorithmMust be "RS256"
typToken typeMust be "JWT"
kidKey IDUsed 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

ClaimFull NameDescriptionValidation
subSubjectUnique user identifier (Cognito UUID)Required, non-empty
issIssuerToken issuer URLMust match Cognito pool URL
audAudienceIntended recipientMust match app client ID
expExpirationToken expiry timestampMust be in future
iatIssued AtToken issue timestampMust be in past
nbfNot BeforeToken valid fromMust be in past (if present)
jtiJWT IDUnique token identifierUsed for revocation tracking

Custom Claims (ASCEND-Specific)

ClaimTypeRequiredDescription
custom:organization_idNumberYesTenant isolation identifier
custom:organization_slugStringYesURL-safe organization name
custom:roleStringYesRBAC role (admin, user, etc.)
custom:is_org_adminStringNo"true" if organization admin
token_useStringYesMust 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

PropertyValue
AlgorithmRS256 (RSASSA-PKCS1-v1_5 with SHA-256)
Key TypeRSA public key
Key Size2048+ bits (Cognito default)
Key SourceJWKS 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.

PropertyValue
UseAPI authentication
Expiration1 hour (default)
ContainsUser identity, custom claims
ValidationFull signature + claims validation

Access Token

Purpose: Resource access authorization (Cognito APIs).

PropertyValue
UseCognito API calls
Expiration1 hour (default)
ContainsScopes, resource permissions
ValidationUsually handled by Cognito

Refresh Token

Purpose: Obtain new ID and Access tokens without re-authentication.

PropertyValue
UseToken refresh
Expiration30 days (default)
ContainsEncrypted session data
StorageSecure, 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 StepFailure ScenarioResponse
Token extractionMissing/malformed header401 - Missing Authorization header
Header decodeInvalid JWT format401 - Invalid token format
JWKS fetchNetwork error503 - Auth service unavailable
Key lookupUnknown kid401 - Invalid token (after refresh)
Signature verificationInvalid signature401 - Invalid authentication token
Issuer validationWrong issuer401 - Invalid token claims
Audience validationWrong audience401 - Invalid token claims
Expiration checkToken expired401 - Token has expired
Custom claimsMissing org_id401 - Token missing required attribute
Revocation checkToken revoked401 - Token has been revoked
Organization lookupOrg not found403 - Organization not found
Subscription checkOrg suspended403 - Organization suspended

Compliance Mapping

FrameworkControlImplementation
SOC 2CC6.1RS256 cryptographic validation
HIPAA164.312(d)Token-based person authentication
PCI-DSSReq 8.2Strong cryptographic authentication
NIST 800-63AAL2Multi-factor + cryptographic token
NIST 800-63AAL3Hardware-backed cryptographic verification
OWASP ASVSV3.5Token-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