Replies: 8 comments 5 replies
|
Finalized account states
These are the published statuses. They are not four values of one column:
Precedence on the wire: Only TransitionsstateDiagram-v2
direction LR
[*] --> ENABLED
state ENABLED {
direction LR
[*] --> ACTIVE
ACTIVE --> LOCKED : failed attempts reach threshold
LOCKED --> ACTIVE : expiry / self-service recovery / admin unlock
ACTIVE --> BLOCKED : admin block<br/>revoke sessions + tokens
LOCKED --> BLOCKED : admin block<br/>revoke sessions + tokens
BLOCKED --> LOCKED : admin unblock<br/>entity-scoped lock, no expiry
}
ENABLED --> DISABLED : admin disable · revoke · condition preserved
DISABLED --> ENABLED : admin enable · preserved condition restored
Unblock is not a release. It clears containment and leaves the account What each status permits
Storage pattern{
"accessState": {
"lock": {
"verifiers": {
"credential": { "failureCount": 0, "lastFailedAt": "2026-09-02T10:04:12Z",
"lockCount": 2, "unlockAt": "2026-09-02T10:19:12Z" },
"otp": { "failureCount": 1, "lastFailedAt": "2026-09-02T10:03:50Z",
"lockCount": 2, "unlockAt": "2026-09-02T10:19:12Z" }
}
},
"block": { "since": "2026-09-10T11:02:00Z" },
"lastLoginAt": "2026-09-09T18:22:04Z"
}
}Under "lock": {
"entity": { "failureCount": 0, "lastFailedAt": "2026-09-02T10:04:12Z",
"lockCount": 2, "unlockAt": "2026-09-02T10:19:12Z" }
}
|
Finalized storage and user responseStorageFor database-backed entities, lifecycle state and runtime attributes remain in the existing
For declarative entities, the resource file is treated as the source of the entity definition, but it cannot store runtime changes. Therefore, server-owned runtime state is stored in
For declarative entities, a configured Runtime-only values such as failure counters, User API responseThe effective status is derived at read time using the following precedence: Proposed response: {
"id": "01a0942a-9793-712b-bdd9-1b0faa69af2e",
"type": "Person",
"attributes": {
"email": "user1@gmail.com",
"username": "user1"
},
"status": {
"value": "LOCKED",
"details": {
"reason": "FAILED_ATTEMPTS",
"lockedScopes": [
{
"scope": "credential",
"expiresAt": "2026-09-12T10:45:15Z"
},
{
"scope": "otp",
"expiresAt": "2026-09-12T10:50:15Z"
}
]
}
},
"lastLoginAt": "2026-09-11T08:24:12Z"
}
{
"scope": "entity"
}An omitted Status precedence also applies to the returned details. For example, if an entity is both blocked and locked, the response exposes These details should only be exposed through authorized management APIs. Authentication error responses should not reveal lock scope, reason, or expiry information. Agents and applicationsAgents and applications use the same Currently, only the following effective statuses apply: Automatic locking is not enabled for Agents and applications entity categories. |
Automatic lock notificationFor admin operations such as Disable / Enable and Block / Unblock, the operation is executed through a flow, so any required notification can be configured as part of that flow. Automatic locking is different because a lock can be formed during credential verification across different authentication flows. Therefore, the automatic-lock notification should be configured together with the common account-lock configuration. Proposed configuration: account_access:
user:
lock_granularity: verifier # verifier or entity
default:
enabled: true
threshold: 5
failure_window_seconds: 900
lock_durations_seconds: [300, 900, 3600, 86400]
lock_decay_seconds: 86400
disclose_hold_reason: never
scopes:
otp:
threshold: 3
failure_window_seconds: 600
lock_durations_seconds: [300, 900]
lock_decay_seconds: 3600
entity:
notifications:
on_lock:
email:
enabled: true
recipient_attribute: email
user_types:
employee:
recipient_attribute: workEmail
customer:
recipient_attribute: emailAddress
service_user:
enabled: falseProposed behavior:
|
|
Hi @yeshandev @PasinduYeshan , The authn provider manager's just routing between authn provider implementations, it shouldn't own this governance logic IMO. And if we push governance to the manager level, it'll need an entity/governance dependency even for setups that only use an external custom provider and manage their own governance. That dependency would just sit there unused since the manager can't check state for entities it doesn't own anyway. For entities we manage, we handle governance too. If someone's entities live on their side, they implement that as part of their own authentication logic, not something we can enforce from the manager. |
|
Hi @PasinduYeshan , Informative proposal.
End user notifications - As all the modifications are done by the admin which may or may not notify to the account holder or end user case by case.
Thanks and Regards |
Spec PR |








Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Related Feature Issue
#5073
Problem Summary
ThunderID has no account lifecycle management. An entity is either present and usable or absent, which leaves three gaps an operator hits immediately:
Architecture Overview
ThunderID's account access rules live in one identity governance service. Authentication, sessions, token issuance and flows ask this service whether access is allowed. An entity is a user, agent or application. A verifier checks a credential, such as a password or one-time password (OTP).
flowchart TB subgraph RUNTIME["Runtime path · depends only on providers"] direction LR AUTHN["Authentication<br>provider manager"] TOKEN["Token builder"] ACTOR["Actor provider<br>OAuth client funnel"] EXEC["Flow executors"] ADMINEXEC["Identity governance<br>executor · disable · enable · unlock"] REVEXEC["Criteria revocation and<br>session revocation<br>executors · existing"] end AUTHN --> PORT{{"IdentityGovernanceProvider"}} TOKEN --> PORT ACTOR --> PORT EXEC --> PORT ADMINEXEC --> PORT PORT -. default provider delegates to .-> GOV["Identity governance service"] SERVERONLY["Atomic authentication endpoints / DefaultAuthnProvider"] --> GOV GOV --> POLICY["Policy resolver"] GOV -- trusted state read --> ENTITY["Entity service"] GOV -- narrow state write --> ENTITY ENTITY -. "failure and credential-change callbacks" .-> GOV ENTITY --> STORE["Entity store"] STORE --> DB[("ENTITY table")] ADMINEXEC -- publishes the trusted plan --> REVEXEC REVEXEC --> CONTAIN["Revocation and<br>session services"] & MAILNODE["Email executor · existing"]The provider interface lets the full server and embedded engine use the same access checks. The server wires its governance service through the default provider; an embedder can supply a provider. Services that only run in the full server can call governance directly.
Each check supplies the entity ID, the operation being attempted and, where relevant, the verifier. Governance decides which restrictions apply. Callers do not repeat state or policy rules.
States and actions
The current design has three public statuses:
ACTIVELOCKEDDISABLEDENTITY.STATEThe API calculates effective status in this order:
DISABLED>LOCKED>ACTIVE. A disabled account can still have a lock underneath it. Adminenableremoves the disable only;unlockclears automatic locks and their counters only. Neither action reverses the other.Lock scope and policy
A verifier is not necessarily one credential field or delivery channel. Password, or any credential answer share the
credentialverifier; OTP has its ownotpscope. These are the two lockable verifier scopes in this phase. Other sign-in methods still receive the disabled-account check.Lock scope
entitycounterDecision: support both (configurable); default to per-verifier. Under entity-wide locking,
block_existing_sessionsandblock_token_issuancecontrol the extra restrictions. Both default tofalseand have no effect under per-verifier locking. A lock never revokes sessions or tokens; these flags block their use at the relevant checks while the lock applies.Lock policy
At this state, account-lock policies will be defined at the server level in
deployment.yaml. In the future, once centralized policy management is introduced, account-lock policies should also be manageable through that framework.Each entity category starts with a default policy. Verifier-specific settings override only the fields they explicitly define. When entity-wide locking is configured, the
entitysettings override the corresponding category defaults instead.This example matches the supplied policy defaults. The user default locks after five failures. Its failure count starts over after 15 minutes without a failure. Repeated lockouts last 5 minutes → 15 minutes → 1 hour → 1 day; the last duration repeats. After a day beyond the previous lock's end, the next lock starts at the first duration again.
A duration of
0means no automatic expiry. Recovery through a completed recovery or admin unlock can still release it.lock_decay_seconds: 0means lockout history does not decay with time.Automatic locking is off for agents and applications in this phase; their credential-failure handling needs further work. Administrators can still disable and enable them. A future policy store can add rules by user type or other criteria through the policy resolver.
A held account uses the normal authentication-failure response. Hold reasons are hidden by default and controlled by server policy.
Administration and notifications
Lifecycle actions run as administration flows through
IdentityGovernanceExecutor. Default flows provide disable and enable for users, agents and applications, plus unlock for users.The default disable flow runs these steps:
Governance owns the state change. The following flow nodes own revocation, session termination and notification. Access checks continue to enforce
DISABLEDeven if a later step fails. Token revocation also depends on where tokens are validated; it does not promise instant removal of access everywhere.Automatic lock notifications are deferred. A lock forms inside failed credential verification, which may return to the input prompt without running another flow node.
Storage and writes
Current Approach (Not decided):
ENTITY.STATE:ACTIVEorDISABLEDENTITY.SYSTEM_ATTRIBUTES.accessStatestatusExample lock data:
{ "accessState": { "credential": { "failureCount": 0, "lastFailedAt": "2026-09-10T10:04:12Z", "lockCount": 2, "unlockAt": "2026-09-10T10:19:12Z" } } }failureCounttracks failed attempts;lockCountselects the next duration. A timed lock is active whilenow < unlockAt; a no-expiry lock uses a reserved timestamp. Expiry needs no database write or scheduled job.ENTITY.STATEholds only states that require a write to change, soLOCKEDdoes not belong there.Counter increments should use atomic database updates. Forming a lock checks that both observed counters still match before writing, with bounded retries for conflicts. Resets use narrow writes. Generic profile updates must preserve this server-owned data, and must not change the administrative state.
Security Considerations
Impacted Areas
Alternatives Considered
A. How should an admin security hold be represented?
For a suspected compromise, an admin may need to stop access, replace unsafe credentials or recovery
channels, then invite the user to set a new credential before restoring access. The current
DISABLEDstate already blocks access and allows admin edits. A dedicated admin-initiated credential-setup flow is future work under every option.LOCKEDwith a triggerSUSPENDEDDISABLEDwithSUSPENDEDACTIVE,LOCKED,DISABLEDACTIVE,LOCKED,SUSPENDED,DISABLEDACTIVE,LOCKED,SUSPENDEDLOCKEDwithADMIN_INITIATEDSUSPENDEDSUSPENDEDLOCKEDAdmin-initiated lock: denied
SUSPENDED: denied
DISABLED: denied
SUSPENDED: denied
DISABLED: denied
SUSPENDED: allowed
DISABLED: denied
SUSPENDED: allowed
DISABLED: only can enable
DISABLED: only can enable
Preference: option 3 if we do not need two distinct admin restrictions. Option 2 needs a clear reason to treat suspension and disablement differently, such as offering admin-initiated credential setup only while suspended.
B. Where should server-owned runtime data live?
Declarative entities loaded from resource files have no writable database row. Supporting runtime disable and
lock for them needs writable storage. The same question applies to future data such as verified attributes or last-login time.
ENTITYdataENTITYSYSTEM_ATTRIBUTESdocument, runtime writes could replace system attributes declared by the resource. Ownership/merge rules are required so only extension-owned attributes are written thereSYSTEM_ATTRIBUTES, so lock/disable updates cannot replace themPreference: Option 2, dedicated runtime-state table
Keep ThunderID-owned runtime state in one writable location for every entity, regardless of whether its profile comes from database, resource file, or secondary user store. This gives failure counting, locking, credential-change tracking, and future verification data one storage implementation. Separating ownership also prevents profile updates from overwriting runtime state.
Suggested structure in
entitydb:ENTITY_RUNTIME_STATEtable name is not finalized.Tradeoff: additional read integration now, and potentially another extension later. Prefer this over permanently routing ThunderID-owned runtime writes between
ENTITYand fallback storage. Existing runtime data must be preserved before switching authority.All reactions