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:
- 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 who the caller is and what they may do (/v1/admin/me) | nauthera.orgs:read | orgs.read |
| Read an organization, its theme, its email relay and its identity providers | nauthera.orgs:read | orgs.read |
| List an organization's broker memberships and email-domain claims; list and read the problems operators reported for its resources | nauthera.orgs:read | orgs.read |
| List and read the problems of resources in no organization | nauthera.orgs:read | orgs.read, from a platform role |
| List organizations | nauthera.orgs:read | orgs.read, from a platform role |
| Create, update and delete organizations; send a test email | nauthera.orgs:write | orgs.write |
| List and read users; list a user's linked identity provider accounts | nauthera.users:read | users.read |
| Create a user; send a new invitation | 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; require a new one at their next sign-in | nauthera.users:write | users.credentials, and a recent sign-in |
| Email a user a password reset link | nauthera.users:write | users.credentials |
| Suspend, block, reactivate and unlock a user | nauthera.users:write | users.lifecycle, and a recent sign-in |
| List a user's roles, and the roles the caller may change | nauthera.roles:read | roles.read |
| Grant or revoke a role | nauthera.roles:write | roles.assign, 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; forget the browsers a user's second factor remembers | nauthera.sessions:write | sessions.revoke, and a recent sign-in |
| Read the audit log and its catalog; rank who is behind audit events | nauthera.audit:read | audit.read |
| Count audit events over time, an organization's users, sessions and clients, a client's users, the deployment's client-address posture | nauthera.insights:read | insights.read |
| Read the threat signals and the shape of the refused sign-ins | nauthera.threats:read | threats.read |
| Invite, accept, limit and end broker memberships | nauthera.brokers:write | brokers.manage, and a recent sign-in |
| Claim, verify and remove email domains | nauthera.domains:write | domains.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:
| Role | Label | Permissions |
|---|---|---|
org-viewer | Viewer | orgs.read, clients.read, insights.read |
org-auditor | Auditor | orgs.read, users.read, users.pii.read, clients.read, audit.read, insights.read, threats.read, roles.read |
org-support | Support agent | orgs.read, users.read, users.pii.read, users.lifecycle, users.activity, sessions.read, sessions.revoke |
org-user-admin | User administrator | Everything org-support holds, plus users.create, users.update, users.delete, users.credentials, roles.read |
org-owner | Owner | Everything org-user-admin and org-auditor hold, plus roles.assign, brokers.manage and domains.manage |
platform-viewer | Platform viewer | orgs.read, clients.read, insights.read |
platform-auditor | Platform auditor | orgs.read, clients.read, audit.read, platform.audit, insights.read, threats.read, roles.read |
platform-owner | Platform owner | All 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_ 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:)
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:org-user-adminandorg-auditor, plusplatform-ownerin 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 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., which is empty by default, and then the secret is discarded.admin. bootstrap_ secret_ path {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:writeandnauthera.domains:write, 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, 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.to a writable path before the server's first start, read theadmin. bootstrap_ secret_ path default-admin-m2msecret from that file, and use theclient_credentialsrequest above. The Helm chart leaves this off (adminBootstrap.) because its image has no shell to read the file with; you need a sidecar or debug container that shares the volume.enabled: false - 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, 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 grantedplatform-ownerin the main org, which holds 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, 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
| Method | Path | Purpose |
|---|---|---|
GET | / | List and search users (paginated) |
POST | / | Create a user |
GET | / | Read a user |
PATCH | / | Update email, username, email_verified or attributes |
DELETE | / | Delete a user and revoke their sessions and refresh tokens |
PUT | / | Set a new password and end the user's sessions |
POST | / | Send the user's next password sign-in to the page that sets a new password |
POST | / | Email the user the forgot-password link, naming the administrator |
POST | / | Suspend the user, until until or until reactivated |
POST | / | Block the user until reactivated |
POST | / | End a suspension or block |
POST | / | Release the lock repeated wrong passwords set |
POST | / | Email a user who has no password yet a new invitation |
GET | / | The user's known browsers and networks |
DELETE | / | Stop remembering one browser (device), or all of them, for the user's second factor |
GET | / | The identity provider accounts linked to the user |
GET | / | The user's roles, and the roles the caller may change |
PUT | / | Grant a role |
DELETE | / | 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" }
}'emailis required, and exactly one ofpasswordandinvite: true; both or neither is refused400. The password must have betweenauth.password.min_lengthandauth.password.max_lengthcharacters (8 and 128 by default). It is hashed with argon2id, or with PBKDF2-HMAC-SHA-256 in an organization in FIPS mode. Forinvite, see Inviting a user.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,password_set,password_change_required,invitation_pending,second_factors(the methods of the user's active second factors),failed_login_attempts,locked_untilandlast_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.subjectsis 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 …/ 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. (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 / 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 |
email_verified | true for users whose address is verified, 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. The user record carries status_reason, suspended_until and
status_changed_at.
POST …/ (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. 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 / 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 / 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..
Linked accounts
GET / 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 / lists the
providers themselves, without their secrets; they are declared with an
IdentityProvider resource.
Sessions
| Method | Path | Purpose |
|---|---|---|
GET | / | Active sessions, filtered by user_id or client_id (paginated) |
GET | / | A user's ended sessions and why they ended, between from and to (default the last 30 days, at most 90) |
DELETE | / | End one session |
DELETE | / | 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.. See
Sessions, Consent & Logout.
Signing up, verifying an address, resetting a password
Users can register themselves on the hosted UI when auth. 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 /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 forv1/ auth/ password-reset auth.(defaultpassword_ reset. validity 30m, at most24h), and an account is sent at mostauth.(defaultpassword_ reset. max_ requests 3) in that time. - Email verification.
POST /emails a single-use link that marks the address as verified, which ID tokens and UserInfo publish asv1/ auth/ verify-email/ resend email_verified. It is sent only for an address not yet verified. A link works forauth.(defaultemail_ verification. validity 24h, at most 7 days), with at mostauth.(defaultemail_ verification. max_ requests 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_. 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 | / | List the organization's OIDC clients (paginated, searchable with q) |
GET | / | One client's registered configuration |
GET | / | 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, and optionally display_name, fips_mode, fapi_mode, email_, 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 | / | The theme the admin dashboard wears for it |
GET | / | Its email relay, without credentials |
POST | / | Send a test email to the caller's own address |
GET | / | Its identity providers, without their secrets |
GET | / | Count its users, sessions and clients |
GET, POST | / | List its broker memberships, on either side; invite a member |
POST | / | Accept a broker's invitation, as the member |
PUT | / | Choose which of the broker's clients the membership admits |
DELETE | / | End a membership, from either side |
GET, POST | / | List its email-domain claims; claim a domain |
POST | / | Verify a claim against its DNS TXT record |
DELETE | / | Remove a claim |
GET | /, / | 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 / 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"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. Deleting a user removes their roles.
GET / 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-ownerhands out every organization role,org-ownerincluded, 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.assignis refused409last_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.