FerrisKeyDocs

Users

A user represents an identity within a realm. Users authenticate through clients, prove who they are with credentials, receive tokens, and are authorized through role assignments.

Login, token issuance, authorization: at runtime it all resolves back to a user.

Anatomy of a user

The user record itself is small. It describes who someone is; credentials, roles, and sessions live in related objects.

PropertyDescription
idStable identifier (UUID). Used everywhere tokens reference the subject. Generated, unless you supply one when you create the user.
usernameUnique identifier within the realm. The login handle.
emailEmail address. Optional on the record, but without one the user cannot receive a reset link, a magic link or a verification email.
firstname, lastnameGiven name and family name.
email_verifiedWhether the email has been confirmed.
enabledWhether the account is active. A disabled user cannot authenticate.
required_actionsWhat the user still owes before a login completes.
localeThe language the user chose, or null to follow the realm default. See Localization.
failed_login_attempts, locked_untilLockout state. See Lockout.
federation_provider_idThe user federation provider the account comes from, if any.
client_idSet only on service account users, where it links them to their owning client. Left out otherwise.
realm_idThe realm this user belongs to.
created_at, updated_atTimestamps.

A representative JSON payload returned by the admin API:

{
  "id": "0199a4f2-3c1e-7b8a-9f00-1a2b3c4d5e6f",
  "realm_id": "0199a4ee-91d0-7c3a-8e21-5b7f0c2d4a18",
  "username": "alice",
  "firstname": "Alice",
  "lastname": "Martin",
  "email": "alice@example.com",
  "email_verified": true,
  "enabled": true,
  "required_actions": [],
  "created_at": "2026-10-30T20:36:41.347264Z",
  "updated_at": "2026-10-30T20:36:41.347264Z",
  "failed_login_attempts": 0,
  "locked_until": null,
  "locale": null,
  "federation_provider_id": null
}

Identifiers

Use id to reference a user from other resources (role assignments, audit logs, tokens). username is a human-facing handle and a realm can let users change their own; id never changes.

Creating a user

POST /realms/{realm_name}/users

The body takes username, plus optional email, email_verified, firstname and lastname. A new user has no credential yet: give it a password with reset-password.

The body also takes an optional id. Supply one when another system already identifies the person by a UUID and the sub claim of their tokens has to match. If any user in any realm already has that id, the request fails with 409 and the reason user_id_already_exists. Without an id, FerrisKey generates one.

Realm scoping

Users belong to exactly one realm. The same email address can exist in several realms as entirely separate accounts. There is no cross-realm lookup: authentication always happens inside one realm.

graph LR
    subgraph R1["Realm: home"]
      U1["alice@example.com"]
    end
    subgraph R2["Realm: work"]
      U2["alice@example.com"]
    end
    U1 -. no shared state .- U2

For one identity across several realms, federate it through the Abyss module. Each realm still keeps its own user record, linked to the same external provider.

User lifecycle

A user moves through a small number of states between creation and deletion.

stateDiagram-v2
    [*] --> Provisioned: created (enabled, may have required actions)
    Provisioned --> Active: required actions completed
    Active --> Disabled: admin disables
    Disabled --> Active: admin re-enables
    Active --> Deleted: admin deletes
    Disabled --> Deleted: admin deletes
    Deleted --> [*]
StateWhat it means
ProvisionedThe account exists but may still have pending required actions: verify an email, set a permanent password, enroll in MFA. Until those are cleared, authentication only produces a temporary token.
ActiveAuthentication produces full tokens, and role assignments apply.
Disabledenabled = false. The account is kept but cannot authenticate.
DeletedThe record is removed.

Disabling a user cuts access at once. FerrisKey ends the user’s sessions and revokes their refresh tokens and access tokens, and its own API refuses those access tokens from that moment. An API that only checks the JWT signature offline keeps accepting an access token until it expires, 300 seconds by default. Re-enabling the account lets the user sign in again; it does not bring the revoked sessions back.

Deleting a user, one at a time or in bulk, ends the sessions and revokes the tokens before the record goes.

Required actions

A required action is something a user has to finish before full authentication is granted. While any are pending, the sign-in answers with the actions still owed instead of redirecting back to the application.

ActionWhen it is addedWhat the user must do
verify_emailThe user registered themselves in a realm with email verification on, or an administrator assigned itClick the link sent by the Trident module
update_passwordAn administrator reset the password as temporary, or assigned the actionSet a new password
configure_otpThe realm, or one of the user’s roles, requires MFA and the user has no TOTP credential yet. It waits until a temporary password has been replacedEnroll a TOTP authenticator
configure_passkeyAn administrator assigned it, and the user has at least one credentialRegister a WebAuthn passkey

configure_otp is worked out at each sign-in from the realm’s require_mfa setting and the user’s roles. It is never stored on the user, so you cannot clear it by editing the user: enrolling an authenticator, or lifting the requirement, does that.

How they flow

sequenceDiagram
    participant P as Sign-in page
    participant FK as FerrisKey
    participant U as User

    P->>FK: POST /realms/{realm}/login-actions/authenticate
    FK->>FK: Validate credentials
    FK->>FK: Detect required actions
    FK-->>P: 200 { status: "RequiresActions", required_actions: [...] }
    P->>U: Walk through each action
    U->>FK: Complete the action (login-actions endpoint)
    FK-->>P: 200 { required_actions: [...remaining] }
    Note over P,FK: Loop until no actions remain
    FK-->>P: 200 { url: redirect back to the client }

The sign-in page is the browser-based authorization code flow. The password grant on the token endpoint does not walk through these steps: it refuses to issue tokens for an account that still owes one, and tells the client to use the browser flow.

Temporary tokens

The temporary token that comes with a pending action is a short-lived JWT, sent as an HTTP-only cookie limited to /realms/{realm}/login-actions. It authorizes the required-action endpoints and nothing else. It cannot reach a protected resource or call userinfo.

Order matters

Required actions are evaluated in a fixed order, usually verify_email, then update_password, then the MFA enrollment actions. Read the required_actions array in the response rather than hard-coding that order in a client.

Service account users

When a client has service_account_enabled, FerrisKey creates a linked service account user. It is an ordinary user record with a few distinguishing traits:

  • username is derived from the client (service-account-<client_id>).
  • client_id points back to the owning client.
  • It has no password, and authenticates only through the client_credentials grant.
  • It can be assigned roles and permissions like any other user.

It exists so machine to machine calls have a real subject for authorization decisions and audit logs.

Example

A nightly batch job is represented by a client batch-runner with service_account_enabled = true. The linked service account user service-account-batch-runner is granted the role billing:read.

curl -X POST https://sso.example.com/realms/home/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=batch-runner" \
  -d "client_secret=••••••••"

The resulting access token has sub set to the service account user’s id, and carries the billing:read role. The billing API can authorize the call exactly as it would for a human user.

See Authentication, client credentials for the full grant.

Administering a user

The console groups everything about one account behind five tabs: overview, credentials, role mapping, organizations and attributes.

FerrisKey user credentials tab showing the registered password, the reset form and its validity choice FerrisKey user credentials tab showing the registered password, the reset form and its validity choice

Listing users

GET /realms/{realm_name}/users

The list is paginated: page, limit (1 to 100, 20 by default) and the answer comes back as { data, metadata }. See Paginated listings for the shared contract.

ParameterWhat it does
searchMatches anywhere in the username, email, first name or last name, ignoring case
username, email, firstname, lastnameMatches anywhere in that one field, ignoring case
enabled, email_verifiedExact match on the flag
service_accounttrue keeps only service account users, false keeps only people. Without it both are listed
role_idKeeps the users the role is assigned to
idsA comma-separated list of at most 100 user ids
not_in_groupAn organization group id: keeps the users who are not members of that group
not_in_organizationAn organization id: keeps the users who are not members of it
created_from, created_toBounds on the creation date, RFC 3339 with an offset. created_from is inclusive, created_to is exclusive
order_byusername, email, firstname, lastname, enabled, created_at (the default) or updated_at

Filters combine with AND. A parameter that is not in the table, offset included, is refused with 400 and the reason invalid_query.

Credentials

GET    /realms/{realm_name}/users/{user_id}/credentials
DELETE /realms/{realm_name}/users/{user_id}/credentials/{credential_id}
PUT    /realms/{realm_name}/users/{user_id}/reset-password
POST   /realms/{realm_name}/users/{user_id}/credentials/import

A read lists the authentication methods registered on the account (password, authenticator, passkeys), never the secrets. For a password it shows the algorithm of the stored hash. The stored password is not displayed anywhere: it can only be replaced.

A reset takes a value and a temporary flag, which the console presents as a validity:

ValidityEffect
TemporaryThe user must change it at their next sign-in. Sets the update_password required action
PermanentIt stays valid until the user changes it themselves

The new password has to satisfy the realm’s password policy. A reset ends all of the user’s sessions and revokes their tokens, so the person signs in again with the new password.

Deleting a credential is quiet

Deleting a credential is immediate and irreversible. It does not end the user’s sessions, and the user is not notified: they discover it at their next sign-in.

credentials/import is the migration path for hashes coming from another IAM. See Export & Import.

Roles

FerrisKey user role mapping tab listing assigned roles with their permission counts FerrisKey user role mapping tab listing assigned roles with their permission counts
GET    /realms/{realm_name}/users/{user_id}/roles
POST   /realms/{realm_name}/users/{user_id}/roles/{role_id}
DELETE /realms/{realm_name}/users/{user_id}/roles/{role_id}
GET    /realms/{realm_name}/users/{user_id}/permissions
GET    /realms/{realm_name}/users/{user_id}/organizations

permissions is the resolved set, the union of every role the account holds. It is what to read when you are debugging “why can this user do that”. roles is the assignment list; permissions is the consequence.

Attributes

GET    /realms/{realm_name}/users/{user_id}/attributes
PUT    /realms/{realm_name}/users/{user_id}/attributes
DELETE /realms/{realm_name}/users/{user_id}/attributes/{key}

Free-form key-value metadata. Attributes reach tokens only through a protocol mapper on an active scope: storing one changes nothing on its own.

Lifecycle operations

DELETE /realms/{realm_name}/users/bulk
POST   /realms/{realm_name}/users/{user_id}/unlock

bulk deletes several accounts in one call, and it reports the batch as a batch: one user.bulk_deleted webhook carrying the id list, and one user_deleted audit event whose details hold user_ids. Neither produces one record per account, so a consumer that counts rows will undercount a bulk delete.

unlock clears a lockout before its window expires. See Lockout.

GET    /realms/{realm_name}/users/{user_id}/identity-provider-links
DELETE /realms/{realm_name}/users/{user_id}/identity-provider-links/{link_id}

The external identities brokered onto this account. Deleting a link does not delete the account; the user can no longer sign in through that provider, and removal raises identity_provider_link_removed in the audit log.

Required permissions: view_users or manage_users to read, manage_users to write. manage_realm grants both.

Lockout

After too many failed attempts an account locks itself, governed by two realm settings: lockout_threshold (default 10 attempts) and lockout_duration_seconds (default 900, so fifteen minutes).

The counter goes up on a wrong password and on a wrong one-time code at the MFA challenge, so a guessed OTP is throttled like a guessed password. A successful sign-in resets it. While the account is locked, even a correct password or code is refused.

The user carries the state: failed_login_attempts and locked_until. A locked account authenticates again on its own once the window passes, or immediately if an administrator calls unlock.

Failed attempts are recorded as login_failure in SeaWatch, against the account that was targeted. SeaWatch events carry no IP address, so the source of a spray such as “one IP, forty accounts, nine attempts each” shows up in Compass instead, whose flows are searchable by ip_address.

Sessions

A user can hold several sessions at once, one per browser, device, or service holding a valid refresh token.

ConceptLifetimeWhat it represents
Auth sessionSeconds to minutesAn in-progress login (state, nonce, code, MFA challenge). Discarded once tokens are issued.
User sessionHours to daysAn authenticated session. Backed by a refresh token.
Access tokenMinutesShort-lived bearer token derived from a user session.

Ending a user session revokes its refresh token, and FerrisKey’s own API refuses the access tokens issued from it from then on. An API that only checks the signature offline accepts them until they expire. Tokens and Authentication, auth sessions have the details.

Listing and revoking

GET    /realms/{realm_name}/users/{user_id}/sessions
DELETE /realms/{realm_name}/users/{user_id}/sessions/{session_id}

The list is paginated and includes expired sessions. It filters on search (IP address or user agent), ip_address, user_agent, persistent and the created_from and created_to bounds, and sorts on last_seen_at, expires_at or created_at. See Paginated listings.

API only

These two have no screen in the admin console: a user’s sessions are reachable through the API only. The user’s own account page does have a sessions tab, so the person can review their own.

Session creation and revocation are both audited, as session_created and session_revoked.

Security considerations

Disable rather than delete. Disabling keeps the audit history and the role assignments, and it ends the user’s access immediately. Delete only when you are certain the identity will never come back.

To force a password rotation, reset the password as temporary. The next sign-in triggers the update_password required action. A password never expires on its own, because the realm’s max_age_days is not applied; see Password Policy.

To force MFA enrollment, set require_mfa on the realm. Existing users without a TOTP credential get configure_otp on their next sign-in.

To cut a user’s access without disabling the account, end their sessions with the DELETE above, or reset their password.

Reference users by id in logs and external systems. Usernames and emails change; the id does not.