Skip to main content

Identity Service — Domain Model

Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · 02 DDD · 04 Events

1. Aggregates

AggregateRoot entityPrimary IDInvariants
UserUserUserId (usr_)Email unique per tenant; status transitions constrained; keycloakUserId present when backend != in_house
SessionSessionSessionId (ses_)Refresh rotation replaces refreshHash; absolute expiry ≤ 8h; idle expiry ≤ tenant config; revoked_at or expires_at set on terminal state
CredentialCredentialCredentialId (crd_)Exactly one active argon2id password hash per user OR ≥1 WebAuthn credential when MFA required; breach-list check passes for new hashes
MFAFactorMFAFactorMFAFactorId (mfa_)One TOTP seed, ≤ 5 WebAuthn credentials, ≤ 10 recovery codes; each MFAFactor has verifiedAt when enrolled
DeviceDeviceDeviceId (dev_)Public-key unique per user; trust level in {untrusted, trusted, offline_bound}; offline binding certificate expiry ≤ 30 days
APIKeyAPIKeyAPIKeyId (apk_)Tenant-scoped; secret is HMAC-stored, returned once; scopes drawn from platform vocabulary; revoke is terminal
ServiceAccountServiceAccountServiceAccountId (svc_)clientId globally unique; tenantId nullable for platform-level; status terminal on revoke
ExternalIdentityExternalIdentityExternalIdentityId (eid_)(issuer, subject) unique; links to exactly one User within one tenant
ModuleModuleModuleId (mod_)code globally unique, immutable (BR-LIC-001); is_always_on cannot be deleted
LicenseAssignmentLicenseAssignmentLicenseAssignmentId (lic_)(module_id, node_id) unique; terminated terminal; trials must have effective_to; dependencies licensed at same/ancestor node
LicenseAssignmentHistoryn/a (child)lih_Append-only; one row per change with before/after JSONB

2. State machines

2.1 User

2.2 Session

2.3 LicenseAssignment

2.4 Device

3. Entities (non-root)

EntityParent aggregatePurpose
RefreshTokenSessionRotating refresh token with one-time-use semantics
RecoveryCodeMFAFactorSingle-use break-out code, hashed
DeviceFingerprintDeviceUA string, screen, timezone hash used for risk scoring
BindingCertificateDeviceShort-lived X.509-style cert for offline content encryption
AllowedRedirectUriExternalIdentityWhitelisted callback URIs per tenant/IdP
LicenseConstraintLicenseAssignmentStructured { seats, expiresAt, usageCap } read from JSONB

4. Value objects

Value objectShapeNotes
UserIdbranded ULID usr_<ulid>Frozen F03
TenantIdbranded ULID ten_<ulid>Inherited from tenant-service
EmailAddressRFC 5322 lower-cased, length ≤ 320Normalized at boundary
PasswordHashargon2id$v=19$m=65536,t=3,p=1$...Opaque outside domain
DeviceFingerprintSHA-256 hex over canonical UA/screen/timezoneOnly stored hashed
JWTClaims{ sub, tid, tids, jti, amr, context_node_id?, exp, iat, iss, aud }Frozen F01
ModuleCodedotted string ehr.core, diag.laboratoryImmutable (F05)
LicenseScopeenum exact | inherit-down
LicenseStatusenum trial | active | suspended | expired | terminated
ProviderKindenum in_house | keycloak_broker | oidc | samlSelects adapter
PermissionScope (API key)tenant:{resource}:{action}Tenant-bound

5. Domain events

All events use platform envelope; subject format identity.{aggregate}.{event}.v1.

5.1 User & session

EventSubjectFired by
UserRegisteredidentity.user.registered.v1RegisterUser use case
UserEmailVerifiedidentity.user.email_verified.v1VerifyEmail
UserSuspendedidentity.user.suspended.v1SuspendUser
UserReactivatedidentity.user.reactivated.v1ReactivateUser
UserDeactivatedidentity.user.deactivated.v1DeactivateUser / GDPR
UserLoggedInidentity.user.logged_in.v1Login* handlers (local/OIDC/SAML)
UserPasswordChangedidentity.user.password_changed.v1ChangePassword
UserMFAEnrolledidentity.user.mfa_enrolled.v1EnrollMFA
UserMFAChallengeFailedidentity.user.mfa_challenge_failed.v1VerifyMFA
SessionCreatedidentity.session.created.v1Login*
SessionRevokedidentity.session.revoked.v1Logout / admin / suspension

5.2 Device, key, federation

EventSubjectPurpose
DeviceRegisteredidentity.device.registered.v1New device enrolled
DeviceBoundForOfflineidentity.device.bound_for_offline.v1Offline cert issued (consumed by content/chart)
DeviceRevokedidentity.device.revoked.v1Cert revocation
APIKeyIssuedidentity.api_key.issued.v1New tenant API key created
APIKeyRevokedidentity.api_key.revoked.v1
ServiceAccountCreatedidentity.service_account.created.v1
ServiceAccountRevokedidentity.service_account.revoked.v1Consumers drop tokens
ExternalIdentityLinkedidentity.external_identity.linked.v1OIDC/SAML JIT

5.3 Licensing (merged module)

EventSubjectPurpose
ModuleCreatedidentity.license.module.created.v1Super Admin adds module
ModuleUpdatedidentity.license.module.updated.v1Description / dependency change
LicenseAssignedidentity.license.assignment.created.v1New license attached to node
LicenseStatusChangedidentity.license.assignment.status_changed.v1activesuspended etc.
LicenseConstraintsUpdatedidentity.license.assignment.constraints_updated.v1Seats/expiry change
LicenseAssignmentExpiredidentity.license.assignment.expired.v1Scheduled job fires

6. Ubiquitous language

TermMeaning
PrincipalAny authenticated actor (User or ServiceAccount)
Provider (backend)An identity adapter: in_house, keycloak_broker, oidc, saml
Provider (clinical)A licensed clinical user (physician, nurse, ...). Profile data lives in tenant-service
SessionAccess+refresh pair bound to a User and Device
BindingA Device's offline capability, proved by a signed BindingCertificate
ModuleA licensable capability (ehr.core, clinical.immunizations, diag.radiology)
License assignmentA (module, node) grant with scope, status, constraints
Effective license setRuntime-computed set of modules active for a (provider, node) after ancestor inheritance
Access contextAggregated view combining roles (tenant), memberships (tenant), and effective licenses (identity)
JWKSPublic keys published at /.well-known/jwks.json for JWT verification
amrOIDC amr claim = methods used to authenticate (pwd, mfa, webauthn, fed)
Break-glassEmergency override login with tenant-signed audit trail (S4)

7. Aggregate relationships

8. Invariants enforced in domain layer

#Invariant
INV-01email unique per tenantId
INV-02A suspended or deactivated user cannot open new sessions
INV-03Refresh rotation is one-time-use; replay produces SessionRevoked and user security-incident event
INV-04MFA required when tenant config mfa_required=true OR role set requires it OR adaptive score > threshold
INV-05Offline binding certificates expire within 30 days; issuance requires device.status == trusted
INV-06Module dependencies must be licensed at same or ancestor node before assignment
INV-07is_always_on modules cannot transition to suspended or terminated
INV-08terminated license is terminal; new assignment must be created to re-license
INV-09Trial licenses MUST have effective_to
INV-10Cross-tenant license assignment prohibited; node's tenant must match assignment tenant
INV-11JWT amr reflects all factors used; mfa claim present iff a second factor was verified in session
INV-12ExternalIdentity.(issuer, subject) unique and bound to exactly one User within a tenant

9. Open questions

  • Should WebAuthn platform authenticators replace TOTP as default second factor for clinical roles by M2? Align with HIPAA guidance.
  • Do we need per-module usage caps enforced at runtime (vs billing-only) for S1?