Admin API & Users
The admin API is a versioned REST surface under /v1/admin on the public listener. It
manages organizations and users, and reads clients, sessions and the audit log. The
admin dashboard is built on it; anything the dashboard does
not do yet, you do by calling the API directly.
Authorization
Every call needs an access token, and two checks must both pass:
- the token carries the operation's OAuth scope, and
- 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.
| Operations | Scope | Permission |
|---|---|---|
| Read an organization, its theme and its email relay | nauthera.orgs:read | orgs.read |
| List organizations | nauthera.orgs:read | orgs.read, from a platform role |
| Create, rename and delete organizations; send a test email | nauthera.orgs:write | orgs.write |
| List and read users | nauthera.users:read | users.read |
| Create a user | nauthera.users:write | users.create |
| Update a user | nauthera.users:write | users.update |
| Delete a user | nauthera.users:write | users.delete, and a recent sign-in |
| Set a user's password | nauthera.users:write | users.credentials, and a recent sign-in |
| List clients, read one | nauthera.clients:read | clients.read |
| List sessions and a user's session history; read a user's known devices | nauthera.sessions:read | sessions.read |
| End one session | nauthera.sessions:write | sessions.revoke |
| End every session of a user | nauthera.sessions:write | sessions.revoke, and a recent sign-in |
| Read the audit log and its catalog | nauthera.audit:read | audit.read |
| Count audit events over time, a client's users, the deployment's client-address posture | nauthera.insights:read | insights.read |
The roles grant these permissions:
| Role | Permissions |
|---|---|
org-viewer | orgs.read, clients.read, insights.read |
org-auditor | orgs.read, users.read, users.pii.read, clients.read, audit.read, insights.read, roles.read |
user-support | orgs.read, users.read, users.pii.read, users.lifecycle, users.activity, sessions.read, sessions.revoke |
manage-users | orgs.read, every users.* permission, sessions.read, sessions.revoke, roles.read |
org-owner | Everything manage-users holds, plus clients.read, audit.read, insights.read, roles.assign |
manage-org | users.pii.read, audit.read |
platform-viewer | orgs.read, clients.read, insights.read |
platform-auditor | orgs.read, clients.read, audit.read, platform.audit, insights.read, roles.read |
manage-organizations | Every permission |
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; 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, renaming and deleting organizations need such a role, so managing
organizations is reserved for the main organization.
Recent sign-in
Deleting a user, setting a password and ending every session of a user 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. 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 three operations need an administrator's token from
{slug}-admin-console.
They also take a required reason query parameter, 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-m2mfor automation, using theclient_credentialsgrant. It registers the user scopes andnauthera.audit:read, plus the organization scopes in the main org. Its roles are fixed when it is provisioned:manage-usersandmanage-org, plusmanage-organizationsin the main org. For an org created through the admin API, its secret is returned once asadmin_m2m_secret. An org created with the gRPCCreateOrganizationcall, whichnauthera-cli create orguses, gets no secret back, so nobody holds it and that client cannot be used. For the main org, the secret is written once toauth.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 and roles, andnauthera.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. Its redirect URI is{issuer}/admin/callback. Only the main organization'sauth.admin.subjectsbootstrap grants roles, and no API grants them yet (#332), so in any other organization this client's tokens open no admin operation. Administer another organization with its{slug}-admin-m2mclient, or with a main-org token whose caller holdsmanage-organizations.
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_pathto a writable path before the server's first start, read thedefault-admin-m2msecret from that file, and use theclient_credentialsrequest 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.subjectsand restart. The account must exist first: self-registration refuses an address that is listed there. The user is granted themanage-organizationsandmanage-orgroles in the main org, which together hold every permission, in every organization. They then sign in to the admin dashboard at{issuer}/admin/, or usedefault-admin-consolewith the authorization code flow and PKCE from their own tooling (see Connect an Application).
Either way, the grant happens once. Removing an address from auth.admin.subjects later
does not demote anyone, and roles cannot yet be revoked over the API (#332).
Users
| Method | Path | Purpose |
|---|---|---|
GET | /v1/admin/orgs/{slug}/users | List and search users (paginated) |
POST | /v1/admin/orgs/{slug}/users | Create 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 |
GET | /v1/admin/orgs/{slug}/users/{id}/devices | The user's known browsers and networks |
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" }
}'emailandpasswordare required. The password must be at least 8 characters. It is hashed with argon2id, or with PBKDF2-HMAC-SHA-256 in an organization in FIPS mode.usernamedefaults to the lowercased email.attributesis a free-form map of strings. OnPATCH, it replaces the stored map.email_verified: trueon create or update marks the address as verified, for an address the administrator vouches for. Changingemailleaves the new address unverified unlessemail_verified: trueis sent with it.- Emails are unique per organization.
- A user record includes
status,email_verified_at,failed_login_attempts,locked_untilandlast_login_at. The password hash is never returned. - Admin access is not a property of the user: it comes only from roles.
Finding users
GET /v1/admin/orgs/{slug}/users takes a search and filters, which combine:
| Parameter | Matches |
|---|---|
q | Users whose username, display name or email starts with it, case-insensitively (2 to 100 characters) |
match | Which of username, display_name and email q searches. Name and email need users.pii.read; asking for one without it is refused with 403. |
status | Comma-separated statuses in force now: active, suspended, blocked |
locked | true for users the lock after repeated wrong passwords refuses now, false for the rest |
second_factor | true for users with an active second factor, false for the rest |
created_from, created_to | Creation time, start included, end excluded |
last_sign_in_from, last_sign_in_to | Last 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:
activemay sign in.suspendedmay not, until it is reactivated or itssuspended_untilpasses, when it readsactiveagain by itself.blockedmay not, until it is reactivated.
A suspended or blocked account cannot sign in, and cannot obtain or renew a token through
any grant. Its existing sessions no longer sign it in. The user record carries
status_reason, suspended_until and status_changed_at. The admin API reports the
status but has no operation to change it yet (#505, #598).
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.
Sessions
| Method | Path | Purpose |
|---|---|---|
GET | /v1/admin/orgs/{slug}/sessions | Active sessions, filtered by user_id or client_id (paginated) |
GET | /v1/admin/orgs/{slug}/users/{id}/session-history | A 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-resetemails 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 forauth.password_reset.validity(default30m, at most24h), and an account is sent at mostauth.password_reset.max_requests(default3) in that time. - Email verification.
POST /v1/auth/verify-email/resendemails a single-use link that marks the address as verified, which ID tokens and UserInfo publish asemail_verified. It is sent only for an address not yet verified. A link works forauth.email_verification.validity(default24h, at most 7 days), with at mostauth.email_verification.max_requests(default3) 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
| Method | Path | Purpose |
|---|---|---|
GET | /v1/admin/orgs/{slug}/clients | List 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}/usage | Its 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
| Method | Path | Purpose |
|---|---|---|
GET | /v1/admin/orgs | List organizations |
POST | /v1/admin/orgs | Create an organization (slug, optional display_name) |
GET | /v1/admin/orgs/{slug} | Read one |
PATCH | /v1/admin/orgs/{slug} | Change its display_name |
DELETE | /v1/admin/orgs/{slug} | Delete it (not the main org) |
GET | /v1/admin/orgs/{slug}/mail | Its email relay, without credentials |
POST | /v1/admin/orgs/{slug}/mail/test | Send a test email to the caller's own address |
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"sorttakes 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
itemsand apageobject. Followpage.next_cursorwhilepage.has_moreis true, and usepage.prev_cursorto 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_atandusername; users who never signed in sort first ascending and last descending. Organizations can be sorted bycreated_at,display_name,idandslug, clients byclient_id,created_at,display_nameandid, and active sessions bycreated_atandid.
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. The only way a user gets a role today is the auth.admin.subjects
bootstrap, which grants manage-organizations and manage-org in the main org. Deleting
a user removes their roles. Listing, granting and revoking roles over the API is planned
(#332), as are monitor and helpdesk-admin roles (#635) and per-application access for
developers (#623). See Admin Dashboard.