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
httpxfor HTTP andpydanticfor configuration - The existing
JwtClaimsDtoalready handles Cognito-specific claim mapping (cognito:groupsvia alias) - Adapter must satisfy
TokenValidatorandPrincipalResolverProtocols 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:
- GETs the JWKS endpoint (
/.well-known/jwks.json) - 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 cachingtests/adapters/cognito/test_principal_resolver.py— 6 tests covering human/machine actor resolutiontests/test_integration_cognito.py— 3 end-to-end tests wiring Cognito adapters throughAuthenticationService- 141 total tests pass with no regressions
More Information
- joserfc documentation
- Related: ADR-0004 Strategy Pattern, ADR-0005 Result-Type Dataclasses, ADR-0007 In-Memory Adapters
- Affected files:
src/hdc_auth_proxy/adapters/cognito/