Skip to content

Cognito JWT Adapter — Library and Testing Choices

Context and Problem Statement

The Adapter Layer Phase 2 requires concrete implementations of TokenValidator and PrincipalResolver for AWS Cognito. The adapter must validate RS256-signed JWTs against the Cognito JWKS endpoint, verify standard claims (iss, aud, exp), and map Cognito-specific claims to domain models.

Several JWT libraries and testing approaches exist. Which combination provides correctness, RFC compliance, and fast test feedback without requiring AWS infrastructure?

Decision Drivers

  • Cognito JWTs follow OIDC/RFC 7519 standards — the library must handle RS256, JWKS, and standard claims correctly
  • Tests must run locally without AWS credentials or network access
  • The project already uses httpx for HTTP and pydantic for configuration
  • The existing JwtClaimsDto already handles Cognito-specific claim mapping (cognito:groups via alias)
  • Adapter must satisfy TokenValidator and PrincipalResolver Protocols without touching the domain layer

Considered Options

JWT Library

  • joserfc — RFC-compliant JOSE library from the authlib team
  • PyJWT — Widely-used, simpler API
  • python-jose — Older, multiple backend support

Testing Strategy

  • Local RSA keys + pytest-httpx — Generate keys locally, mock JWKS HTTP endpoint
  • LocalStack / moto — Simulate full Cognito API
  • respx — Alternative HTTP mocking for httpx

Decision Outcome

Chosen options: joserfc for JWT handling, local RSA keys + pytest-httpx for testing.

Why joserfc

Criterion joserfc PyJWT
RFC compliance Full RFC 7515/7516/7517/7518/7519 Partial
Decode/verify separation extract_compact() for header, jwt.decode() for verify Combined API
JWKS support Native KeySet.import_key_set() Requires PyJWKClient wrapper
Key types Key union type (RSAKey, ECKey, ...) Dict-based
Maintenance Active (authlib team) Active

joserfc separates header extraction (extract_compact().headers()) from signature verification (jwt.decode()), which maps cleanly to the validator's flow: extract kid → find key in JWKS → verify signature.

Why NOT LocalStack

CognitoTokenValidator never calls Cognito APIs (CreateUserPool, InitiateAuth, etc.). It only:

  1. GETs the JWKS endpoint (/.well-known/jwks.json)
  2. Verifies RSA signatures locally

Local RSA key generation + HTTP mocking is sufficient, faster, and avoids Docker/Java dependencies. LocalStack becomes relevant when implementing OIDC login flows (authorization code + PKCE) that require Cognito API interaction.

Why pytest-httpx over respx

Both are capable httpx mocking libraries. pytest-httpx was chosen for alignment with the project's fixture-based test pattern — it provides httpx_mock as a pytest fixture, matching the approach used for cognito_settings, sign_token, and other test infrastructure.

Implementation

CognitoTokenValidator (adapters/cognito/token_validator.py):

validate(token) → TokenValidationResult
├── extract_compact(token) → headers → kid
├── _find_key(kid) → Key (fetch JWKS if not cached)
├── jwt.decode(token, key, algorithms=["RS256"])
├── validate claims: iss, aud, exp
└── JwtClaimsDto.model_validate(claims).to_domain()

JWKS caching: fetched once, invalidated and re-fetched on unknown kid (single retry).

CognitoPrincipalResolver (adapters/cognito/principal_resolver.py):

Pure logic — no network. Maps TokenClaims.custom_claims to Principal: - email present → ActorType.HUMAN - No email, client_id present → ActorType.MACHINE - Provider: always IdentityProvider.COGNITO

Consequences

  • Good, because the adapter satisfies existing Protocols without domain changes
  • Good, because joserfc's API maps naturally to the validation flow
  • Good, because tests run in ~0.5s with no external dependencies
  • Good, because JwtClaimsDto (existing) handles all Cognito-specific claim mapping
  • Neutral, because joserfc is less widely known than PyJWT (but well-maintained)
  • Bad, because JWKS caching is simple (no TTL) — sufficient for now, may need enhancement for high-traffic production use

Confirmation

  • tests/adapters/cognito/test_token_validator.py — 10 tests covering valid tokens, claim mapping, rejection cases, and JWKS caching
  • tests/adapters/cognito/test_principal_resolver.py — 6 tests covering human/machine actor resolution
  • tests/test_integration_cognito.py — 3 end-to-end tests wiring Cognito adapters through AuthenticationService
  • 141 total tests pass with no regressions

More Information