ADR-008: SCIM Per-Org Tokens (Not Global)
ADR-008: SCIM Per-Org Tokens (Not Global)
Section titled “ADR-008: SCIM Per-Org Tokens (Not Global)”Status: Accepted Date: 2026-03 Deciders: Platform team (platform-0043, T561)
Context
Section titled “Context”SCIM 2.0 (System for Cross-domain Identity Management) is the provisioning protocol used by identity providers (Okta, Azure AD, etc.) to synchronise users and groups into Arbitex. The IdP must authenticate to the SCIM API using a bearer token.
The original implementation used a single global bearer token (SCIM_BEARER_TOKEN) shared across all tenant organisations. This design has several security problems:
- Blast radius on compromise: If the global token is leaked, an attacker gains provisioning access to all organisations on the platform — not just one. Revoking the token requires coordinating with every customer’s IdP simultaneously.
- No per-tenant rotation: A customer who suspects their SCIM token is compromised cannot rotate just their token; rotating the global token affects every other customer.
- No audit isolation: With a shared token, the audit log cannot distinguish which customer’s IdP performed a provisioning operation — the token identity is the same for all.
- Compliance: HIPAA Business Associate Agreements and SOC 2 CC6 controls require that access credentials be rotatable per-tenant without affecting other tenants.
Decision
Section titled “Decision”Replace the global SCIM_BEARER_TOKEN with per-organisation SCIM tokens stored in the org_scim_tokens database table.
Key design properties:
- Each organisation has exactly one active SCIM token at a time (identified by
rotated_at IS NULL). - Tokens are stored as bcrypt hashes — the raw token is never persisted after generation.
- The raw token is returned to the admin once at rotation time. It cannot be recovered from the database afterwards.
- When a new token is rotated in, the previous token’s
rotated_atis set to the rotation timestamp, preserving the history for audit purposes. - Rotating one organisation’s token has zero effect on any other organisation.
- A CRUD API allows admins to rotate, list (history), and delete tokens per org.
Consequences
Section titled “Consequences”Positive:
- Compromising one organisation’s SCIM token gives no access to any other organisation’s provisioning.
- Organisations can rotate their SCIM token independently, any time, without coordinating with other customers or with Arbitex support.
- Bcrypt hashing means a database breach does not expose usable SCIM tokens.
- Audit trail:
created_by_user_idrecords which admin rotated the token;created_atandrotated_atprovide full token history per org. - Satisfies tenant isolation requirements in SOC 2, ISO 27001, and HIPAA BAA contexts.
Negative / trade-offs:
- Migration effort: existing customers using the global
SCIM_BEARER_TOKENmust generate a per-org token via the Admin Portal and update their IdP configuration. The global token continues to work during migration but should be removed after all orgs are migrated. - One-time token display: admins must securely store or immediately configure the raw token in their IdP. There is no recovery path — only rotation to a new token.
- The
org_scim_tokenstable adds a new DB migration and join on every SCIM API authentication. The bcrypt check adds ~100ms per SCIM request — acceptable for provisioning operations (not a hot path). - Organisations without an active token (no rotations performed) have SCIM provisioning disabled. The first token must be explicitly rotated in.
Configuration:
SCIM_BEARER_TOKEN— legacy global token (still accepted during migration, deprecated).- Admin API:
POST /v1/scim/orgs/{org_id}/token/rotateto generate a new per-org token.