Skip to content

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()):

  1. .env is loaded first as the base config (override=False — does not clobber process env)
  2. .env.{state} is loaded on top as state-specific overrides (override=True)
  3. ProxySettings() reads from os.environ, so every nested model (CognitoSettings, SessionConfig, PKCEConfig) picks up its own prefixed vars

Priority (highest to lowest):

  1. Process environment variables (already set before startup)
  2. .env.{state} file values
  3. .env base file values
  4. 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

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/refresh renews the session when the access token expires
  • /auth/logout clears 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