Skip to content

API keys

cdk_ keys, admin-only management routes, scope validation, and runtime authentication.

Intended audience: Stakeholders, Business analysts, Solution architects, Developers, Testers

Learning outcomes by role

Stakeholders

  • Explain API keys as long-lived automation credentials with scope risk.

Business analysts

  • Write stories for issuance, rotation, and revocation of cdk_ keys.

Solution architects

  • Plan storage, hashing, and encryption for keys at rest and in transit.

Developers

  • Send X-API-KEY and interpret ApiKeyTokenSession behavior.

Testers

  • Validate scope enforcement, org binding, and invalid key paths.

API keys are long-lived secrets for machine-to-machine automation — scripts, CI pipelines, and service integrations that cannot go through the browser OAuth flow. The raw key value is shown exactly once at creation; after that only a hash is stored. At runtime, callers send the key in a single header and the platform resolves it to an ApiKeyTokenSession with the same permission model as interactive users.

  • Risk profile — A leaked key provides persistent access until explicitly revoked. Keys bypass interactive MFA; treat them like passwords, scope them to the minimum permissions needed, and rotate on any suspicion of exposure.
  • Lifecycle — Only platform sys-admins create keys. Revocation is immediate once the key row is deleted.
  • Actors — Platform admins (or delegated automation accounts) create keys; integrations authenticate without an interactive user session.
  • Acceptance — Each key carries a scopes list of cadence:* permission strings. Requesting a scope the creating user does not hold is rejected at creation.
API key authentication flow X-API-KEY header is validated in AuthenticationMiddleware, then TenantContextMiddleware builds a synthetic session. HTTP request with X-API-KEY header AuthenticationMiddleware Hash lookup → request.state.api_key_row TenantContextMiddleware ApiKeyTokenSession + memberships; scopes via sanitize_api_key_flat_scopes

See Security and access for JWT versus API key paths and cadence.core.middleware_setup registration order in cadence.main.

  • sys_admin flag on the caller’s session (enforced by require_platform_sys_admin)
  • cadence:system:api_keys:write permission for create and revoke
  • cadence:system:api_keys:read permission for list

| Field | Present at | Notes | | -------------- | -------------------------- | ------------------------------------------------------------------------- | | id | Always | UUID — use for revoke | | name | Always | Human-readable label set at creation | | key_prefix | Always | First few chars of the key for identification without exposing the secret | | raw_key | Creation response only | Full cdk_ secret — store immediately, never retrievable again | | scopes | Always | List of cadence:* permission strings the key grants | | expires_at | Always | Optional expiry; null means no expiry | | last_used_at | Always | Best-effort timestamp of last authenticated request | | created_at | Always | Creation timestamp | | is_active | Always | false means the key is soft-revoked and will return 401 |

All routes live under /api/admin/users/api-keys and require both require_platform_sys_admin (which checks session.is_sys_admin) and the matching roles_allowed(...) permission. There is no self-service /api/me/api-keys surface — only sys-admins can create or revoke keys for any user.

| Method | Path | Permission | Action | | -------- | ------------------------------------------ | ------------------------------- | -------------------------------------- | | POST | /api/admin/users/api-keys | cadence:system:api_keys:write | Create a key; returns raw_key once | | GET | /api/admin/users/api-keys | cadence:system:api_keys:read | List active keys for the caller’s user | | DELETE | /api/admin/users/api-keys/{key_id} | cadence:system:api_keys:write | Revoke a key immediately |

When creating a key, each requested scope must satisfy two conditions. First, the scope must appear in the global PERMISSIONS set — requesting an unknown permission returns 422. Second, unless the creating user is sys_admin, the user must already hold that permission either globally or in at least one org — you cannot grant a scope you do not yourself possess.

cadence/domain/api_key/service.py
def validate_scopes(self, scopes: List[str], session) -> None:
"""Ensure each scope is known and the caller may grant it."""
for s in scopes:
if s not in PERMISSIONS:
raise ValidationError(f"Unknown scope/permission: {s}")
if session.is_sys_admin:
continue
if session.has_permission(s, None):
continue
org_ok = any(
session.has_permission(s, oid) for oid in session.membership_org_ids
)
if not org_ok:
raise AuthorizationError(f"Cannot grant scope you do not hold: {s}")

At runtime, TenantContextMiddleware further strips any dangerous permissions from the session via sanitize_api_key_flat_scopes — so even if a scope was granted at creation, the three elevated permissions (cadence:system:admin, cadence:system:api_keys:read, cadence:system:api_keys:write) are removed from the effective session. An API key can never act as a platform superuser or manage other API keys.

  1. Send the raw cdk_ secret in the X-API-KEY header on every request. Do not use Authorization: Bearer — that header is for JWTs only (see Security and access).
  2. AuthenticationMiddleware hashes the incoming value and looks up the matching row. A missing or revoked row returns 401.
  3. TenantContextMiddleware builds an ApiKeyTokenSession carrying the user id, org memberships, and the sanitized scope list. Route-level roles_allowed checks run against this session exactly as they would for a JWT session.