Appearance
Signing in
Signing in with a password and a second factor, through OIDC or SAML, and signing out.
POST /v1/login
Exchange a username and password for a session token.
Returns {token, principal} and sets the session cookie (HttpOnly, SameSite=Lax, and Secure when the request came over HTTPS directly or with X-Forwarded-Proto: https). A principal with mustChangePassword must call POST /v1/me/password before anything else. After 5 failed attempts for a username, or 20 from one address, within 15 minutes, sign-in is refused with 429 and Retry-After. A user without a password (one who signs in through an OIDC provider) is refused as a wrong password is. With auth.passwords off, only admin.username may sign in here.
A user with MFA gets no session for the right password. The answer is {mfa: {ticket, methods, expiresIn}}: methods lists totp, passkey and recovery, as the user has them, and the ticket answers POST /v1/login/mfa within expiresIn seconds (300). A wrong password looks the same for every user. A principal with mfaEnrolmentRequired must set up MFA (/v1/me/mfa) before anything else.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
username | string | yes | |
password | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
200 | Session, or the second step's ticket | |
401 | Wrong username or password, or the user is disabled | |
403 | Passwords are off (passwords-off) and this isn't admin.username | |
429 | Too many failed attempts, or too many sign-in requests from this address (the signin rate limit); Retry-After says how many seconds to wait |
POST /v1/login/mfa
Answer the second step of a password sign-in.
Sends the ticket from POST /v1/login with one answer: totp (a code from the user's app), recoveryCode, or passkey (the browser's PublicKeyCredential for the options of /v1/login/mfa/passkey-options, as JSON). A passkey whose sign counter went backwards is refused, as a wrong answer. A right answer ends the ticket and answers {token, principal} with the session cookie, as POST /v1/login does; a recovery code adds recoveryCodesLeft. A ticket allows 5 wrong answers, then it is dropped. Wrong answers also count as failed sign-ins (429 with Retry-After).
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
ticket | string | yes | |
totp | string | ||
recoveryCode | string | ||
passkey | object |
Responses
| Status | Description | Body |
|---|---|---|
200 | Session | |
400 | No answer was sent | |
401 | The ticket is unknown, used or expired (ticket-expired), or the answer is wrong (wrong-code, with triesLeft) | |
409 | TOTP secrets don't open with this server's key (totp-unavailable); use a recovery code | |
429 | Too many failed attempts, or too many sign-in requests from this address (the signin rate limit); Retry-After says how many seconds to wait |
POST /v1/login/mfa/passkey-options
The WebAuthn request options for a ticket's passkey answer.
Answers {publicKey: PublicKeyCredentialRequestOptions}: a fresh challenge, kept with the ticket, and the user's passkeys. The RP ID is publicURL's host.
Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
ticket | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
200 | The request options, for navigator.credentials.get | |
401 | The ticket is unknown, used or expired (ticket-expired) | |
409 | The user has no passkey, or passkeys are off without publicURL (no-passkeys) | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/logout
End the caller's login session.
Ends the login token (bearer or session cookie) and wipes the viewer sessions opened under it, with their data. API keys aren't ended this way; their viewer sessions last until they are deleted or expire.
Responses
| Status | Description | Body |
|---|---|---|
204 | Logged out | |
401 | Not logged in | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/auth/providers
The OIDC and SAML providers people can sign in with.
Needs no sign-in. kind is oidc or saml, and start is the path a sign-in button navigates to (with ?next=). available is false until the provider's discovery (OIDC) or metadata (SAML) has answered. passwords is false when auth.passwords is off; only admin.username may then sign in with a password.
Responses
| Status | Description | Body |
|---|---|---|
200 | {providers: [{id, name, kind, start, available}], passwords} | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/auth/oidc/{provider}/start
Start signing in through an OIDC provider.
A browser navigates here (a link or location.assign, not a form). The flow (state, nonce and PKCE verifier) is sealed into the rf_oidc cookie, scoped to /v1/auth/oidc/, for 10 minutes, and the browser is sent to the provider. next is a path on this server to return to; anything else, or a path under /v1/auth/, returns to /.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | yes | |
next | query | string | ||
loginHint | query | string | Passed to the provider as login_hint. |
Responses
| Status | Description | Body |
|---|---|---|
302 | To the provider's authorization endpoint | |
303 | The provider isn't available yet; to the sign-in page with signInError=failed | |
404 | No such provider | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/auth/oidc/{provider}/callback
Finish signing in through an OIDC provider.
The provider sends the browser here. The rf_oidc cookie is used once; its state must match, and the ID token must pass every check (docs/plans/oidc.md). A known identity signs its user in; else an invite binds it; else, with jit, a user is made. The user's mapped roles come from the provider's role rules. On success the session cookie is set, as by POST /v1/login, and the browser goes to next. Any failure goes to the sign-in page with signInError set to denied, expired, no-account, disabled, domain or failed. Failures count against the client's address as failed password sign-ins do.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | yes | |
code | query | string | ||
state | query | string | ||
iss | query | string | ||
error | query | string |
Responses
| Status | Description | Body |
|---|---|---|
303 | To next, signed in, or to the sign-in page with signInError | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/auth/saml/{provider}/metadata
reportd's SAML SP metadata for an IdP.
Needs no sign-in. The metadata to import into the IdP: reportd's entity ID (<publicURL>/v1/auth/saml/<id>/metadata unless the provider sets entityID), one assertion consumer service (HTTP-POST) at <publicURL>/v1/auth/saml/<id>/acs, the SP certificate for signing and encryption, whether requests are signed, that assertions must be, and the persistent NameID format.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
200 | The SP metadata | string |
404 | No such provider | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/auth/saml/{provider}/start
Start signing in through a SAML IdP.
A browser navigates here (a link or location.assign, not a form). It makes a signed AuthnRequest (unless signRequests is false), seals the flow (the provider, the request ID, a random RelayState and next) into the rf_saml cookie, scoped to /v1/auth/saml/, for 10 minutes, HttpOnly, Secure and SameSite=None, since the IdP returns with a cross-site POST. The browser goes to the IdP by the HTTP-Redirect binding; an IdP that takes only HTTP-POST gets a page that posts the request. next is as for OIDC.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | yes | |
next | query | string |
Responses
| Status | Description | Body |
|---|---|---|
200 | A page that posts the request to an IdP that takes only HTTP-POST | string |
302 | To the IdP's single sign-on service | |
303 | The IdP's metadata hasn't been read yet; to the sign-in page with signInError=failed | |
404 | No such provider | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
POST /v1/auth/saml/{provider}/acs
Finish signing in through a SAML IdP (assertion consumer service).
The IdP's page posts its response here, cross-site: the one POST the cross-site check lets through, since the signed response and the rf_saml cookie authenticate it. The cookie is used once; its provider and RelayState must match, and the response must answer its request and pass every check (docs/plans/saml.md). At most 512 KiB. Then as the OIDC callback: a known identity signs its user in, an invite binds it (with trustEmail), or, with jit, a user is made, and the session cookie is set. A response with no flow cookie, as from an IdP-initiated sign-in, gets expired. Failures count against the client's address. A flow that links an identity goes on to GET /v1/auth/saml/{provider}/link.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | yes |
Request body, optional (application/x-www-form-urlencoded)
| Field | Type | Required | Description |
|---|---|---|---|
SAMLResponse | string | ||
RelayState | string |
Responses
| Status | Description | Body |
|---|---|---|
303 | To next, signed in, to the link route, or to the sign-in page with signInError | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |
GET /v1/auth/saml/{provider}/link
Finish linking a SAML identity.
The ACS sends the browser here when the flow links an identity (POST /v1/me/identities/{provider}): the identity it verified is sealed in the rf_saml_link cookie for a minute. This GET carries the SameSite=Lax session cookie that the IdP's cross-site POST didn't, so the identity is linked to the user who began, if they are still the one signed in.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
provider | path | string | yes |
Responses
| Status | Description | Body |
|---|---|---|
303 | To next, linked, or to the sign-in page with signInError | |
429 | The request's class is over its rate limit (limits.rate, or the API key's own rateLimits): {error: "rate-limited", class, message, retryAfter}. The MCP tool-call and agent edit limits answer the same body without class. | object |