Documentation menu

API reference

43 operations in auth. Raw spec: openapi.json.

GET/api/v1/auth/edge/sources

GET /api/auth/edge/sources — admin listing of edge-assertion sources.

Responses

StatusDescription
200
403

Security: bearer

PUT/api/v1/auth/edge/sources/{name}

PUT /api/auth/edge/sources/{name} — create or replace an edge source. Pins are stored verbatim and enforced byte-for-byte at verification time; every write is audited.

Parameters

NameInTypeRequiredDescription
namepathstringYes

Request body

UpsertEdgeSourceRequest

Responses

StatusDescription
200
403
409this name is already claimed by another workspace

Security: bearer

DELETE/api/v1/auth/edge/sources/{name}

DELETE /api/auth/edge/sources/{name}.

Parameters

NameInTypeRequiredDescription
namepathstringYes

Responses

StatusDescription
204
403
404

Security: bearer

POST/api/v1/auth/edge/{name}/exchange

POST /api/auth/edge/{name}/exchange — swaps a valid `Cf-Access-Jwt-Assertion` (header) for Marula tokens. The assertion MUST verify against the source's pinned team JWKS with issuer AND audience pinned; failures are a uniform typed 401.

Parameters

NameInTypeRequiredDescription
namepathstringYes

Responses

StatusDescription
200
401
POST/api/v1/auth/email-verification/confirm

POST /api/auth/email-verification/confirm — marks the email verified (single-use, audited).

Request body

TokenRequest

Responses

StatusDescription
200
401invalid or expired token
429
POST/api/v1/auth/email-verification/request

POST /api/auth/email-verification/request — resend (public, THROTTLED; admin override rides POST /api/org/users/{id}/email-verified).

Request body

EmailVerifyRequest

Responses

StatusDescription
200
429
POST/api/v1/auth/login

POST /api/auth/login — resolves the org by email (SECURITY DEFINER bridge), verifies the argon2 hash under RLS scope. When any second factor is enrolled, returns a short-lived `mfa_pending` bridge instead of tokens; POST /api/auth/mfa/verify completes it.

Answers 503 with `Retry-After` when the process's password-hash slots are saturated (Wave 56 lane `p56-auth-bounds`) — backpressure under a burst, never a 500. Left out of the `utoipa` response list for the reason given on [`register`].

Request body

LoginRequest

Responses

StatusDescription
200
401bad credentials
POST/api/v1/auth/logout

POST /api/auth/logout — revokes the refresh session and audits the event.

Request body

RefreshRequest

Responses

StatusDescription
200
POST/api/v1/auth/mfa/recovery-codes/regenerate

POST /api/auth/mfa/recovery-codes/regenerate — invalidates all previous codes, issues a new batch (revealed once).

Request body

RegenerateRecoveryRequest

Responses

StatusDescription
200
401

Security: bearer

POST/api/v1/auth/mfa/step-up

POST /api/auth/mfa/step-up — proves password plus a live factor and records it against this sign-in lineage.

This is the door the destructive factor changes point at. It is deliberately separate from `mfa/verify`: that one completes a login, this one raises what an already-signed-in session may do, and the record it leaves expires. A passkey user does not call this — their step-up is the passkey login, which stamps the same record on the session it creates.

Request body

MfaStepUpRequest

Responses

StatusDescription
200
401
428

Security: bearer

POST/api/v1/auth/mfa/totp/disable

POST /api/auth/mfa/totp/disable — requires password AND a live factor; wipes TOTP enrollment and all recovery codes.

This is the explicit "turn MFA off" action the last-factor rule points at. A successful call also stamps a step-up, because the caller just proved a factor on this session; the next change within the window does not re-prompt.

Request body

TotpDisableRequest

Responses

StatusDescription
204
401

Security: bearer

POST/api/v1/auth/mfa/totp/enroll-begin

POST /api/auth/mfa/totp/enroll-begin — generates a secret (returned ONCE) and stores it sealed+unconfirmed.

A FIRST enrolment (no confirmed factor) simply restarts the pending window. Replacing a CONFIRMED factor is a destructive act and needs a fresh step-up proved by a method OTHER than TOTP: a live code cannot authorize replacing the secret it belongs to, so the caller uses a recovery code or a passkey. The account keeps its recovery codes through the swap, which is what stops it holding zero working factors while the new secret is pending. After an admin reset (`users.mfa_reenroll_required`) enrolment is the sanctioned way out and the audited reset is its authorization: there is no factor left to step up with, which is exactly why the reset travels in this lane.

Responses

StatusDescription
200

Security: bearer

POST/api/v1/auth/mfa/totp/enroll-verify

POST /api/auth/mfa/totp/enroll-verify — confirms the enrollment with the first valid code and issues the one-time recovery-code batch.

Request body

TotpEnrollVerifyRequest

Responses

StatusDescription
200
401

Security: bearer

POST/api/v1/auth/mfa/verify

POST /api/auth/mfa/verify — completes an MFA-gated login with a TOTP or recovery code. Tries TOTP first (advancing the replay watermark), then falls back to single-use recovery codes.

Request body

MfaVerifyRequest

Responses

StatusDescription
200
401
POST/api/v1/auth/mfa/webauthn/begin

POST /api/auth/mfa/webauthn/begin — assertion request for the pending MFA login. Requires a valid mfa_pending token.

Request body

MfaTokenRequest

Responses

StatusDescription
200
401
GET/api/v1/auth/mfa/webauthn/credentials

GET /api/auth/mfa/webauthn/credentials — list registered passkeys.

Responses

StatusDescription
200

Security: bearer

POST/api/v1/auth/mfa/webauthn/credentials/{id}/remove

POST /api/auth/mfa/webauthn/credentials/{id}/remove — removes one passkey.

The password is not the authorization. A phished password must not be able to downgrade a passkey-only account to password-only sign-in (DECISIONS record 2), so removal needs a FRESH step-up on this sign-in lineage, proved by a factor that is not the credential being removed. The last factor of any kind cannot leave this way: that is the explicit "turn MFA off" action (`POST /api/auth/mfa/totp/disable`), and the refusal says so.

Parameters

NameInTypeRequiredDescription
idpathstringYes

Request body

WebauthnRemoveRequest

Responses

StatusDescription
204

Security: bearer

POST/api/v1/auth/mfa/webauthn/finish

POST /api/auth/mfa/webauthn/finish — verifies the assertion and issues full tokens. Ceremony state is single-use; replay of a finish call fails.

Request body

WebauthnLoginFinishRequest

Responses

StatusDescription
200
401
POST/api/v1/auth/mfa/webauthn/register/begin

POST /api/auth/mfa/webauthn/register/begin — starts a passkey registration for the authenticated user.

Responses

StatusDescription
200

Security: bearer

POST/api/v1/auth/mfa/webauthn/register/finish

POST /api/auth/mfa/webauthn/register/finish — verifies the attestation and stores the passkey. The public key is stored; no secret exists.

Request body

WebauthnRegisterFinishRequest

Responses

StatusDescription
204

Security: bearer

GET/api/v1/auth/oidc/presets

GET /api/auth/oidc/presets — the declarative preset catalog that drives the admin "add provider" UI. Pure data; no configuration.

Responses

StatusDescription
200

Security: bearer

GET/api/v1/auth/oidc/providers

GET /api/auth/oidc/providers — admin listing. Secrets excluded by type.

Responses

StatusDescription
200
403

Security: bearer

GET/api/v1/auth/oidc/providers/public

GET /api/auth/oidc/providers/public — drives config-driven SSO buttons on the login screen. No configuration detail beyond names.

Responses

StatusDescription
200
PUT/api/v1/auth/oidc/providers/{name}

PUT /api/auth/oidc/providers/{name} — create or replace a provider. The client secret is required on every write (rotate-friendly).

Parameters

NameInTypeRequiredDescription
namepathstringYes

Request body

UpsertProviderRequest

Responses

StatusDescription
200
403
409this name is already claimed by another workspace

Security: bearer

DELETE/api/v1/auth/oidc/providers/{name}

DELETE /api/auth/oidc/providers/{name}.

Parameters

NameInTypeRequiredDescription
namepathstringYes

Responses

StatusDescription
204
403
404

Security: bearer

GET/api/v1/auth/oidc/{name}/callback

GET /api/auth/oidc/{name}/callback — validates state signature/expiry/ provider binding and nonce echo, runs the conservative decision, then hands tokens (or link confirmation) to the web app via a single-use code.

Parameters

NameInTypeRequiredDescription
namepathstringYes

Responses

StatusDescription
303redirect to web app
GET/api/v1/auth/oidc/{name}/login

GET /api/auth/oidc/{name}/login — 303 to the IdP with signed state, nonce, and PKCE S256 bound together.

Parameters

NameInTypeRequiredDescription
namepathstringYes

Responses

StatusDescription
303redirect to provider
404
POST/api/v1/auth/password-reset/confirm

POST /api/auth/password-reset/confirm — consumes the token, sets the new password, and revokes ALL of the user's live sessions (the reset's security payload: a stolen session cannot outlive it).

Request body

PasswordResetConfirm

Responses

StatusDescription
200
401invalid or expired token
422validation error
429
POST/api/v1/auth/password-reset/request

POST /api/auth/password-reset/request — issues a single-use token + mail when the account exists; ALWAYS the uniform answer.

Request body

PasswordResetRequest

Responses

StatusDescription
200
429flood throttle
POST/api/v1/auth/refresh

POST /api/auth/refresh — rotates the refresh secret as a NEW chain node. Presenting an already-consumed (rotated/revoked/expired) token proves replay: the WHOLE chain is revoked and the event is audited.

Request body

RefreshRequest

Responses

StatusDescription
200
POST/api/v1/auth/register

POST /api/auth/register — creates the org, its default roles/board/SLA policy, and the first user as Owner. One transaction: a half-created org can never exist.

Answers 503 with `Retry-After` when the process's password-hash slots are saturated (Wave 56 lane `p56-auth-bounds`). The 503 is deliberately NOT in the `utoipa` response list: every route can answer it from the middleware layer (draining, database unreachable), the pinned OpenAPI document is regenerated only by `scripts/regen-openapi-pins.sh` against a live API, and adding one description here would leave both committed pins stale for a status the whole surface already shares.

Request body

RegisterRequest

Responses

StatusDescription
200
409slug or email already exists
422validation error
GET/api/v1/auth/sessions

GET /api/auth/sessions — the caller's live refresh sessions (security settings page).

Responses

StatusDescription
200

Security: bearer

POST/api/v1/auth/sessions/{id}/revoke

POST /api/auth/sessions/{id}/revoke — kills one of the caller's own sessions (sign-out-everywhere-else primitive).

Parameters

NameInTypeRequiredDescription
idpathstringYes

Responses

StatusDescription
204

Security: bearer

POST/api/v1/auth/users/{id}/mfa-reset

POST /api/auth/users/{id}/mfa-reset — clears another user's factors.

This exists because the strict rule needs an exit: a user who lost their only factor cannot step up, and without a reset path the step-up requirement locks them out of their own account changes (the ruling of 2026-10-25 puts the two in one lane for exactly that reason). What makes it safe rather than a takeover tool: - `manage_users` is required, and an administrator cannot reset their own factors — a stolen admin session must not be able to turn its owner's MFA off with one call. - An Owner's or Admin's factors can only be reset by an Owner, so a lesser administrator cannot strip a peer's second factor. - The account is left owing a factor (`users.mfa_reenroll_required`), not merely factor-free: enrolment is the only factor change it may make until one is confirmed, and the flag is audited with the counts. - Every live session is revoked, so the user re-authenticates with their password and learns the factor is gone. - The audit row names the administrator, the target, what was removed and how many sessions died.

Parameters

NameInTypeRequiredDescription
idpathstringYes

Responses

StatusDescription
200
403
404

Security: bearer

GET/api/v1/me

GET /api/me — the authenticated principal with resolved permissions.

Responses

StatusDescription
200

Security: bearer

POST/api/v1/sso/exchange

POST /api/auth/sso/exchange — swaps the single-use callback code for tokens (or a link confirmation). Codes die on first use or after 60s.

Request body

SsoExchangeRequest

Responses

StatusDescription
200
401
PATCH/api/v1/users/me

Wave 26 onboarding leg (b): the per-user first-run "check your profile" step needs a self-editable name/timezone. Self-scoped: PATCH /api/v1/users/me changes ONLY the caller's own display name and timezone (email is immutable here by design), with an audit row in the same transaction.

Request body

PatchMeRequest

Responses

StatusDescription
200
422

Security: bearer