Identity Service — Application Logic
Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · 05 API · 04 Events
1. Use case map
1.1 Commands
| Use case | Command DTO | Emits | Notes |
|---|---|---|---|
RegisterUser | RegisterUserCommand | identity.user.registered.v1 | Creates shadow user in Keycloak when backend != in_house; triggers email verification |
VerifyEmail | VerifyEmailCommand | identity.user.email_verified.v1 | Activates user, completes registration |
LoginWithPassword | LoginPasswordCommand | identity.session.created.v1, identity.user.logged_in.v1 | argon2id verify; MFA challenge if required |
LoginWithOIDC | LoginOIDCCommand | identity.external_identity.linked.v1 (JIT), identity.session.created.v1 | PKCE; state/nonce validation; JIT link or create |
LoginWithSAML | LoginSAMLCommand | identity.external_identity.linked.v1 (JIT), identity.session.created.v1 | Encrypted assertion; entity ID allowlist |
LoginWithKeycloakBroker | LoginKeycloakCommand | same as OIDC | Token exchange (RFC 8693); re-mint Ghasi JWT |
RefreshSession | RefreshSessionCommand | identity.session.created.v1 (new), identity.session.revoked.v1 (old) | Refresh rotation one-time-use |
Logout / LogoutAllDevices | LogoutCommand | identity.session.revoked.v1 | Revoke one or all sessions |
EnrollTOTP | EnrollTOTPCommand | identity.user.mfa_enrolled.v1 | QR code issued; verify before enabling |
EnrollWebAuthn | EnrollWebAuthnCommand | identity.user.mfa_enrolled.v1 | Passkey / platform authenticator |
VerifyMFA | VerifyMFACommand | identity.user.mfa_challenge_failed.v1 (on fail) | Consumed during login flow |
ChangePassword | ChangePasswordCommand | identity.user.password_changed.v1 | Breach-list check; invalidates other sessions |
RequestPasswordReset | RequestPasswordResetCommand | domain notice (consumed by communication-service) | Signed token email |
SuspendUser / ReactivateUser / DeactivateUser | — | identity.user.{suspended,reactivated,deactivated}.v1 | Admin scope |
RegisterDevice | RegisterDeviceCommand | identity.device.registered.v1 | Returns device ID + challenge |
IssueOfflineBindingCert | BindDeviceCommand | identity.device.bound_for_offline.v1 | Consumed by content/chart services |
RevokeDevice | RevokeDeviceCommand | identity.device.revoked.v1 | Cascades to revoke sessions bound to device |
CreateAPIKey | CreateAPIKeyCommand | identity.api_key.issued.v1 | Returns secret once |
RotateAPIKey | RotateAPIKeyCommand | identity.api_key.issued.v1, identity.api_key.revoked.v1 | Overlap window configurable |
RevokeAPIKey | RevokeAPIKeyCommand | identity.api_key.revoked.v1 | — |
CreateServiceAccount | CreateServiceAccountCommand | identity.service_account.created.v1 | Keycloak confidential client |
RevokeServiceAccount | RevokeServiceAccountCommand | identity.service_account.revoked.v1 | — |
CreateModule / UpdateModule (licensing) | — | identity.license.module.{created,updated}.v1 | Super Admin only |
AssignLicense | AssignLicenseCommand | identity.license.assignment.created.v1 | Validates dependencies + min_node_types |
ChangeLicenseStatus | ChangeLicenseStatusCommand | identity.license.assignment.status_changed.v1 | Enforces always-on immutability |
UpdateLicenseConstraints | UpdateLicenseConstraintsCommand | identity.license.assignment.constraints_updated.v1 | — |
ApplyModuleBundle | ApplyBundleCommand | N × identity.license.assignment.created.v1 | Transactional |
1.2 Queries
| Query | Returns |
|---|---|
GetMe | Own User profile |
GetMeAccessContext | Aggregated { roles[], memberships[], effectiveModules[] } for the caller |
GetUserById | Single User scoped by tenant |
ListUsersByTenant | Paginated user list with filters |
GetDevice / ListMyDevices | User-owned devices with trust + binding status |
GetAPIKey / ListAPIKeys | Tenant API keys (metadata only, never secret) |
ListServiceAccounts | Super Admin / Tenant Admin |
JWKS | Public keys for JWT verification |
GetModuleCatalogue / GetModule | Module definitions + dependency graph |
GetNodeAssignments | Direct licenses at a node (no inheritance) |
GetEffectiveLicenses | Computed effective module set for (tenantId, providerId, nodeId) |
GetLicenseHistory | Append-only change log for an assignment |
2. Orchestration flows
2.1 Login with MFA + context
2.2 Effective license resolution
2.3 License assignment change → cache invalidation
3. Application ports
3.1 Authentication / sessions
| Port | Purpose | Default adapter |
|---|---|---|
PasswordHasher | argon2id hash & verify | Argon2idAdapter |
TokenSigner | Sign & verify Ghasi JWTs | KMSTokenSignerAdapter |
RefreshStore | Persist refresh hashes | PostgresRefreshStoreAdapter |
SessionRepository | Read/write sessions | Postgres |
UserRepository | User aggregate persistence | Postgres |
MFAChallengeStore | Challenge tokens (short-lived) | Redis |
3.2 Identity provider abstraction
| Port | Purpose | Adapters |
|---|---|---|
IdentityAuthenticationProvider | High-level authenticate(credentials) | InHouseAuth, KeycloakBrokerAuth, OIDCClientAuth, SAMLSpAuth |
CredentialStore | Credential CRUD | PostgresCredentialStore (in_house), KeycloakCredentialStore |
ExternalIdentityLinker | Link OIDC/SAML subject ↔ User | PostgresExternalIdentityAdapter |
DeviceRepository | Device + BindingCertificate | Postgres |
CertificateIssuer | Sign short-lived binding certs | KMSCertificateIssuer |
3.3 Licensing
| Port | Purpose | Adapter |
|---|---|---|
ModuleRepository | Module catalogue | Postgres |
LicenseAssignmentRepository | Assignments + history | Postgres |
HierarchyAncestryClient | Fetch ancestor chain | HTTP → tenant-service /tenant/internal/hierarchy/nodes/:id/ancestors |
EffectiveLicenseCache | Redis cache | Redis |
LicenseBreachNotifier | Emit seat/usage breach events | NATS |
3.4 Cross-cutting
| Port | Purpose |
|---|---|
EventPublisher | Outbox-relayed domain events |
AuditWriter | Low-retention audit tap (primary record via audit-service event consumer) |
AccessContextAssembler | Combine tenant-service roles/memberships + local effective licenses |
BreachPasswordDenyList | Check pwned-passwords hash prefix |
4. Saga / outbox patterns
| Pattern | Where | Notes |
|---|---|---|
| Outbox relay | All commands that emit events | outbox table + relay worker; exactly-once per subject+key with NATS JetStream |
| Inbox dedupe | Consumers of tenant.activated, tenant.suspended, tenant.terminated | inbox table keyed on event id |
| Saga — registration | RegisterUser → async email → VerifyEmail | No compensating tx; expiry at 24h re-issues token |
| Saga — refresh rotation | RefreshSession uses SERIALIZABLE tx to invalidate old hash + mint new | Replay detection emits SessionRevoked for entire chain |
| Saga — device revocation on suspend | tenant.suspended → revoke all devices → publish device.revoked events | Idempotent per device id |
5. Error handling
| Layer | Strategy |
|---|---|
| Controllers | Map domain errors → HTTP via ErrorToHttp interceptor. Standard envelope {error, message, requestId, timestamp} |
| Use cases | Throw typed DomainError subclasses (CrossTenantError, UserSuspendedError, DependencyNotLicensedError, ...). No raw throws. |
| Adapters | Translate infrastructure exceptions to transient vs permanent. Retries at adapter level only for transient network classes. |
| Outbox | Failed publish kept in table; relay retries with exponential backoff + jitter; alert after 15 min continuous failure |
| External IdPs | Circuit breaker per (issuer, tenantId); half-open after 60s; breaker-open returns IDENT_FEDERATION_UNAVAILABLE |
6. Input validation
| Surface | Validator |
|---|---|
| Public REST | Zod schemas in presentation/dto — fail fast with 400 + field-level errors |
| Internal REST | Same Zod schemas; plus ip-allowlist guard |
| Events (inbox) | Ajv against registered schema in @ghasi/event-envelope |
| JWT | jose with KMS-backed JWKS; enforced audience, issuer, algorithm |
7. Ports → adapters wiring
// infrastructure/app.module.ts
providers: [
{ provide: PasswordHasher, useClass: Argon2idAdapter },
{ provide: TokenSigner, useClass: KMSTokenSignerAdapter },
{ provide: IdentityAuthenticationProvider, useFactory: authProviderFactory, inject: [Config] },
{ provide: HierarchyAncestryClient, useClass: HttpHierarchyClient },
{ provide: EffectiveLicenseCache, useClass: RedisEffectiveLicenseCache },
{ provide: EventPublisher, useClass: OutboxEventPublisher },
]
8. Open questions
- Should
ApplyBundlesupport partial failure (continue on dependency error) with compensating notes, or strictly all-or-nothing? Current design: strictly transactional. - Adaptive MFA scoring engine (in S4): in-house or behind
RiskScorerport for vendor swap?