Auth and API key management#

The service authenticates callers with simple bearer-token API keys, scoped to a tenant. This page covers operating that system.

For the design (TenantApiKey, validate_api_key, middleware), see Components and ADR-005.

How auth works at runtime#

  1. Caller sends Authorization: Bearer <key>.

  2. The middleware compares <key> against every entry in AUTH_API_KEYS using secrets.compare_digest (constant-time).

  3. On match, the request is tagged with the matching tenant_id (and is_superuser flag). On no match → 401.

GET /v1/health skips auth. Everything else requires it.

API key shape#

AUTH_API_KEYS is a JSON array of objects. Five fields:

Field

Type

Notes

key_id

string

Unique identifier for this key. Used for logging and rotation; not for auth.

name

string

Human-readable label (e.g. "crm-production").

key

string

The secret. Stored as SecretStr after load; never logged.

tenant_id

string

The tenant this key represents.

is_superuser

bool, optional

Default false. Grants access to /v1/usage/all.

Example:

[
    {
        "key_id": "k-prd-0",
        "name": "crm-production",
        "key": "sk-prod-abc123def456",
        "tenant_id": "tenant-redcross-nl",
        "is_superuser": false
    }
]

Production: rotating and adding keys#

In production the JSON is stored as the auth-api-keys secret in Key Vault and loaded via Key Vault references. Use the helper script — never edit the secret by hand:

az login
export AZURE_KEYVAULT="qfa-${ENV}-keyvault"   # e.g. qfa-prd-keyvault

# Add a key for a tenant (keeps existing keys)
uv run python3 scripts/update_auth_api_keys.py --add <tenant>

# Replace all keys for a tenant with one new key
uv run python3 scripts/update_auth_api_keys.py --replace <tenant>

# Remove all keys for a tenant
uv run python3 scripts/update_auth_api_keys.py --remove <tenant>

The script prints the generated key to stdout. Copy it and share it with the tenant immediately — Key Vault stores it, but you cannot retrieve it again from the CLI in clear text.

Key rotation steps:

  1. --add <tenant> — adds a new key for the tenant alongside the old one.

  2. Give the new key to the tenant.

  3. Wait until you’ve verified the tenant is using the new key (look for the new key_id in logs).

  4. --replace <tenant> with their now-confirmed new key, or --remove <tenant> plus --add if you want a clean cut.

App Service picks up Key Vault changes within a few minutes. To force an immediate refresh, restart the App Service from the Azure portal.

Local dev#

For local development, set AUTH_API_KEYS directly in .env or your shell:

export AUTH_API_KEYS='[{"key_id":"local-0","name":"local","key":"dev-key","tenant_id":"local","is_superuser":true}]'

is_superuser=true is convenient locally because /v1/usage/all then works.

Superuser scope#

is_superuser=true only grants access to the cross-tenant GET /v1/usage/all route. It does not elevate access to other tenants’ analyze/summarize/assign-codes endpoints — those are scoped purely by tenant_id derived from the matching key.

What’s not supported (yet)#

  • Per-key revocation by key_id alone (rotation goes through the script’s --replace / --remove per-tenant flow).

  • Per-key rate limits.

  • Non-bearer auth schemes (OAuth, mTLS).

These are deliberate omissions for v1, not bugs.