Key Rotation Integration
This guide covers the developer-facing details of the unified credential model: how CredentialService works under the hood, how to add new credential types, and how credentials propagate to outpost deployments.
For admin operations (rotating, revoking, listing), see Credential Management. For the API reference, see Credentials API.
Unified credential model
Section titled “Unified credential model”All credential types — OAuth client secrets, API keys, SCIM tokens, and client identity keys — are stored in a single credentials table. This replaces the previous pattern of separate tables per credential type.
Data model
Section titled “Data model”| Column | Type | Description |
|---|---|---|
id |
UUID | Credential identifier |
org_id |
UUID | Owning organization |
type |
enum | oauth_client, api_key, scim_token, client_identity |
owner_id |
UUID | Polymorphic FK — user, client, or service that owns this credential |
owner_type |
enum | user, oauth_client, scim_config, client_identity |
name |
string(255) | Human-readable label |
key_prefix |
string(8) | First 8 chars of the secret (for identification) |
current_hash |
string(64) | SHA-256 hex digest of the active secret |
previous_hash |
string(64) | Nullable — hash of prior secret during grace period |
previous_expires_at |
timestamp | Nullable — when the grace period ends |
rotated_at |
timestamp | Nullable — when last rotation occurred |
grace_period_seconds |
int | Default 3600 (1 hour), configurable per credential |
status |
enum | active, revoked, expired |
created_at |
timestamp | Creation time |
revoked_at |
timestamp | Nullable — when revoked |
created_by |
UUID | Who created or last rotated |
Indexes
Section titled “Indexes”The table uses targeted indexes for the two primary access patterns:
| Index | Columns | Purpose |
|---|---|---|
ix_credentials_current_hash |
current_hash |
Fast authentication lookup by current secret |
ix_credentials_previous_hash |
previous_hash |
Fast authentication lookup during grace period |
ix_credentials_org_id |
org_id |
List credentials by organization |
ix_credentials_org_type |
org_id, type |
List credentials by org + type filter |
ix_credentials_owner |
owner_id, owner_type |
Look up credentials by owner entity |
How authentication works
Section titled “How authentication works”CredentialService.authenticate() is the single entry point for all credential authentication. Every middleware that validates secrets calls this method — there are no type-specific authentication paths.
Lookup logic
Section titled “Lookup logic”The method hashes the incoming secret with SHA-256 and runs a single query:
SELECT * FROM credentialsWHERE (current_hash = :hash OR (previous_hash = :hash AND previous_expires_at > now())) AND status = 'active'This query checks:
- Current secret — direct hash match
- Previous secret — hash match AND within the grace window
If both conditions fail, authentication returns None (invalid credential).
Optional filters
Section titled “Optional filters”The authenticate() method accepts optional filters to narrow the lookup:
| Parameter | Type | Effect |
|---|---|---|
type_filter |
CredentialType |
Restrict to a specific credential type |
owner_id |
UUID | Restrict to a specific owner entity |
org_id |
UUID | Restrict to a specific organization |
Use these filters when the calling context knows which credential type to expect. For example, the SCIM middleware passes type_filter=CredentialType.SCIM_TOKEN to avoid matching an API key that happens to share the same hash (cryptographically unlikely but defensively correct).
Integration example
Section titled “Integration example”from backend.app.services.credential_service import CredentialServicefrom backend.app.models.credential import CredentialType
async def authenticate_request(db, bearer_token: str): credential = await CredentialService.authenticate( db, bearer_token, type_filter=CredentialType.API_KEY, ) if credential is None: raise HTTPException(status_code=401, detail="Invalid credential") return credentialAdding new credential types
Section titled “Adding new credential types”Adding a new credential type requires two changes:
1. Extend the enums
Section titled “1. Extend the enums”In backend/app/models/credential.py, add values to both enums:
class CredentialType(str, enum.Enum): OAUTH_CLIENT = "oauth_client" API_KEY = "api_key" SCIM_TOKEN = "scim_token" CLIENT_IDENTITY = "client_identity" # Add your new type: WEBHOOK_SECRET = "webhook_secret"
class CredentialOwnerType(str, enum.Enum): USER = "user" OAUTH_CLIENT = "oauth_client" SCIM_CONFIG = "scim_config" CLIENT_IDENTITY = "client_identity" # Add the owner type if new: WEBHOOK_CONFIG = "webhook_config"2. Create credentials through CredentialService
Section titled “2. Create credentials through CredentialService”No model changes needed. Use CredentialService.create() with the new type:
credential, plaintext = await CredentialService.create( db, org_id=org.id, type=CredentialType.WEBHOOK_SECRET, owner_id=webhook_config.id, owner_type=CredentialOwnerType.WEBHOOK_CONFIG, name="production-webhook", grace_period_seconds=1800, # 30 minutes created_by=admin.id,)# Return `plaintext` to the caller — it is never retrievable after thisThe new credential type immediately gets:
- Zero-downtime rotation via
POST /api/v1/admin/credentials/{id}/rotate - Grace-period support with the configured duration
- Revocation via
DELETE /api/v1/admin/credentials/{id} - Listing and filtering via
GET /api/v1/admin/credentials?type=webhook_secret
No new endpoints, middleware, or database migrations required (beyond the Alembic migration for the enum value).
Grace period configuration
Section titled “Grace period configuration”The grace period is set per credential at creation time via grace_period_seconds. It cannot be changed after creation — to adjust the grace period, create a new credential with the desired value.
| Value | Behavior |
|---|---|
3600 (default) |
Previous secret valid for 1 hour after rotation |
0 |
No grace period — previous secret invalidated immediately on rotation |
86400 |
24-hour grace period for credentials with many distributed consumers |
Choosing a grace period
Section titled “Choosing a grace period”- API keys for CI/CD: 3600 (1 hour) — sufficient for pipeline secret rotation
- OAuth client secrets: 3600 (1 hour) — standard for M2M credential updates
- SCIM tokens: 1800 (30 minutes) — IdP sync is typically fast to reconfigure
- High-fanout credentials: 86400 (24 hours) — when many distributed services consume the same credential and need time to pick up the new value
Secret generation
Section titled “Secret generation”All secrets are generated by _generate_secret() in credential_service.py:
- Generate 24 cryptographically random bytes → 48 hex characters
- Prepend the
arx_prefix → final secret is 52 characters - Compute SHA-256 of the plaintext → stored as
current_hash - Extract first 8 characters → stored as
key_prefix
The plaintext is returned to the caller and never stored. Only the hash is persisted.
Outpost credential sync
Section titled “Outpost credential sync”Outpost deployments maintain a local credential cache that syncs from the platform. When a credential is rotated on the platform:
- The outpost’s next sync cycle picks up the updated
current_hashandprevious_hash - During the sync interval, the outpost continues to accept the old credential via its local cache
- After sync completes, the outpost recognizes both current and previous secrets (grace period applies)
The platform-side grace period and the outpost sync interval are independent clocks. The grace period counts from the moment of rotation on the platform — not from when the outpost receives the update. Plan accordingly for environments with long sync intervals.
Audit trail
Section titled “Audit trail”Every credential operation generates an audit event:
| Operation | Audit fields |
|---|---|
| Create | Credential ID, type, owner, org, created_by |
| Rotate | Credential ID, old key_prefix, new key_prefix, grace duration, triggered_by |
| Revoke | Credential ID, triggered_by, revoked_at |
Audit events are written to the platform audit log and are queryable via the audit export API.
See also
Section titled “See also”- Credential Management — admin guide for day-to-day operations
- Credentials API — endpoint specifications and schemas
- API Keys — legacy API key management
- Security Architecture — platform security overview