Skip to content

The v1 docs follow nauthera-server's main branch and its 0.x releases on the way to 1.0, so fields and APIs can still change.

v1

Admin API & Users

Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated

The admin API is a versioned REST surface under /v1/admin on the public listener. It manages organizations, users, their status and roles, broker memberships and email domains, and reads clients, identity providers, sessions, the audit log, the threat signals and the problems operators report. The admin dashboard is built on it; what the dashboard does not offer, you do by calling the API directly.

Authorization

Every call needs an access token, and two checks must both pass:

  1. the token carries the operation's OAuth scope, and
  2. the caller holds the operation's permission, which the server maps from the caller's roles through a fixed table.

The roles that count are those the caller holds in the organization that issued the token. For a user's token, the server reads the user's roles from the database on every call and ignores the token's roles claim, so a role removed there stops working on the next call, and a role added there works without a new token. A client_credentials token's roles are the ones in its roles claim, which only an organization's {slug}-admin-m2m client carries. The API contract declares each operation's scope and permission (x-permission).

Only an organization's own reserved clients, {slug}-admin-console and {slug}-admin-m2m, can obtain a token that opens the admin API. The gRPC control plane refuses an admin scope on any other client, and the admin API refuses a token minted for any other client with 403.

OperationsScopePermission
Read who the caller is and what they may do (/v1/admin/me)nauthera.orgs:readorgs.read
Read an organization, its theme, its email relay and its identity providersnauthera.orgs:readorgs.read
List an organization's broker memberships and email-domain claims; list and read the problems operators reported for its resourcesnauthera.orgs:readorgs.read
List and read the problems of resources in no organizationnauthera.orgs:readorgs.read, from a platform role
List organizationsnauthera.orgs:readorgs.read, from a platform role
Create, update and delete organizations; send a test emailnauthera.orgs:writeorgs.write
List and read users; list a user's linked identity provider accountsnauthera.users:readusers.read
Create a user; send a new invitationnauthera.users:writeusers.create
Update a usernauthera.users:writeusers.update
Delete a usernauthera.users:writeusers.delete, and a recent sign-in
Set a user's password; require a new one at their next sign-innauthera.users:writeusers.credentials, and a recent sign-in
Email a user a password reset linknauthera.users:writeusers.credentials
Suspend, block, reactivate and unlock a usernauthera.users:writeusers.lifecycle, and a recent sign-in
List a user's roles, and the roles the caller may changenauthera.roles:readroles.read
Grant or revoke a rolenauthera.roles:writeroles.assign, and a recent sign-in
List clients, read onenauthera.clients:readclients.read
List sessions and a user's session history; read a user's known devicesnauthera.sessions:readsessions.read
End one sessionnauthera.sessions:writesessions.revoke
End every session of a user; forget the browsers a user's second factor remembersnauthera.sessions:writesessions.revoke, and a recent sign-in
Read the audit log and its catalog; rank who is behind audit eventsnauthera.audit:readaudit.read
Count audit events over time, an organization's users, sessions and clients, a client's users, the deployment's client-address posturenauthera.insights:readinsights.read
Read the threat signals and the shape of the refused sign-insnauthera.threats:readthreats.read
Invite, accept, limit and end broker membershipsnauthera.brokers:writebrokers.manage, and a recent sign-in
Claim, verify and remove email domainsnauthera.domains:writedomains.manage, and a recent sign-in

Roles are named <scope>-<job>: the scope is the organization the role is held in (org) or every organization (platform). They grant these permissions:

RoleLabelPermissions
org-viewerViewerorgs.read, clients.read, insights.read
org-auditorAuditororgs.read, users.read, users.pii.read, clients.read, audit.read, insights.read, threats.read, roles.read
org-supportSupport agentorgs.read, users.read, users.pii.read, users.lifecycle, users.activity, sessions.read, sessions.revoke
org-user-adminUser administratorEverything org-support holds, plus users.create, users.update, users.delete, users.credentials, roles.read
org-ownerOwnerEverything org-user-admin and org-auditor hold, plus roles.assign, brokers.manage and domains.manage
platform-viewerPlatform viewerorgs.read, clients.read, insights.read
platform-auditorPlatform auditororgs.read, clients.read, audit.read, platform.audit, insights.read, threats.read, roles.read
platform-ownerPlatform ownerAll 21 permissions, orgs.write and platform.audit included

The three platform roles count only in the main organization; held in any other organization, they grant nothing. users.pii.read decides whether a caller may search users by name or email, and whether session listings and known devices include names, addresses and user agents, and linked accounts their subjects; without it they are withheld. Audit reads include platform-scoped records only for a caller with platform.audit. A call refused for a missing permission answers 403, and its detail names the permission. Each refusal by these checks is recorded as an admin.access.denied audit event with required_permission and required_scope.

A token acts only in the organization that issued it; on another organization's path it gets 403. The exception is a caller who holds a platform role in the main organization: they can act in any organization, with the permissions their main-org roles grant. Listing, creating, updating and deleting organizations need such a role, so managing organizations is reserved for the main organization.

Protected accounts

No administrator changes their own account or a more privileged one. Changing a user's email or username; setting their password, requiring a new one or emailing them a reset link; suspending, blocking, reactivating or unlocking them; and deleting them are refused with 403 protected_account when the user is the caller, or holds, in any organization, a permission beyond orgs.read, clients.read and insights.read that the caller lacks. Changing any such privileged account also needs a caller holding roles.assign. So a support agent or user administrator acts only on end users and viewers. Ending a user's sessions only removes access, and is not refused this way.

An identity broker's linked account takes no credential, email or username of its own, since its member organization decides all three: setting its password, emailing it a reset link, or changing its email or username is refused with protected_account from every caller. Each refusal is recorded as admin.access.denied with the reason protected_account and its cause.

Recent sign-in

Deleting a user; setting a password or requiring a new one; suspending, blocking, reactivating and unlocking a user; granting and revoking a role; ending every session of a user; forgetting a user's remembered browsers; and every broker-membership and email-domain write need a sign-in no older than auth.admin.max_auth_age (default 15m, at most 24h), with a second factor when the deployment requires one (mfa.required). An older token is answered 401 insufficient_user_authentication with an RFC 9470 challenge carrying max_age; sign in again and repeat the call. A client_credentials token has no sign-in, so it never passes: these operations need an administrator's token from {slug}-admin-console, and no machine client grants a role.

The user, role and session operations among them also take a required reason query parameter (at most 200 characters), as does ending one session. It is recorded on the audit event and nowhere else.

If the token is DPoP-bound, send it with the DPoP scheme (Authorization: DPoP <token>) and a matching DPoP proof on every call. A bound token sent as Bearer is refused.

Getting a token

Each organization has two reserved admin clients:

  • {slug}-admin-m2m for automation, using the client_credentials grant. It registers the user scopes and nauthera.audit:read, plus the organization scopes in the main org. Its roles are fixed when it is provisioned: org-user-admin and org-auditor, plus platform-owner in the main org. It registers no role, session, client, insights or threat scope. For an org created through the admin API, its secret is returned once as admin_m2m_secret. An org created with the gRPC CreateOrganization call, which nauthera-cli create org uses, gets no secret back, so nobody holds it and that client cannot be used. For the main org, the secret is written once to auth.admin.bootstrap_secret_path, which is empty by default, and then the secret is discarded.
  • {slug}-admin-console, a public PKCE client for people, which the admin dashboard signs in with. It registers every admin scope the dashboard uses: users, audit, clients, sessions, insights, threats and roles, nauthera.brokers:write and nauthera.domains:write, and nauthera.orgs:read (both organization scopes in the main org). The admin API reads the signed-in user's roles from the database on every call, so the client's tokens open what the user's roles in that organization grant, and nothing for a user who holds none. Its redirect URI is {issuer}/admin/callback.
TOKEN=$(curl -s https://auth.example.com/token \
  -u "default-admin-m2m:$ADMIN_M2M_SECRET" \
  -d grant_type=client_credentials \
  -d scope="nauthera.users:read nauthera.users:write" | jq -r .access_token)

The first administrator

On a fresh install no user account holds an admin role. There are two ways in:

  • The bootstrap secret. Set auth.admin.bootstrap_secret_path to a writable path before the server's first start, read the default-admin-m2m secret from that file, and use the client_credentials request above. The Helm chart leaves this off (adminBootstrap.enabled: false) because its image has no shell to read the file with; you need a sidecar or debug container that shares the volume.
  • A promoted user. Create the user's account, then list their email address in auth.admin.subjects and restart. The account must exist first: self-registration refuses an address that is listed there, and in the main org the admin API refuses to create an account with a listed address, or to change an account's email to one, unless the caller already holds everything the bootstrap grants. The user is granted platform-owner in the main org, which holds every permission in every organization. They then sign in to the admin dashboard at {issuer}/admin/, or use default-admin-console with the authorization code flow and PKCE from their own tooling (see Connect an Application).

Either way, the grant happens once, and is bound to the account rather than the address. Removing an address from auth.admin.subjects later demotes nobody; revoke the role through the roles operations instead, and a restart does not undo the revoke.

From there, a platform owner grants roles to other users. To hand a tenant organization to its own administrators, create a user in it and grant them org-owner there; they then grant roles in their organization from its own dashboard.

Users

MethodPathPurpose
GET/v1/admin/orgs/{slug}/usersList and search users (paginated)
POST/v1/admin/orgs/{slug}/usersCreate a user
GET/v1/admin/orgs/{slug}/users/{id}Read a user
PATCH/v1/admin/orgs/{slug}/users/{id}Update email, username, email_verified or attributes
DELETE/v1/admin/orgs/{slug}/users/{id}?reason=…Delete a user and revoke their sessions and refresh tokens
PUT/v1/admin/orgs/{slug}/users/{id}/password?reason=…Set a new password and end the user's sessions
POST/v1/admin/orgs/{slug}/users/{id}/password-change?reason=…Send the user's next password sign-in to the page that sets a new password
POST/v1/admin/orgs/{slug}/users/{id}/password-resetEmail the user the forgot-password link, naming the administrator
POST/v1/admin/orgs/{slug}/users/{id}/suspend?reason=…Suspend the user, until until or until reactivated
POST/v1/admin/orgs/{slug}/users/{id}/block?reason=…Block the user until reactivated
POST/v1/admin/orgs/{slug}/users/{id}/reactivate?reason=…End a suspension or block
POST/v1/admin/orgs/{slug}/users/{id}/unlock?reason=…Release the lock repeated wrong passwords set
POST/v1/admin/orgs/{slug}/users/{id}/invitationEmail a user who has no password yet a new invitation
GET/v1/admin/orgs/{slug}/users/{id}/devicesThe user's known browsers and networks
DELETE/v1/admin/orgs/{slug}/users/{id}/devices/remembered?reason=…Stop remembering one browser (device), or all of them, for the user's second factor
GET/v1/admin/orgs/{slug}/users/{id}/identitiesThe identity provider accounts linked to the user
GET/v1/admin/orgs/{slug}/users/{id}/rolesThe user's roles, and the roles the caller may change
PUT/v1/admin/orgs/{slug}/users/{id}/roles/{role}?reason=…Grant a role
DELETE/v1/admin/orgs/{slug}/users/{id}/roles/{role}?reason=…Revoke a role
curl -s https://auth.example.com/v1/admin/orgs/default/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "email": "alice@example.com",
        "password": "a-long-initial-password",
        "attributes": { "name": "Alice Example" }
      }'
  • email is required, and exactly one of password and invite: true; both or neither is refused 400. The password must have between auth.password.min_length and auth.password.max_length characters (8 and 128 by default). It is hashed with argon2id, or with PBKDF2-HMAC-SHA-256 in an organization in FIPS mode. For invite, see Inviting a user.
  • username defaults to the lowercased email.
  • attributes is a free-form map of strings. On PATCH, it replaces the stored map.
  • email_verified: true on create or update marks the address as verified, for an address the administrator vouches for. Changing email leaves the new address unverified unless email_verified: true is sent with it.
  • Emails are unique per organization.
  • A user record includes status, email_verified_at, password_set, password_change_required, invitation_pending, second_factors (the methods of the user's active second factors), failed_login_attempts, locked_until and last_login_at. The password hash is never returned.
  • Changing a user's email, username, password or status and deleting them follow the protected-account rule. In the main org, an address listed in auth.admin.subjects is refused like a taken one unless the caller holds everything the bootstrap grants.
  • Admin access is not a property of the user: it comes only from roles.

Inviting a user

With "invite": true instead of a password, the user is created without one, and their address is emailed a single-use link to the hosted page /accept-invitation, naming the administrator who invited them. Following it sets their password and verifies the address. locale picks the language of the message (English, German or French; any other falls back to English). The organization needs an email relay: without one, the create is refused 409 mail_relay_missing and nothing is stored.

curl -s https://auth.example.com/v1/admin/orgs/default/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "bob@example.com", "invite": true, "locale": "de" }'

A link works for auth.invitation.validity (default 168h, at most 30 days), and stops working once the account has a password, however it got one, or holds another address. Until then, signing in to the account is refused as a wrong password is. That sign-in, a password-reset or verification request for the address, or a sign-up with it, is answered as for any address and emails the account a new invitation, naming the organization as the inviter, whose link returns to the application the user started from (Emailed links). POST …/users/{id}/invitation sends a new link; earlier ones keep working until they expire or one is accepted. It is refused 409 conflict when the user already has a password, and 429 once the user was sent auth.invitation.max_requests (default 5) links within one validity window, the ones they asked for themselves included. invitation_pending is true while the user has no password and no linked identity provider account, so an account a provider created is never shown as waiting on an invitation. Sending one is recorded as user.invitation.sent, and accepting it as auth.invitation.accepted.

Finding users

GET /v1/admin/orgs/{slug}/users takes a search and filters, which combine:

ParameterMatches
qUsers whose username, display name or email starts with it, case-insensitively (2 to 100 characters)
matchWhich of username, display_name and email q searches. Name and email need users.pii.read; asking for one without it is refused with 403.
statusComma-separated statuses in force now: active, suspended, blocked
lockedtrue for users the lock after repeated wrong passwords refuses now, false for the rest
second_factortrue for users with an active second factor, false for the rest
email_verifiedtrue for users whose address is verified, false for the rest
created_from, created_toCreation time, start included, end excluded
last_sign_in_from, last_sign_in_toLast sign-in time. Users who never signed in match last_sign_in_to and not last_sign_in_from.
curl -s "https://auth.example.com/v1/admin/orgs/default/users?q=ali&status=active&sort=-last_login_at" \
  -H "Authorization: Bearer $TOKEN"

Account status

An administrator's decision about an account is its status, separate from the automatic lock after repeated wrong passwords:

  • active may sign in.
  • suspended may not, until it is reactivated or its suspended_until passes, when it reads active again by itself.
  • blocked may not, until it is reactivated.

A suspended or blocked account cannot sign in, and cannot obtain or renew a token through any grant. The user record carries status_reason, suspended_until and status_changed_at.

POST …/users/{id}/suspend (with an optional until), /block and /reactivate change the status, and /unlock releases the automatic lock before it lifts by itself. Each needs users.lifecycle, a recent sign-in and a reason. Suspending and blocking end every session the user holds, retire their refresh grants and tell relying parties over back-channel logout; after reactivation the user signs in again. A suspension, block or reactivation emails the account's holder, and the changes are recorded as user.suspended, user.blocked, user.reactivated and user.unlocked.

By default a refused sign-in answers like a wrong password. auth.brute_force.reveal_lockout tells a caller with the right password that the account is suspended or blocked, and tells any caller that a lock stands.

Known devices

GET /v1/admin/orgs/{slug}/users/{id}/devices lists the browsers and networks the user signed in from, most recently seen first, with when each was first and last seen. These are what a new-sign-in alert compares a sign-in with. A browser is named by its browser and system (Firefox on Linux), and a network by its range (203.0.113.0/24). Without users.pii.read the names are withheld and withheld is true. Reading the list is audited.

A browser can also be remembered for the user's second factor, where the organization's policy spares the challenge on one. DELETE /v1/admin/orgs/{slug}/users/{id}/devices/remembered forgets the browser the device parameter names (its id in the list), or every browser of the user without it, so the next sign-in from it asks for the factor. The browsers stay known, so a sign-in from one is not reported as new. It needs a recent sign-in and a reason, and is recorded as user.remembered_browsers.forgotten.

Linked accounts

GET /v1/admin/orgs/{slug}/users/{id}/identities lists the accounts at the organization's identity providers that sign in as the user, oldest link first: the provider, the issuer that assigned the subject, the subject, and when the account was first linked. active is false once the provider was deleted or re-pointed at another issuer, so the link no longer signs anyone in. Without users.pii.read the subjects are withheld and withheld is true. Reading the list is audited as user.identities.listed. GET /v1/admin/orgs/{slug}/identity-providers lists the providers themselves, without their secrets; they are declared with an IdentityProvider resource.

Sessions

MethodPathPurpose
GET/v1/admin/orgs/{slug}/sessionsActive sessions, filtered by user_id or client_id (paginated)
GET/v1/admin/orgs/{slug}/users/{id}/session-historyA user's ended sessions and why they ended, between from and to (default the last 30 days, at most 90)
DELETE/v1/admin/orgs/{slug}/sessions/{sid}?reason=…End one session
DELETE/v1/admin/orgs/{slug}/users/{id}/sessions?reason=…Sign the user out everywhere

Each session carries its public sid, the one its applications receive, never the browser's credential. Ended sessions are kept for auth.session.history_retention. See Sessions, Consent & Logout.

Signing up, verifying an address, resetting a password

Users can register themselves on the hosted UI when auth.registration.enabled is on.

The sign-in app also offers two links by email. Both need the organization to have an email relay, and both answer 202 whether or not the address has an account, so neither tells who is registered:

  • Password reset. POST /v1/auth/password-reset emails a single-use link to set a new password; the sign-in app's forgot-password page sends it. Setting the new password spends every reset link the account holds, ends its sessions and the grants issued under them, and clears a lockout, as an administrator's reset does. A link works for auth.password_reset.validity (default 30m, at most 24h), and an account is sent at most auth.password_reset.max_requests (default 3) in that time.
  • Email verification. POST /v1/auth/verify-email/resend emails a single-use link that marks the address as verified, which ID tokens and UserInfo publish as email_verified. It is sent only for an address not yet verified. A link works for auth.email_verification.validity (default 24h, at most 7 days), with at most auth.email_verification.max_requests (default 3) per account in that time. Nothing sends one automatically at registration.

A link that is unknown, expired or already used is refused with invalid_reset_link or invalid_verification_link. Requests count toward the per-address sign-in throttle.

Ending a user's access

Setting a password is meant as an incident-response control. It ends every session the user has, retires every refresh token the user was issued, drops any sign-in that is halfway through an MFA step, and tells the applications of each ended session over back-channel logout. The user keeps the account and signs in again with the new password. Deleting a user revokes all of their refresh tokens and ends their sessions before the account is removed. Ending one session or all of a user's sessions, above, does the same for sessions alone.

Access tokens already issued stay valid until they expire, because revocation does not reach access tokens yet (#91). Keep issuer.access_token_ttl short.

Clients

MethodPathPurpose
GET/v1/admin/orgs/{slug}/clientsList the organization's OIDC clients (paginated, searchable with q)
GET/v1/admin/orgs/{slug}/clients/{id}One client's registered configuration
GET/v1/admin/orgs/{slug}/clients/{id}/usageIts daily, monthly and all-time users

These are read-only. Clients are registered and changed through the gRPC AdminService, the operator or dynamic registration; see Clients.

Organizations

MethodPathPurpose
GET/v1/admin/orgsList organizations
POST/v1/admin/orgsCreate an organization: slug, and optionally display_name, fips_mode, fapi_mode, email_verification_required, branding, second_factor and network_zones
GET/v1/admin/orgs/{slug}Read one
PATCH/v1/admin/orgs/{slug}Change any of those but slug and fips_mode, which are fixed at creation
DELETE/v1/admin/orgs/{slug}Delete it (not the main org)
GET/v1/admin/orgs/{slug}/themeThe theme the admin dashboard wears for it
GET/v1/admin/orgs/{slug}/mailIts email relay, without credentials
POST/v1/admin/orgs/{slug}/mail/testSend a test email to the caller's own address
GET/v1/admin/orgs/{slug}/identity-providersIts identity providers, without their secrets
GET/v1/admin/orgs/{slug}/snapshotCount its users, sessions and clients
GET, POST/v1/admin/orgs/{slug}/broker-membershipsList its broker memberships, on either side; invite a member
POST/v1/admin/orgs/{slug}/broker-memberships/{id}/acceptAccept a broker's invitation, as the member
PUT/v1/admin/orgs/{slug}/broker-memberships/{id}/clientsChoose which of the broker's clients the membership admits
DELETE/v1/admin/orgs/{slug}/broker-memberships/{id}End a membership, from either side
GET, POST/v1/admin/orgs/{slug}/email-domainsList its email-domain claims; claim a domain
POST/v1/admin/orgs/{slug}/email-domains/{id}/verifyVerify a claim against its DNS TXT record
DELETE/v1/admin/orgs/{slug}/email-domains/{id}Remove a claim
GET/v1/admin/orgs/{slug}/problems, /v1/admin/orgs/{slug}/problems/{id}The resources an operator could not apply for it, with the reason and its runbook

Beside them, GET /v1/admin/problems lists, for platform staff, the problems of resources in no organization, GET /v1/admin/me says who the caller is and what they may do, and GET /v1/admin/deployment/client-ip reports how the deployment reads the client address. The API reference holds every operation. See Organizations.

Pagination and sorting

Lists use opaque cursor (keyset) pagination:

curl -s "https://auth.example.com/v1/admin/orgs/default/users?limit=50&sort=-created_at,email" \
  -H "Authorization: Bearer $TOKEN"
  • sort takes a comma-separated list of fields. A leading - sorts that field descending. The primary key is appended so the order is total.
  • The response has items and a page object. Follow page.next_cursor while page.has_more is true, and use page.prev_cursor to go back. No total count is returned. A cursor is bound to the search and filters it was issued for, so send them with it.
  • Sortable fields are declared per endpoint and checked against the request. Users can be sorted by created_at, email, id, last_login_at and username; users who never signed in sort first ascending and last descending. Organizations can be sorted by created_at, display_name, id and slug, clients by client_id, created_at, display_name and id, and active sessions by created_at and id.

Roles

Roles are stored per user and organization; see Authorization for what each grants and where the server reads them. Access tokens also carry a user's roles in the roles claim, set when the token is issued, but the admin API ignores the claim on a user's token. Deleting a user removes their roles.

GET /v1/admin/orgs/{slug}/users/{id}/roles returns the roles the user holds in the organization, and assignable: the roles the caller may grant to or revoke from them.

curl -s -X PUT \
  "https://auth.example.com/v1/admin/orgs/acme/users/$USER_ID/roles/org-owner?reason=New+team+lead" \
  -H "Authorization: DPoP $TOKEN" -H "DPoP: $PROOF"

A grant or a revoke answers 204, also when the user already held the role or did not hold it. Both need roles.assign, which only org-owner and platform-owner hold, a recent sign-in and a reason, within these bounds:

  • Within your own authority. A caller grants or revokes a role only when their own roles cover every permission it grants, and its platform reach. So an org-owner hands out every organization role, org-owner included, and no platform role.
  • Never your own. Nobody grants or revokes their own roles.
  • Platform roles in the main org only. Granted anywhere else, they would grant nothing, so they are refused.
  • Never the last one who can grant. Revoking a role from the organization's last active account holding roles.assign is refused 409 last_role_administrator, so no organization is left without someone who can grant roles.

The first three answer 403 protected_account. The role must be one of the eight built-in ids. The user is emailed when a role they did not hold is granted, or one they held is revoked, through the organization's relay when it has one (see Email). Each change is recorded as role.granted or role.revoked with the role, the reason and whether the user was notified; a refusal is recorded too. A role counts from the user's next request.

Grants are made within one organization, to that organization's own users. Granting main-org staff a role in another organization (#543), listing everyone who holds a role in an organization (#601, #332), further bounds on owner-tier grants (#542), monitor and helpdesk-admin roles (#635) and per-application access for developers (#623) are planned. See Admin Dashboard.