Skip to main content

Auth Service — Domain Model

Status: populated Owner: Platform Engineering + Security Last updated: 2026-04-19

Change log

  • v1.2 (2026-04-19) — Rebaselined for multi-IdP. Introduced TenantIdentityProvider and ExternalIdentity aggregates; User.firebaseUid removed in favour of a 1..N collection of ExternalIdentity links. MFA aggregate applies only to native / Keycloak-managed users; tenant-federated users inherit MFA from their IdP.

1. Aggregates

User

FieldTypeNotes
userIdUUIDv4Identity
accountIdUUIDv4Parent account
emailEmail VOUnique per account
externalIdentitiesExternalIdentity[]0..N federation links (see below)
passwordHashstring | nullargon2id; null unless the user uses the native fallback provider
primaryProviderIdstringe.g. keycloak, tenant-oidc:acme, firebase-legacy, native
mfaMfaConfigTOTP secret + backup codes (native / Keycloak-managed only)
statusenumactive, locked, suspended, deleted
rolesRoleAssignment[]Account-scoped RBAC
lastLoginAtInstant | null

TenantIdentityProvider

FieldTypeNotes
providerIdstringe.g. keycloak, tenant-oidc:acme, tenant-saml:acme, firebase-legacy
tenantIdUUIDv4Tenant binding
kindenumoidc | saml | firebase
statusenumactive, disabled, failing
discoveryUrl / metadataRefstring | nullOIDC discovery URL or SAML metadata ref
keycloakIdpAliasstring | nullAlias in the Keycloak realm when brokered
attributeMappersJSONemail / groups / roles mappers
isDefaultbooleanExactly one default per tenant (keycloak unless overridden)
createdAt, lastValidatedAtInstant

ExternalIdentity

FieldTypeNotes
userIdUUIDv4Platform user
providerIdstringFK → TenantIdentityProvider
externalSubjectstringIdP sub / SAML NameID
linkedAtInstant
lastAuthenticatedAtInstant | null
claimsJSON (trimmed)Last-seen normalised claims for audit

Account

FieldTypeNotes
accountIdUUIDv4Identity
tenantIdUUIDv4Platform tenant
namestringDisplay name
planIdstringBilling plan reference
statusenumactive, suspended, deleted
quotaTierenumUsed by Kong rate-limit routing

ApiKey

FieldTypeNotes
keyIdULID key_*Identity (shown to user)
accountIdUUIDv4Owner
keyHashstringsha256(raw); raw shown once at creation
scopesstring[]sms:send, sms:read, billing:read, ...
statusenumactive, revoked, expired
lastUsedAtInstant | null
expiresAtInstant | nullOptional

Session

Short-lived; access token stateless (JWT), refresh token stored for revocation.

JwkKey

FieldNotes
kidKey id
algRS256
statusactive, next, retiring
activatedAt, retiresAtrotation schedule

2. Value Objects

VOInvariant
EmailRFC-5322-subset regex; normalized (lowercase)
PasswordHashargon2id m=64MB, t=3, p=4
ApiKeyScopeValidated against scope registry

3. Domain Events

EventTrigger
auth.user.registered.v1New user created
auth.user.logged_in.v1Successful sign-in (payload includes providerId)
auth.user.locked.v1Repeated failures
auth.user.erased.v1GDPR erasure
auth.api_key.issued.v1New key created (includes keyId, NOT raw key)
auth.api_key.revoked.v1Key revoked
auth.jwks.rotated.v1New key promoted to active
auth.role.assigned.v1Role granted
auth.idp.configured.v1Tenant registers a TenantIdentityProvider
auth.idp.disabled.v1Tenant disables an IdP (emergency / manual)
auth.idp.removed.v1Tenant deregisters
auth.external_identity.linked.v1ExternalIdentity created
auth.external_identity.unlinked.v1ExternalIdentity removed
auth.sso.session.started.v1Successful federated login
auth.sso.session.failed.v1Federation failure (signature, issuer, reserved domain, etc.)

4. Invariants

  • email unique per accountId.
  • Raw API key never persisted; only keyHash.
  • JwkKey with status active is always ≥ 1 and ≤ 1 at any time.
  • User.roles scoped to the user's accountId; cross-account assignment rejected.
  • Every tenantId has exactly one TenantIdentityProvider with isDefault = true (Keycloak unless explicitly overridden).
  • (providerId, externalSubject) is globally unique; an external subject cannot be linked to more than one platform userId.
  • A user with primaryProviderId != 'native' cannot have a passwordHash set.
  • Reserved email domains (e.g. *@ghasi.io) cannot be claimed by any non-keycloak provider; the broker rejects such assertions at mapper time.