0007. Stable account key: immutable local IDs; external identity keyed by issuer and subject#
Status: Accepted
Context#
Email addresses and phone numbers change and are recycled. Using them as identity keys enables account takeover by recycled or matching addresses (DOM-001, USR-021, TEST-026).
Decision#
- Every principal has an opaque immutable ID: a type prefix and a lowercase ULID
(
usr_01...,svc_01...,org_01...), generated withcrypto/randentropy. Access tokens use the user ID assub(or a pairwise value, below). - Login identifiers are rows in
user_identifierswith an original value (for display) and a canonical value (for comparison, see docs/protocols/identifiers.md). Uniqueness of the canonical value per realm and type is enforced only for verified identifiers and usernames, so an unverified claim never blocks the real owner. - External identities (
external_identities) are unique per provider, upstream issuer and upstream subject (DB-003). Upstream OpenID Connect and GitHub-style OAuth federation is implemented (internal/federation,TestFederation); SAML and LDAP were not at acceptance (0018). Update 2026-10-11: SAML connections key external identities the same way (issuer = the IdP's entity ID, subject = the NameID or a configured subject attribute), and LDAP/Active Directory accounts are linked by the entry's immutable ID (entryUUIDorobjectGUID) as a connector-scoped external ID inconnector_external_ids, never by DN or username (0018). - Clients may use pairwise subjects (
subject_type: pairwisewith asector_identifier):sub = "pws_" + base64url(HMAC(purpose key, sector || user ID))[:20 bytes], derived on first use and then stored inpairwise_subjects(migration0018). - Deleting an account keeps the
usersrow in statedeletedwith personal fields cleared, and writes digest-only tombstones for each identifier so imports and restores do not resurrect it.
Alternatives considered#
- Email as primary key: rejected (DOM-001).
- Random UUIDv4: equally opaque; ULID was chosen for index locality and sortable listings. Consequence: IDs reveal their creation time.
Consequences#
- Changing an email never changes the subject seen by applications.
- A new account that later verifies a previously used address receives a new ID and none of the old grants (USR-021).
- Pairwise subjects are derived with the active root key version the first
time they are issued (0015) and then kept in
pairwise_subjects, so later changes ofkeys.active_versiondo not change them (Update 2026-10;TestPairwiseSubjects). Values issued before migration0018are stored only when next issued, with the version active at that time. Stored values are not removed when an account is deleted (theusersrow is kept).
Migration implications#
Imports store source-qualified mappings (import_mappings) and never merge by
email (MIG-004). The durable pairwise mapping table (pairwise_subjects,
migration 0018) records values as they are issued; it was not backfilled.
Related requirement IDs#
DOM-001, DOM-002, DOM-006, DB-002, DB-003, REG-002, REG-003, USR-021, APP-012, OIDC-010, MIG-004, MIG-017