# HTTP Endpoints of nauthera-server

> Every HTTP endpoint nauthera-server serves: discovery, OAuth 2.0 and OpenID Connect, client registration, the sign-in and admin APIs, and the other listeners.

All paths below are relative to an organization's base URL. That is the server root for
the main org and `/orgs/{slug}` for every other org (see
[Organizations](https://www.nauthera.io/docs/v1/organizations)). Every public-listener endpoint on this page
is declared in the OpenAPI document `api/openapi/nauthera.yaml`, except those marked as
mounted by hand, and the discovery document advertises only endpoints that actually
serve requests. A test drives the live server and fails the build if an advertised
endpoint answers `temporarily_unavailable`.

## Discovery

| Method | Path | Specification |
|---|---|---|
| `GET` | `/.well-known/openid-configuration` | OpenID Connect Discovery 1.0 |
| `GET` | `/.well-known/oauth-authorization-server` | RFC 8414 |
| `GET` | `/.well-known/jwks.json` | RFC 7517, with ES256 and RS256 keys |

For every org other than the main one, the discovery documents and the JWKS under
`/orgs/{slug}/.well-known/` are mounted by hand. Both discovery documents are also served
at the RFC 8414 path-inserted locations on the server root,
`/.well-known/openid-configuration/orgs/{slug}` and
`/.well-known/oauth-authorization-server/orgs/{slug}`, which are mounted by hand too.

## OAuth 2.0 and OpenID Connect

| Method | Path | Notes |
|---|---|---|
| `GET`, `POST` | `/authorize` | Authorization code flow. `response_type=code`, `response_mode=query`, PKCE S256. Supports `prompt` (`none`, `login`, `consent`, `select_account`), `max_age`, `id_token_hint`, `login_hint`, `resource`, `dpop_jkt`, a signed `request` object, or a PAR `request_uri`. Returns `iss` (RFC 9207). |
| `POST` | `/par` | Pushed authorization requests (RFC 9126). Returns a single-use `urn:ietf:params:oauth:request_uri:` reference. |
| `POST` | `/token` | Grants: `authorization_code`, `refresh_token`, `client_credentials`, `urn:ietf:params:oauth:grant-type:device_code`. Token exchange and the JWT bearer grant answer `503 temporarily_unavailable` and are not advertised. |
| `POST` | `/device_authorization` | Device authorization grant (RFC 8628). `verification_uri` is the issuer followed by `/device`. |
| `POST` | `/introspect` | RFC 7662, including `cnf` for DPoP-bound tokens. Caller-bound: only the confidential client the token was issued to may introspect it. |
| `POST` | `/revoke` | RFC 7009. Revokes refresh tokens; access tokens run to expiry (#91). |
| `GET`, `POST` | `/userinfo` | Claims gated by scope. A DPoP-bound token must be sent with the `DPoP` scheme and a matching proof. |
| `GET`, `POST` | `/end_session` | RP-initiated logout. Ends the session at once when a verified `id_token_hint` names the signed-in user, redirects without asking when nobody is signed in, and asks the user to confirm in every other case. |
| `GET` | `/check_session_iframe` | OIDC Session Management. Off by default (`auth.session_management.enabled`). The flag controls the discovery entry, `session_state` and the browser-state cookie; the frame itself is served either way, and any page may embed it. The advertised URL needs a `client_id` query parameter to answer (#313). |

Front-channel and back-channel logout are both supported and advertised
(`frontchannel_logout_supported`, `backchannel_logout_supported`). Front-channel runs
only when `/end_session` ends the session at once.

**Tokens.** Access tokens are JWTs with `typ: at+jwt` (RFC 9068), signed with ES256 or
RS256. Depending on scope and flow, ID tokens and access tokens carry `sub`, `iss`,
`aud`, `exp`, `iat`, `jti`, `nbf`, `auth_time`, `nonce`, `at_hash`, `amr`, `acr`, `sid`,
`scope`, `client_id`, `org`, `roles`, `fam`, `email`, `email_verified`, `name`,
`preferred_username`, `member_org` and `member_org_slug`. `fam`, on a user's access token,
names the grant family the token belongs to, which a replayed code or refresh token
revokes. `member_org` (in both tokens) and `member_org_slug` (in the ID token) name the
member organization an [identity broker](https://www.nauthera.io/docs/v1/identity-broker) signed the account in
from. See
[Tokens & Sender Constraint](https://www.nauthera.io/docs/v1/tokens).

## Dynamic client registration

| Method | Path | Notes |
|---|---|---|
| `POST` | `/register` | RFC 7591. Needs an initial access token with `nauthera.clients:register`. |
| `GET`, `PUT`, `DELETE` | `/register/{client_id}` | RFC 7592. Needs the registration access token. Read and update responses do not repeat it (#78). |

## Hosted sign-in API

The [hosted sign-in UI](https://www.nauthera.io/docs/v1/login-experience) calls these JSON endpoints. They are
part of the public contract, but you will rarely call them yourself.

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/v1/auth/sign-in` | Password sign-in. May answer with an MFA step. Answers `404 not_found` before the password is checked unless it names a `client_id` registered in the organization or continues the device flow. Asks for a captcha after repeated failures. |
| `POST` | `/v1/auth/passkey/options`, `/v1/auth/passkey/verify` | Passwordless sign-in with a passkey, where the second-factor policy opens it. Needs a registered client like the password sign-in. |
| `GET` | `/v1/auth/captcha` | The captcha this deployment asks for: a fresh ALTCHA challenge, or a hosted provider's site key. |
| `POST` | `/v1/auth/register` | Self-registration. Off by default (`auth.registration.enabled`). Needs a captcha solution when one is configured. |
| `POST` | `/v1/auth/password-reset` | Email a single-use link to set a new password. Answers `202` whether or not the address has an account. Needs a captcha solution. |
| `POST` | `/v1/auth/password-reset/confirm` | Set a new password from the link. Ends the account's sessions, retires every refresh token it was given, and clears a lockout. |
| `POST` | `/v1/auth/invitation/accept` | Choose a password from an invitation link, which also verifies the address. |
| `POST` | `/v1/auth/verify-email/resend` | Email a single-use link that verifies the address. Answers `202` whether or not the address has an account. Needs a captcha solution. |
| `POST` | `/v1/auth/verify-email/confirm` | Mark the address verified from the link. |
| `GET` | `/v1/auth/federation/{slug}/start` | Send the browser to the organization's identity provider `{slug}`, with state, nonce and PKCE. The client must offer the provider. |
| `GET` | `/v1/auth/federation/{slug}/callback` | The identity provider's redirect back: redeems its code, verifies its ID token, and signs in, links or signs up the account. Every refusal goes back to the sign-in page with `federation_error`. |
| `POST` | `/v1/auth/broker/route` | A [broker's](https://www.nauthera.io/docs/v1/sign-in-flows/broker) email step: route an address to the member organization that verified its domain. The answer depends on the domain alone. |
| `GET` | `/v1/auth/broker/{member}/start` | Send the browser to the member organization's own sign-in, with state, nonce and PKCE. The membership must be active and admit the client. |
| `GET` | `/v1/auth/broker/{member}/callback` | The member's redirect back: redeems its code, verifies its ID token, and signs in the broker's account linked to the member account. Every refusal goes back to the sign-in page with `federation_error`. |
| `POST` | `/v1/auth/sign-out` | End the session. |
| `POST` | `/v1/auth/logout/confirm` | Confirm an RP-initiated logout. |
| `POST` | `/v1/auth/consent`, `/v1/auth/consent/decision` | Load and answer the consent prompt. |
| `POST` | `/v1/auth/device/verify`, `/v1/auth/device/decision` | Look up and approve or deny a device user code. |
| `GET` | `/v1/auth/mfa` | MFA status. |
| `GET` | `/v1/auth/mfa/totp/enroll` | Start TOTP enrolment. |
| `POST` | `/v1/auth/mfa/totp/activate` | Confirm enrolment with a first code. |
| `POST` | `/v1/auth/mfa/totp/recovery-ack` | Acknowledge the recovery codes. |
| `POST` | `/v1/auth/mfa/totp/verify` | Answer a TOTP challenge. |
| `POST` | `/v1/auth/mfa/email/enroll`, `/v1/auth/mfa/email/activate` | Email the code that enrols the emailed-code factor, and activate it with that code. |
| `POST` | `/v1/auth/mfa/email/send`, `/v1/auth/mfa/email/verify` | Email a sign-in code, and answer the challenge with it. |
| `POST` | `/v1/auth/mfa/passkey/registration/options`, `/v1/auth/mfa/passkey/registration/verify` | Register a passkey as a second factor. |
| `POST` | `/v1/auth/mfa/passkey/authentication/options`, `/v1/auth/mfa/passkey/authentication/verify` | Answer the second-factor challenge with a passkey. |
| `POST` | `/v1/auth/mfa/passkey/offer/decline` | Decline the offer to add a passkey and finish the sign-in. |
| `POST` | `/v1/auth/mfa/recovery-code/verify` | Answer the challenge with a recovery code. |
| `POST` | `/v1/auth/mfa/recovery-codes` | Regenerate recovery codes. |
| `DELETE` | `/v1/auth/mfa/totp` | Disable TOTP. |

What a change of second factor ends is on
[Multi-Factor Authentication](https://www.nauthera.io/docs/v1/mfa#changing-a-factor); the screens are in the
[sign-in flows](https://www.nauthera.io/docs/v1/sign-in-flows).

## Admin API

The admin API lives at the server root, and the organization it acts on is named in the
path. A token acts in the org whose issuer minted it, and only a token minted for one of
that org's two reserved admin clients is accepted: the dashboard's `{slug}-admin-console`
or the `{slug}-admin-m2m` machine client. A token from any other client is refused, whatever
scopes and roles stand behind it. Every call needs an access token that carries the
operation's scope, and its caller must hold the operation's permission, which the
contract declares per operation (`x-permission`). Permissions come from the roles the
caller holds in the token's org: for a user, read from the database on every call; for
an org's `{slug}-admin-m2m` client, the roles it was provisioned with. A token can act in
another org only if its caller holds a platform role in the main org. See
[Admin API & Users](https://www.nauthera.io/docs/v1/user-management#authorization).

| Method | Path | Scope | Permission |
|---|---|---|---|
| `GET` | `/v1/admin/me` | `nauthera.orgs:read` | `orgs.read` |
| `GET` | `/v1/admin/orgs`, `/v1/admin/orgs/{slug}` | `nauthera.orgs:read` | `orgs.read` |
| `POST` | `/v1/admin/orgs` | `nauthera.orgs:write` | `orgs.write` |
| `PATCH`, `DELETE` | `/v1/admin/orgs/{slug}` | `nauthera.orgs:write` | `orgs.write` |
| `GET` | `/v1/admin/orgs/{slug}/theme` | `nauthera.orgs:read` | `orgs.read` |
| `GET` | `/v1/admin/orgs/{slug}/mail` | `nauthera.orgs:read` | `orgs.read` |
| `POST` | `/v1/admin/orgs/{slug}/mail/test` | `nauthera.orgs:write` | `orgs.write` |
| `GET` | `/v1/admin/orgs/{slug}/snapshot` | `nauthera.insights:read` | `insights.read` |
| `GET` | `/v1/admin/orgs/{slug}/identity-providers` | `nauthera.orgs:read` | `orgs.read` |
| `GET` | `/v1/admin/orgs/{slug}/problems`, `/v1/admin/orgs/{slug}/problems/{id}`, `/v1/admin/problems`, `/v1/admin/problems/{id}` | `nauthera.orgs:read` | `orgs.read` |
| `GET` | `/v1/admin/orgs/{slug}/broker-memberships` | `nauthera.orgs:read` | `orgs.read` |
| `POST` | `/v1/admin/orgs/{slug}/broker-memberships`, `/v1/admin/orgs/{slug}/broker-memberships/{id}/accept` | `nauthera.brokers:write` | `brokers.manage`, recent sign-in |
| `PUT` | `/v1/admin/orgs/{slug}/broker-memberships/{id}/clients` | `nauthera.brokers:write` | `brokers.manage`, recent sign-in |
| `DELETE` | `/v1/admin/orgs/{slug}/broker-memberships/{id}` | `nauthera.brokers:write` | `brokers.manage`, recent sign-in |
| `GET` | `/v1/admin/orgs/{slug}/email-domains` | `nauthera.orgs:read` | `orgs.read` |
| `POST` | `/v1/admin/orgs/{slug}/email-domains`, `/v1/admin/orgs/{slug}/email-domains/{id}/verify` | `nauthera.domains:write` | `domains.manage`, recent sign-in |
| `DELETE` | `/v1/admin/orgs/{slug}/email-domains/{id}` | `nauthera.domains:write` | `domains.manage`, recent sign-in |
| `GET` | `/v1/admin/orgs/{slug}/clients`, `/v1/admin/orgs/{slug}/clients/{id}` | `nauthera.clients:read` | `clients.read` |
| `GET` | `/v1/admin/orgs/{slug}/clients/{id}/usage` | `nauthera.insights:read` | `insights.read` |
| `GET` | `/v1/admin/orgs/{slug}/users`, `/v1/admin/orgs/{slug}/users/{id}` | `nauthera.users:read` | `users.read` |
| `POST` | `/v1/admin/orgs/{slug}/users` | `nauthera.users:write` | `users.create` |
| `PATCH` | `/v1/admin/orgs/{slug}/users/{id}` | `nauthera.users:write` | `users.update` |
| `DELETE` | `/v1/admin/orgs/{slug}/users/{id}` | `nauthera.users:write` | `users.delete`, recent sign-in |
| `PUT` | `/v1/admin/orgs/{slug}/users/{id}/password` | `nauthera.users:write` | `users.credentials`, recent sign-in |
| `POST` | `/v1/admin/orgs/{slug}/users/{id}/password-change` | `nauthera.users:write` | `users.credentials`, recent sign-in |
| `POST` | `/v1/admin/orgs/{slug}/users/{id}/password-reset` | `nauthera.users:write` | `users.credentials` |
| `POST` | `/v1/admin/orgs/{slug}/users/{id}/suspend`, `/block`, `/reactivate`, `/unlock` | `nauthera.users:write` | `users.lifecycle`, recent sign-in |
| `POST` | `/v1/admin/orgs/{slug}/users/{id}/invitation` | `nauthera.users:write` | `users.create` |
| `GET` | `/v1/admin/orgs/{slug}/users/{id}/identities` | `nauthera.users:read` | `users.read` |
| `GET` | `/v1/admin/orgs/{slug}/users/{id}/roles` | `nauthera.roles:read` | `roles.read` |
| `PUT`, `DELETE` | `/v1/admin/orgs/{slug}/users/{id}/roles/{role}` | `nauthera.roles:write` | `roles.assign`, recent sign-in |
| `GET` | `/v1/admin/orgs/{slug}/users/{id}/devices` | `nauthera.sessions:read` | `sessions.read` |
| `DELETE` | `/v1/admin/orgs/{slug}/users/{id}/devices/remembered` | `nauthera.sessions:write` | `sessions.revoke`, recent sign-in |
| `GET` | `/v1/admin/orgs/{slug}/sessions`, `/v1/admin/orgs/{slug}/users/{id}/session-history` | `nauthera.sessions:read` | `sessions.read` |
| `DELETE` | `/v1/admin/orgs/{slug}/sessions/{sid}` | `nauthera.sessions:write` | `sessions.revoke` |
| `DELETE` | `/v1/admin/orgs/{slug}/users/{id}/sessions` | `nauthera.sessions:write` | `sessions.revoke`, recent sign-in |
| `GET` | `/v1/admin/orgs/{slug}/audit-events`, `/v1/admin/orgs/{slug}/audit-events/tail`, `/v1/admin/orgs/{slug}/audit-events/top`, `/v1/admin/orgs/{slug}/audit-events/{id}`, `/v1/admin/audit-catalog` | `nauthera.audit:read` | `audit.read` |
| `GET` | `/v1/admin/orgs/{slug}/audit-events/counts` | `nauthera.insights:read` | `insights.read` |
| `GET` | `/v1/admin/orgs/{slug}/threats`, `/v1/admin/orgs/{slug}/threats/sign-ins` | `nauthera.threats:read` | `threats.read` |
| `GET` | `/v1/admin/deployment/client-ip` | `nauthera.insights:read` | `insights.read` |

The organization update changes the display name, FAPI mode, the verified-address
requirement, the default branding, and the
[second-factor policy and network zones](https://www.nauthera.io/docs/v1/organizations#second-factor-policy-and-network-zones).
A policy or zones the deployment does not allow answer `400` naming the field and the
bound. Deleting an
organization, or changing it, takes effect on every replica within 30 seconds. The
snapshot counts an organization's users, sessions and clients, naming none. The
identity-provider list names the organization's [identity providers](https://www.nauthera.io/docs/v1/identity-provider-resource)
and never returns a provider's secret, only a fingerprint of its sealing. An invitation
emails a user who has no password a new link to choose one; it answers
`409 mail_relay_missing` when the organization has no email relay. A user's identities
are the identity provider accounts linked to it. The role list says which roles the user
holds and which the caller may grant or revoke; granting and revoking are bounded by
the caller's own roles, and revoking the last account that can grant roles answers
`409 last_role_administrator`. Forgetting remembered browsers makes the next sign-in from
them answer the second factor again; it needs a reason, which the audit event records.
Suspending an account, until a set time or until it is reactivated, and blocking it end
every session it holds, retire its refresh grants and tell relying parties over
back-channel logout; reactivating lets it sign in again, and unlocking releases the lock
repeated wrong passwords set. Each needs a reason, and a suspension, block or reactivation
emails the account's holder. Requiring a password change sends the user's next password
sign-in to the page that sets a new one, and the reset operation emails the user the
forgot-password link, naming the administrator. Broker memberships let a
[broker's](https://www.nauthera.io/docs/v1/identity-broker) clients sign in a member organization's users: the
broker invites, the member accepts, the broker chooses the clients a membership admits,
and either side ends it. An email-domain claim is verified against a DNS TXT record and
then routes sign-ins at that domain to the organization. The problem lists name the
resources an operator could not apply, with the reason and its runbook, for an
organization or, to platform staff, for resources in no organization. `/v1/admin/me` says
who the caller is and, per organization, which permissions their roles grant.
The audit ranking names the subjects behind matching records, at most 25, over at most
31 days; ranking or filtering by address also needs `users.pii.read`. The threat
overview and the shape of the refused sign-ins are aggregates over the audit log that
name no address, account or client.

The user list finds users by name or email (`q`, `match`) and filters by `status`,
`locked`, `second_factor`, `email_verified` and creation and last sign-in dates; it sorts
by `created_at`, `email`, `id`, `last_login_at` or `username`. The client list searches and filters by
type, authentication method, grant type and source. The device list names the browsers
and networks a user has signed in from. The session list covers active sessions and
filters by user or client; a user's session history lists the ones that ended. The audit
counts bucket events over time, grouped and filtered like the audit list. The tail returns
audit records in the order they reached the store, from where the last read left off, for
a SIEM or detector following the log. The mail test sends a message through the org's
relay to the calling administrator's own address and reports what the relay answered. The
client-IP endpoint reports where the deployment reads the client address from
(`security.client_ip`), which trusted proxies are wider than they need to be, and whether
recent traffic looks like it arrives through a proxy the configuration does not name. Date-windowed
lists and counts take `from` and `to`, at most 90 days apart. Every list is
cursor-paginated.

The built-in roles are `org-viewer`, `org-auditor`, `org-support`, `org-user-admin` and
`org-owner` in any org, and the platform roles `platform-viewer`, `platform-auditor` and
`platform-owner`, which count only in the main org, where they reach every org. Only
`org-owner` and `platform-owner` hold `roles.assign`, `brokers.manage` and
`domains.manage`. `platform-owner` holds every permission, `platform.audit` included,
which platform-scoped audit records need. Each
organization's machine client holds `org-user-admin` and `org-auditor`, and the main
organization's also `platform-owner`.

No administrator can change the status, credentials, email address or username of, or
delete, their own account or one more privileged than theirs, nor grant or revoke a role
on their own account; such a call answers `403 protected_account`. Ending an account's
sessions is not refused this way, since it only removes access. See
[Admin API & Users](https://www.nauthera.io/docs/v1/user-management#protected-accounts).

An operation marked *recent sign-in* also needs a token whose sign-in is no older than `auth.admin.max_auth_age` (default
15 minutes), with a second factor when `mfa.required` is set; an older or weaker one
gets a `401 insufficient_user_authentication` with an RFC 9470 step-up challenge. A
token with no sign-in behind it, such as a machine client's, is refused there.

The dashboard's console client registers the client, session, insights, threats, role,
broker and email-domain scopes on top of the admin scopes, and a tenant organization's
console also `nauthera.orgs:read`. The machine clients register only the admin scopes:
`nauthera.users:read`, `nauthera.users:write` and `nauthera.audit:read`, plus
`nauthera.orgs:read` and `nauthera.orgs:write` for the main org's.

## Errors

The OAuth endpoints (`/authorize`, `/par`, `/token`, `/device_authorization`,
`/introspect`, `/revoke` and `/end_session`) answer with the RFC 6749 error object, and
`/register` with the RFC 7591 one. Every other endpoint, `/userinfo` included, answers
with an RFC 9457 `application/problem+json` document that carries no internal detail. Its
`code` is one of the codes the OpenAPI document lists, and its `type` is
`https://nauthera.io/problems/<code>`, the code's [runbook](https://www.nauthera.io/docs/v1/runbooks/problems).
An OAuth error object carries the error's runbook as `error_uri`. A problem without a `code`, such as a generic `500`, has
`type: about:blank`. A `401` from `/userinfo` also carries a `WWW-Authenticate`
challenge. Every response carries an `X-Request-ID` header.

A request refused by the rate limit, or for a repeated header, gets a problem document
on **every** endpoint, OAuth ones included. So does a request to an OAuth endpoint that
cannot be read, repeats a form parameter or fails schema validation, instead of the
RFC 6749 error object (#324), and a request to `/register` whose body cannot be read, is
missing or fails schema validation, instead of the RFC 7591 one. A browser navigation can
get an HTML error page instead of the JSON body, and always does when the rate limit
refuses it. See [Errors & Troubleshooting](https://www.nauthera.io/docs/v1/troubleshooting#error-formats).
OAuth responses are sent with `Cache-Control: no-store`.

## Public banner

`GET /healthz` on the public listener answers `{"service":"nauthera-server","version":"…"}`.
It reports the build version and checks no dependency. It is mounted by hand, so it is
not in the OpenAPI document. The Kubernetes probes use the admin listener.

The [admin dashboard](https://www.nauthera.io/docs/v1/admin-dashboard) is served as static files at `/admin/` for the
main org and `/orgs/{slug}/admin/` for every other org, also mounted by hand. Every other
path that no endpoint claims is the hosted sign-in UI, except an unknown `/v1/` path,
which answers a `404` problem document.

## Admin listener (`:9090`)

| Path | Purpose |
|---|---|
| `/metrics` | Prometheus exposition |
| `/healthz` | Liveness |
| `/readyz` | Readiness |
| `/startupz` | Startup |
| `/debug/pprof/*` | Only when `server.admin.pprof: true` |

## gRPC listener (`:9091`)

`grpc.health.v1.Health` and `nauthera.admin.v1.AdminService`. See
[Architecture](https://www.nauthera.io/docs/v1/architecture#the-control-plane).
