# Data Model: External Authentication ## Conventions - IDs are opaque strings generated by Elsa and never reused. - `TenantId = "*"` means host-wide/tenant-agnostic; `TenantId = ""` means only Elsa's default tenant. - Timestamps are UTC `DateTimeOffset`. - Revisions are monotonically increasing 64-bit integers. - Secret values, provider tokens, completion codes, refresh tokens, raw subjects, and unrestricted claims are never stored in public connection documents. - JSON fields are opaque, versioned extension settings. Their provider owns validation and migration. ## IdentityProviderConnection Represents one database-owned provider trust relationship. Configuration-owned connections use the same domain model but are materialized from options and never written through the store. | Field | Type | Rules | | --- | --- | --- | | `Id` | string | Immutable Connection ID | | `TenantId` | string | `*`, empty default tenant, or concrete tenant | | `Key` | string | Stable URL/presentation key; normalized; unique with `TenantId` among database rows | | `AdapterType` | string | Must identify an installed, deployment-allowed adapter | | `AdapterSettingsVersion` | int | Positive schema version | | `AdapterSettings` | JSON object | Opaque to registry; adapter validates/migrates | | `SecretBindings` | map | Names declared by descriptor; contains no values | | `DisplayName` | string | Required, bounded, safe plain text | | `IconId` | string? | Trusted server asset identifier only | | `DisplayOrder` | int | Deterministic tie-break by normalized key | | `IsDefault` | bool | At most one effective automatic default per scope | | `IsEnabled` | bool | Administrative intent; requires validity to be effective | | `ArchivedAt` | DateTimeOffset? | Archive is logical deletion | | `UnlinkedPolicy` | PolicySelection? | Null means deployment default | | `PermissionGrantSources` | list | Ordered and descriptor-validated | | `ClaimProjection` | ClaimProjection | Allowlist, value limits, and redaction policy | | `UpstreamLogoutMode` | enum | `Disabled`, `UserChoice`, or `Always` | | `Revision` | long | Changes on every mutation; concurrency token | | `MaterialRevision` | string | Non-secret fingerprint of all sign-in-affecting state | | `CreatedAt` / `UpdatedAt` | DateTimeOffset | Audit metadata | ### Constraints - Database unique index: `(TenantId, Key)`. - Creation/update additionally rejects an existing configuration-owned `(TenantId, Key)`. - A tenant key may not collide with an inherited host-wide key in its effective registry. - `Id` survives archive/restore. Restore returns disabled and requires revalidation. - Display-only changes advance `Revision` but may retain `MaterialRevision`. - Adapter/trust settings, scope, Secret Binding generation, policy, claim projection, grants, or enable/archive state affect `MaterialRevision`. ### State Transitions ```text Draft/Disabled --validate + enable--> Enabled Enabled --disable------------------> Disabled Draft/Disabled/Enabled --archive---> Archived Archived --restore-----------------> Disabled ``` Invalid or unresolved configuration never becomes effectively enabled. Test observations do not change lifecycle state. ## SecretBinding | Field | Type | Rules | | --- | --- | --- | | `ResolverType` | string | Installed, deployment-allowed resolver | | `Reference` | string | Non-secret lookup key | | `ExpectedType` | string? | Optional resolver/type constraint | | `ExpectedScope` | string? | Optional resolver/scope constraint | Resolution returns a transient value and opaque nonreversible generation fingerprint. The fingerprint contributes to material revision but is not exposed or stored in API models. ## PolicySelection | Field | Type | Rules | | --- | --- | --- | | `Type` | string | Installed, deployment-allowed Unlinked Identity Policy | | `SettingsVersion` | int | Positive schema version | | `Settings` | JSON object | Provider-owned validation/migration | Built-in types are `Reject` and `CreateUser`. ## GrantSourceSelection | Field | Type | Rules | | --- | --- | --- | | `Type` | string | Installed, deployment-allowed Permission Grant Source | | `SettingsVersion` | int | Positive schema version | | `Settings` | JSON object | Provider-owned validation/migration | | `Order` | int | Stable evaluation order | V1 types are Elsa role grants, claim mappings, and group mappings. Stored permission strings remain open vocabulary, but actor delegation and deployment boundaries are validated before save. ## ClaimProjection | Field | Type | Rules | | --- | --- | --- | | `AllowedClaimTypes` | set | Only these normalized claims survive sign-in processing | | `RedactedClaimTypes` | set | Values hidden in preview/diagnostics | | `MaximumClaimCount` | int | Secure bounded default; deployment may lower | | `MaximumValueLength` | int | Per scalar/array member | | `MaximumTotalBytes` | int | Entire normalized projection | Values are only strings or string arrays. Every value carries adapter/provider provenance during evaluation. Claims outside the projection are discarded. ## ExternalIdentityLink Associates one provider identity with one tenant-owned Elsa User. | Field | Type | Rules | | --- | --- | --- | | `Id` | string | Immutable | | `TenantId` | string | Resolved target tenant; never `*` for a user link | | `ConnectionId` | string | Immutable Identity Provider Connection ID | | `Issuer` | string | Validated canonical issuer namespace | | `SubjectHash` | string | Keyed hash of canonical provider subject | | `SubjectHint` | string? | Optional masked operator hint; never the raw subject | | `UserId` | string | Elsa User in the same tenant | | `CreatedAt` | DateTimeOffset | Creation/prelink time | | `LastSignedInAt` | DateTimeOffset? | Safe operational metadata | ### Constraints - Unique index: `(TenantId, ConnectionId, Issuer, SubjectHash)`. - Foreign key to `User`; broker additionally enforces tenant equality. - Connection archive preserves links. - Explicit unlink removes the active association and emits a notification. - Concurrent JIT and prelink use one atomic create-link-or-get-existing operation. ## User Migration The existing `User` entity changes: | Field | Change | | --- | --- | | `HashedPassword` | Nullable; both password fields are present or absent together | | `HashedPasswordSalt` | Nullable | Credential-less users cannot authenticate through legacy or broker-local password validation. Existing password-backed rows require no data change. JIT provisioning generates and atomically reserves a globally unique internal `User.Name`, sets the resolved tenant, leaves password fields null, and creates the External Identity Link in the same transaction. ## AuthenticationClient Deployment-configured registration for an Elsa client. | Field | Type | Rules | | --- | --- | --- | | `ClientId` | string | Unique immutable identifier | | `DisplayName` | string | Operator-facing | | `ClientType` | enum | `Confidential` or `Public` | | `CallbackUris` | set | Exact HTTPS matches; development loopback may be explicitly allowed | | `LogoutCallbackUris` | set | Exact matches | | `AllowedOrigins` | set | Required for public clients; no wildcard | | `AllowedReturnPathPrefixes` | set | Segment-boundary prefixes for post-authentication client-local navigation | | `SecretBinding` | SecretBinding? | Required for confidential clients; forbidden for public clients | | `IsEnabled` | bool | Disabled clients cannot initiate or exchange | Authentication Clients are not Elsa API Applications and contain no roles or permissions. ## BrokerTransaction Shared, protected, short-lived provider/local/preview correlation state. | Field | Type | Rules | | --- | --- | --- | | `HandleHash` | string | Keyed hash of random browser-visible state handle | | `Purpose` | enum | `ExternalSignIn`, `LocalSignIn`, `Preview`, `UpstreamLogout` | | `ClientId` | string | Registered Authentication Client or preview purpose | | `CallbackUri` | URI | Exact registered callback | | `ReturnPath` | string | Validated client-local path | | `TenantId` | string | Resolved before redirect | | `ConnectionId` | string? | Required for external/preview | | `ConnectionMaterialRevision` | string? | Captured effective revision | | `SecretGenerationFingerprint` | string? | Protected, nonreversible | | `PkceChallenge` | string | S256 only | | `ProviderNonce` | string? | External/preview | | `ProtectedPayload` | bytes | Data-protected adapter state | | `ExpiresAt` | DateTimeOffset | Default 10 minutes | | `ConsumedAt` | DateTimeOffset? | Atomic single-use marker | The public handle contains no protected payload. Atomic take transitions pending to consumed; expired or mismatched state is never revived. ## AuthorizationGrant Single-use Elsa completion code record. | Field | Type | Rules | | --- | --- | --- | | `CodeHash` | string | Keyed hash of random code | | `ClientId` | string | Bound client | | `CallbackUri` | URI | Bound exact callback | | `TenantId` | string | Bound tenant | | `UserId` | string | Resolved Elsa User | | `ExternalSessionId` | string? | Null for broker-local sign-in | | `PkceChallenge` | string | S256 | | `ExpiresAt` | DateTimeOffset | Default 60 seconds | | `ConsumedAt` | DateTimeOffset? | Atomic single use | ## ExternalAuthenticationSession | Field | Type | Rules | | --- | --- | --- | | `Id` | string | Included as safe session claim in access tokens | | `TenantId` | string | Resolved target tenant | | `UserId` | string | Same-tenant Elsa User | | `ConnectionId` | string | Source connection | | `ConnectionMaterialRevision` | string | Revision at full sign-in | | `Issuer` | string | Validated issuer | | `SubjectHash` | string | No raw subject | | `ExternalGrants` | JSON array | Permission strings plus source provenance; no raw claims | | `StartedAt` | DateTimeOffset | Full external sign-in | | `LastRefreshedAt` | DateTimeOffset | Rotation time | | `ExpiresAt` | DateTimeOffset | Maximum session age; default eight hours | | `RefreshExpiresAt` | DateTimeOffset | Inactivity bound | | `CurrentRefreshTokenHash` | string | Keyed hash of current opaque token | | `RefreshGeneration` | long | Compare-and-swap rotation counter | | `RevokedAt` | DateTimeOffset? | Explicit or reuse-detection revocation | | `RevocationReason` | string? | Safe category | Refresh atomically verifies current token hash and generation, rotates the token, reevaluates current Elsa-owned role grants, and retains `ExternalGrants`. Reuse of a superseded token revokes the session. ## ConnectionObservation Latest on-demand test result only. | Field | Type | Rules | | --- | --- | --- | | `ConnectionId` | string | Primary key | | `TestedMaterialRevision` | string | Determines freshness | | `ObservedAt` | DateTimeOffset | UTC | | `Status` | enum | `Succeeded`, `Failed`, `Warning` | | `Category` | string | Stable redacted category | | `Duration` | TimeSpan | Test duration | | `Summary` | string | Safe bounded message | | `Warnings` | list | Safe bounded warnings | | `CorrelationId` | string | Diagnostic correlation | An observation is stale when its tested revision differs from the current effective material revision. No history table is required. ## PreviewResult | Field | Type | Rules | | --- | --- | --- | | `HandleHash` | string | One-time result handle | | `AdministratorId` | string | Initiating authenticated actor | | `TenantId` | string | Preview target | | `ConnectionId` | string | Draft connection | | `MaterialRevision` | string | Exact previewed revision | | `Issuer` | string | Validated issuer | | `MaskedSubject` | string | Never raw subject | | `ProjectedClaims` | JSON object | Allowlisted and descriptor-redacted only | | `PolicyDecision` | JSON object | Proposed action; no mutation | | `PermissionProjection` | JSON array | Proposed grants and provenance | | `Warnings` | list | Safe bounded warnings | | `ExpiresAt` | DateTimeOffset | Default 10 minutes | | `ConsumedAt` | DateTimeOffset? | One-time read | ## PermissionDescriptor Runtime metadata only; not persisted by this feature. | Field | Type | Rules | | --- | --- | --- | | `Name` | string | Elsa permission string | | `DisplayName` | string | Optional authoring label | | `Description` | string? | Optional | | `Category` | string? | Optional grouping | | `SourceModule` | string | Provenance | Descriptors may be incomplete. Their absence never invalidates a permission string.