Development Guide
Prerequisites
- Python >= 3.13
- uv package manager
uv sync --dev
Running the proxy
uv run hdc-auth-proxy
The proxy starts on http://0.0.0.0:8000 and forwards authenticated requests to the upstream service.
Configuration
All settings are loaded from environment variables via pydantic-settings. Source: src/hdc_auth_proxy/adapters/settings.py.
Proxy settings
Prefix: HDC_PROXY_
| Variable | Type | Default | Description |
|---|---|---|---|
HDC_PROXY_UPSTREAM_URL |
str |
http://localhost:8080 |
Upstream service URL to proxy requests to |
HDC_PROXY_HOST |
str |
0.0.0.0 |
Server bind address |
HDC_PROXY_PORT |
int |
8000 |
Server bind port |
HDC_PROXY_API_KEY_HEADER |
str |
X-API-Key |
HTTP header name for API key authentication |
HDC_PROXY_LOG_LEVEL |
str |
INFO |
Uvicorn log level (DEBUG, INFO, WARNING, ERROR) |
Cognito settings
Prefix: COGNITO_
| Variable | Type | Default | Description |
|---|---|---|---|
COGNITO_USER_POOL_ID |
str |
— | AWS Cognito User Pool ID (e.g. eu-west-1_abc123) |
COGNITO_REGION |
str |
eu-west-1 |
AWS region where the User Pool is hosted |
COGNITO_CLIENT_ID |
str |
— | Cognito App Client ID for audience validation |
COGNITO_JWKS_URI |
str |
(derived) | JWKS endpoint URL; auto-derived from pool ID + region if not set |
COGNITO_DOMAIN |
str |
— | Cognito hosted UI domain (enables OIDC browser login) |
COGNITO_CALLBACK_URI |
str |
— | OAuth2 redirect URI (must match Cognito app client config) |
COGNITO_CLIENT_SECRET |
str |
— | Client secret (for confidential clients; leave empty for public) |
COGNITO_SCOPES |
str |
openid profile email |
OAuth2 scopes to request |
When COGNITO_JWKS_URI is empty, it is derived as:
https://cognito-idp.{region}.amazonaws.com/{user_pool_id}/.well-known/jwks.json
Session settings
Prefix: SESSION_
| Variable | Type | Default | Description |
|---|---|---|---|
SESSION_SECRET_KEY |
str |
— | Required for OIDC login. Signs session cookies with HMAC-SHA256 |
SESSION_COOKIE_NAME |
str |
hdc_session |
Name of the session cookie |
SESSION_HTTPONLY |
bool |
true |
HttpOnly flag on the cookie |
SESSION_SECURE |
bool |
true |
Secure flag (set to false for local HTTP development) |
SESSION_SAMESITE |
str |
lax |
SameSite policy |
SESSION_MAX_AGE_SECONDS |
int |
86400 |
Cookie lifetime in seconds (default 24h) |
PKCE settings
Prefix: PKCE_
| Variable | Type | Default | Description |
|---|---|---|---|
PKCE_CHALLENGE_METHOD |
str |
S256 |
PKCE challenge method (only S256 supported) |
PKCE_STATE_TTL_SECONDS |
int |
600 |
Time-to-live for pending PKCE states (10 min) |
PKCE_MAX_PENDING |
int |
1000 |
Max concurrent pending PKCE flows |
Environment-based configuration
The proxy uses a two-phase settings loader controlled by HDC_PROXY_ENV_STATE (see ADR-0012).
How settings are loaded (load_settings() in main()):
.envis loaded first as the base config (override=False— does not clobber process env).env.{state}is loaded on top as state-specific overrides (override=True)ProxySettings()reads fromos.environ, so every nested model (CognitoSettings,SessionConfig,PKCEConfig) picks up its own prefixed vars
Priority (highest to lowest):
- Process environment variables (already set before startup)
.env.{state}file values.envbase file values- Field defaults in settings classes
Adapter selection priority (evaluated in order):
| Priority | Condition | Adapters |
|---|---|---|
| 1 | COGNITO_USER_POOL_ID is set |
Cognito JWT + Cognito principal resolver |
| 2 | env_state = dev |
In-memory with pre-populated test data |
| 3 | (fallback) | Empty in-memory (all requests → 401) |
Cognito always takes precedence — even in dev mode. This allows local testing against a real User Pool.
Concrete scenarios
| Scenario | .env contains |
state resolved |
Also loads | Result |
|---|---|---|---|---|
| Dev (no Cognito) | HDC_PROXY_ENV_STATE=dev |
dev |
.env.dev |
In-memory test data, dev banner |
| Dev + real Cognito | HDC_PROXY_ENV_STATE=dev + COGNITO_* |
dev |
.env.dev |
Cognito adapters, OIDC routes if COGNITO_DOMAIN set |
| Prod (implicit) | COGNITO_* (no ENV_STATE) |
prod |
.env.prod (if exists) |
Cognito adapters |
| Prod (explicit) | HDC_PROXY_ENV_STATE=prod + COGNITO_* |
prod |
.env.prod (if exists) |
Cognito adapters |
| Prod (no Cognito) | (empty) | prod |
.env.prod (if exists) |
Empty in-memory → all requests 401 |
The recommended local setup for testing against a real Cognito User Pool: put all credentials in .env (gitignored) and set HDC_PROXY_ENV_STATE=dev there. .env.dev (committed) provides dev-friendly defaults for upstream URL and log level. Cognito takes precedence automatically.
Dev mode
Start with pre-populated test credentials:
HDC_PROXY_ENV_STATE=dev uv run hdc-auth-proxy
Or use the committed .env.dev file (which sets HDC_PROXY_ENV_STATE=dev automatically):
uv run hdc-auth-proxy # picks up .env.dev if HDC_PROXY_ENV_STATE=dev
The proxy logs a WARNING banner with test credentials at startup:
========================================================
DEV MODE ACTIVE -- NOT FOR PRODUCTION
Pre-populated test data loaded. Auth is NOT secure.
========================================================
Dev credentials:
Bearer token : dev-token
API key : dev-api-key
Subject : dev-user (analyst role, read:data scope)
Test scenarios (curl)
# 401 — no credentials
curl http://localhost:8000/v1/data/indicators
# 200 — valid Bearer token (analyst can read data)
curl -H "Authorization: Bearer dev-token" http://localhost:8000/v1/data/indicators
# 401 — invalid token
curl -H "Authorization: Bearer bad-token" http://localhost:8000/v1/data/indicators
# 403 — analyst cannot access admin endpoints
curl -H "Authorization: Bearer dev-token" http://localhost:8000/v1/admin/users
# 200 — API key on public resource
curl -H "X-API-Key: dev-api-key" http://localhost:8000/public/health
Expected results
Without a running upstream service (default http://localhost:8080), the expected responses are:
| Test | curl | Expected | Why |
|---|---|---|---|
| No credentials | curl /v1/data/indicators |
401 | No Authorization or X-API-Key header |
| Valid Bearer token | curl -H "Authorization: Bearer dev-token" /v1/data/indicators |
502 | Authenticated + authorized, but no upstream |
| Invalid token | curl -H "Authorization: Bearer bad-token" /v1/data/indicators |
401 | Token not recognized |
| Analyst → admin | curl -H "Authorization: Bearer dev-token" /v1/admin/users |
403 | Analyst role cannot access /v1/admin/* |
| API key → public | curl -H "X-API-Key: dev-api-key" /public/health |
502 | Public resource, auth bypassed, but no upstream |
Note
502 means authentication and authorization succeeded — the proxy tried to forward to the upstream service but it wasn't running. With a running upstream you'll get 200.
Examples
Using a .env file (recommended)
Put all vars in a single .env file and run without any inline exports:
# .env
COGNITO_USER_POOL_ID=eu-west-1_abc123
COGNITO_CLIENT_ID=my-app-client
HDC_PROXY_UPSTREAM_URL=http://localhost:9000
SESSION_SECRET_KEY=my-secret
uv run hdc-auth-proxy # .env is loaded automatically
With Cognito JWT validation (inline env vars)
COGNITO_USER_POOL_ID=eu-west-1_abc123 \
COGNITO_CLIENT_ID=my-app-client \
HDC_PROXY_UPSTREAM_URL=http://localhost:9000 \
uv run hdc-auth-proxy
Dev mode with real Cognito (Cognito takes precedence)
HDC_PROXY_ENV_STATE=dev \
COGNITO_USER_POOL_ID=eu-west-1_abc123 \
COGNITO_CLIENT_ID=my-app-client \
uv run hdc-auth-proxy
With OIDC PKCE browser login
When COGNITO_DOMAIN is set, the proxy exposes three auth routes for browser-based login (ADR-0015):
COGNITO_USER_POOL_ID=eu-west-1_abc123 \
COGNITO_CLIENT_ID=my-app-client \
COGNITO_DOMAIN=mypool.auth.eu-west-1.amazoncognito.com \
COGNITO_CALLBACK_URI=http://localhost:8000/auth/callback \
SESSION_SECRET_KEY=dev-secret-key \
uv run hdc-auth-proxy
| Route | Method | Purpose |
|---|---|---|
/auth/login |
GET | Redirect to Cognito with PKCE S256 |
/auth/callback |
GET | Exchange code for tokens, set session cookie |
/auth/logout |
POST | Clear session cookie |
/auth/refresh |
POST | Exchange refresh token for new access/id tokens |
Browser flow: open http://localhost:8000/auth/login → authenticate at Cognito → redirected back with session cookie.
Warning
SESSION_SECRET_KEY is required for OIDC browser login. It signs the session cookie with HMAC-SHA256. Use a strong random value in production.
Manual testing with a real Cognito User Pool
To test the full OIDC PKCE browser login flow against a real AWS Cognito User Pool:
1. Create the User Pool
aws cognito-idp create-user-pool \
--pool-name hdc-auth-pool \
--username-attributes email \
--auto-verified-attributes email \
--region eu-west-1
Note the UserPool.Id (e.g. eu-west-1_abc123).
2. Create the App Client (public, PKCE-enabled)
aws cognito-idp create-user-pool-client \
--user-pool-id eu-west-1_abc123 \
--client-name hdc-auth-proxy \
--no-generate-secret \
--explicit-auth-flows ALLOW_USER_SRP_AUTH ALLOW_REFRESH_TOKEN_AUTH \
--allowed-o-auth-flows code \
--allowed-o-auth-scopes openid profile email \
--allowed-o-auth-flows-user-pool-client \
--callback-urls '["http://localhost:8000/auth/callback"]' \
--supported-identity-providers COGNITO \
--region eu-west-1
Note the UserPoolClient.ClientId.
3. Configure the hosted UI domain
aws cognito-idp create-user-pool-domain \
--user-pool-id eu-west-1_abc123 \
--domain hdc-auth-dev \
--region eu-west-1
The domain will be: hdc-auth-dev.auth.eu-west-1.amazoncognito.com
4. Create a test user
aws cognito-idp admin-create-user \
--user-pool-id eu-west-1_abc123 \
--username analyst@wfp.org \
--user-attributes Name=email,Value=analyst@wfp.org Name=email_verified,Value=true \
--temporary-password 'TempPass123!' \
--region eu-west-1
aws cognito-idp admin-set-user-password \
--user-pool-id eu-west-1_abc123 \
--username analyst@wfp.org \
--password 'YourPermanentPass123!' \
--permanent \
--region eu-west-1
5. Configure .env
Create a .env file in the project root with your Cognito settings. This is the primary config file for production Cognito configuration — uv run hdc-auth-proxy loads it automatically:
COGNITO_USER_POOL_ID=eu-west-1_abc123
COGNITO_REGION=eu-west-1
COGNITO_CLIENT_ID=your-client-id-here
COGNITO_DOMAIN=hdc-auth-dev.auth.eu-west-1.amazoncognito.com
COGNITO_CALLBACK_URI=http://localhost:8000/auth/callback
COGNITO_SCOPES=openid profile email
SESSION_SECRET_KEY=your-secret-key-here
SESSION_SECURE=false
Add .env to your .gitignore — it contains secrets. Generate a strong SESSION_SECRET_KEY:
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
6. Start the proxy and test
uv run hdc-auth-proxy # loads .env automatically, then .env.prod on top (if it exists)
Open http://localhost:8000/auth/login in a browser. You should be redirected to the Cognito hosted UI. After authentication:
- The callback sets a session cookie (
hdc_session) - Subsequent requests use the cookie for authentication
/auth/refreshrenews the session when the access token expires/auth/logoutclears the session cookie
Verify with curl
# Login redirect (should return 302 to Cognito)
curl -v http://localhost:8000/auth/login 2>&1 | grep -i location
# After browser login, check the session cookie works:
curl -b "hdc_session=<cookie-value>" http://localhost:8000/v1/data/indicators
Request flow
Once the proxy is running, every request goes through:
Client → ProxyHandler
├── Extract credential (Authorization: Bearer or X-API-Key header)
│ └── Missing → 401
├── Authenticate (validate token or API key → resolve Principal)
│ └── Invalid → 401
├── Authorize (match resource + permission against policy rules)
│ └── Denied → 403
└── Forward to upstream → StreamingResponse
└── Upstream error → 502
See Ports and Services for the full architecture.
Testing
The test suite uses a four-tier strategy. Tiers 1-3 require no Docker. See ADR-0016 for the full evaluation.
Run all tests
uv run pytest -m "not e2e" # Tiers 1-3 (no Docker)
uv run pytest # All tiers (requires Docker for Tier 4)
uv run pytest -m e2e # Tier 4 only (requires Docker)
Tier 1 — Unit tests (stubs + pytest-httpx)
Fast, isolated. Covers domain, services, adapters, wiring.
uv run pytest tests/ -v --ignore=tests/test_integration_oidc.py --ignore=tests/test_integration_cognito.py
Tier 2 — Integration OIDC (pytest-iam)
Full PKCE S256 flow against a real OIDC provider (Canaille) running in-thread.
uv run pytest tests/test_integration_oidc.py -v
Tier 3 — Integration Cognito (moto)
Cognito-specific JWT behavior: issuer format, JWKS, cognito:groups, AdminInitiateAuth tokens.
uv run pytest tests/test_integration_cognito.py -v
Tier 4 — E2E Cognito (magnito + testcontainers)
Full Cognito auth flow against magnito Docker emulator. SRP authentication, token verification via JWKS, principal resolution. Requires Docker.
uv run pytest tests/e2e/ -m e2e -v
Run by adapter
uv run pytest tests/adapters/http/ -v # HTTP adapter
uv run pytest tests/adapters/oidc/ -v # OIDC adapter
uv run pytest tests/adapters/cognito/ -v # Cognito adapter
uv run pytest tests/adapters/memory/ -v # In-memory adapters
Quality checks
uv run ruff format --check . # formatting
uv run ruff check . # linting
uv run ty check # type checking
Architecture reference
- Domain Layer — Bounded contexts and domain models
- Ports and Services — Protocols, services, and testing tiers
- ADR-0016 Testing Strategy — Tool evaluation and tier rationale
- ADR-0011 — Why Starlette (not FastAPI) for the HTTP adapter