mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
1865 lines
115 KiB
Plaintext
1865 lines
115 KiB
Plaintext
---
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
title: Authentication resource
|
|
description: Registration, login, credentials, sessions, recovery, SSO, IP authorisation, desktop handoff, origin handoff, and the passkey bridge.
|
|
---
|
|
|
|
import RouteHeader from '@/components/RouteHeader.astro';
|
|
|
|
The Authentication resource covers signing up, signing in, recovering an account, and moving a session to a new device. Every route on this page is below `/v1/auth`.
|
|
|
|
## Shared behaviour
|
|
|
|
[Authentication](/authentication/) and [HTTP authentication](/http-api/#authentication) define credential syntax and the credential namespaces. Changing the credentials of an account that is already signed in is covered by [Email and password changes](/http-api/users/email-and-password/), [Multi-factor authentication](/http-api/users/mfa/), and [Phone verification](/http-api/users/phone-verification/).
|
|
|
|
:::caution[Authentication material is confidential and opaque]
|
|
A client MUST treat every session token, one-use token, ticket, SSO state, and handoff code as confidential. It MUST NOT parse one, expose it to untrusted code, or write it to logs, analytics, or a URL the operation did not assign.
|
|
:::
|
|
|
|
Most operations need no `Authorization` credential, because the request has a one-use token, MFA ticket, WebAuthn assertion, or account credential of its own. [List authentication sessions](#list-authentication-sessions), [terminate authentication sessions](#terminate-authentication-sessions), [resend email verification](#resend-email-verification), [log out](#log-out), [complete desktop handoff](#complete-desktop-handoff), and [create origin handoff](#create-origin-handoff) are the only operations on this page that read an `Authorization` credential. Complete desktop handoff reads the header only when its body omits the `token` field. The first three and create origin handoff require an ordinary user session, and each rejects a bot token and an OAuth2 bearer token with 403 `ACCESS_DENIED`. All four still admit a session whose account is flagged for suspicious activity.
|
|
|
|
Every route has a route bucket and is also subject to the [global HTTP limit](/topics/rate-limits/). A route bucket is keyed by the authenticated user when the request has a resolvable credential and by the client IP address otherwise. Bucket or global denial returns 429 `RATE_LIMITED`.
|
|
|
|
An invalid JSON shape returns 400 `INVALID_FORM_BODY` with [validation error object](/http-api/#validation-error-object) entries in `errors`. An unexpected failure returns 500 `INTERNAL_SERVER_ERROR`. Every enumerated code on this page is registered in [Errors](/http-api/errors/), and every snowflake field is the decimal string form defined by [Snowflakes](/snowflakes/).
|
|
|
|
When SSO is both enabled and enforced, every local authentication operation returns 403 `SSO_REQUIRED`. The SSO status route, SSO start and completion, logout, both session routes, every handoff route, and the passkey bridge options, complete, and cancel routes stay available under enforcement. Every other route on this page is a local authentication operation.
|
|
|
|
:::caution[SSO enforcement precedes every other check]
|
|
An enforced instance returns 403 `SSO_REQUIRED` even for a malformed or unauthenticated local authentication request.
|
|
:::
|
|
|
|
A successful sign-in creates one authentication session and issues its token. Fluxer sets no ceiling on live sessions and evicts none when a further session starts, so an account holds one session per sign-in until it revokes them through [terminate authentication sessions](#terminate-authentication-sessions).
|
|
|
|
Revoking a session, whether the account revoked it or an administrator terminated it, stops its token authenticating requests. Its main Gateway connection receives [Invalid Session](/gateway/overview/#invalid-session) with `d: false` and stays open, unauthenticated. No Gateway Dispatch is emitted for the revocation itself, so a client learns of it from that frame or from the next request that fails to authenticate.
|
|
|
|
<a id="authentication-token-response"></a>
|
|
|
|
## Authentication token response object
|
|
|
|
A newly issued session token and the account it belongs to. Password login without a second factor, MFA completion, discoverable WebAuthn authentication, immediate registration, password reset, email reversion, SSO completion, IP authorisation polling, and handoff polling all return these fields.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| token<sup>1</sup> | string | The newly issued user session token |
|
|
| user_id | snowflake | The authenticated user ID |
|
|
| user | [partial user](/http-api/users/#partial-user-object) object | The public representation of the authenticated account |
|
|
|
|
<sup>1</sup> The token is the literal prefix `flx_` followed by exactly 36 base62 characters, and this response is the only way to read it
|
|
|
|
:::caution[A session token is returned once]
|
|
A lost session token cannot be recovered through any operation. The account MUST authenticate again to obtain a new session.
|
|
:::
|
|
|
|
:::note[The successful result has no discriminator]
|
|
Password login, registration, and password reset each return either this object or an alternative shape. The successful shape has no `mfa` or `registration_pending_approval` member of its own, so a client MUST branch on the presence of `token`.
|
|
:::
|
|
|
|
## SSO status object
|
|
|
|
The public single sign-on state. The same object is embedded by the [instance discovery document](/http-api/instance/#instance-discovery-object).
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| enabled<sup>1</sup> | boolean | Whether SSO can be started on this instance |
|
|
| enforced<sup>2</sup> | boolean | Whether SSO is required for every user |
|
|
| display_name | ?string | The configured provider display name, or null when none is set |
|
|
| redirect_uri | string | The default OAuth2 redirect URI used for the provider callback |
|
|
|
|
<sup>1</sup> The value is true only when the operator has enabled SSO and the provider has an authorisation URL, a token URL, a client ID, and a JWKS URL or user info URL. Each URL is configured or discovered from the issuer. A provider that lacks any of them reports false
|
|
|
|
<sup>2</sup> The value is true only when `enabled` is also true, and every local authentication operation then returns 403 `SSO_REQUIRED`
|
|
|
|
## SSO start object
|
|
|
|
The parameters for sending the user to the identity provider, bound to one new SSO state.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| authorization_url | string | The provider authorisation URL with the state, nonce, and PKCE challenge |
|
|
| state<sup>1</sup> | string | The one-use CSRF state |
|
|
| redirect_uri | string | The callback URI bound to this state |
|
|
|
|
<sup>1</sup> The first [complete SSO](#complete-sso) request that presents the unexpired state consumes it, whether or not that request then succeeds
|
|
|
|
A client MUST return the state unchanged and MUST NOT interpret its contents.
|
|
|
|
## SSO completion response object
|
|
|
|
SSO completion always issues a session. It extends the [authentication token response](#authentication-token-response) with the redirect bound to the consumed state.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| token | string | The newly issued user session token |
|
|
| user_id | snowflake | The authenticated user ID |
|
|
| user | [partial user](/http-api/users/#partial-user-object) object | The public representation of the authenticated account |
|
|
| redirect_to<sup>1</sup> | string | The sanitised redirect that was bound to the consumed SSO state |
|
|
|
|
<sup>1</sup> The value is the empty string when [start SSO](#start-sso) received no `redirect_to` or when the supplied value did not survive sanitisation
|
|
|
|
:::note[SSO completion never issues an MFA challenge]
|
|
Completion does not check the account's second factor and cannot return an [MFA challenge response](#mfa-challenge-response). A client MUST NOT branch on an `mfa` member here.
|
|
:::
|
|
|
|
## Registration pending approval response object
|
|
|
|
The ID of a registration that an administrator has yet to approve. Registration returns this object when the account enters approval.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| registration_pending_approval | boolean | Always `true` |
|
|
| user_id | snowflake | The registered account ID awaiting approval |
|
|
|
|
## Partial user object
|
|
|
|
Every authenticated result on this page embeds the public account representation. The [partial user object](/http-api/users/#partial-user-object) on the Users resource defines it, together with its [reply mention preferences](/http-api/users/#reply-mention-preferences).
|
|
|
|
<a id="mfa-challenge-response"></a>
|
|
|
|
## MFA challenge response object
|
|
|
|
The ticket and the method list a client needs to finish a login with a second factor. Password login returns this object when the account has one.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| mfa | boolean | Always `true` |
|
|
| ticket<sup>1</sup> | string | The ticket consumed by a TOTP or WebAuthn MFA completion |
|
|
| allowed_methods<sup>2</sup> | array[string] | The methods available to this account, drawn from `totp`, `webauthn`, and `backup_codes` (max 10 items) |
|
|
| totp | boolean | Whether the account holds the time-based one-time password authenticator type |
|
|
| webauthn<sup>3</sup> | boolean | Whether passkeys are enabled as a second factor |
|
|
| backup_codes | boolean | Whether the account holds at least one unconsumed backup code |
|
|
|
|
<sup>1</sup> The ticket is retained for five minutes, is destroyed after five failed code attempts, and is consumed by the completion that issues the session
|
|
|
|
<sup>2</sup> The array lists the account's methods in the fixed order `totp`, `webauthn`, then `backup_codes`, and it omits any value the account cannot use
|
|
|
|
<sup>3</sup> The account holds the WebAuthn [authenticator type](/http-api/users/#authenticator-types), which it takes from [Set WebAuthn two-factor authentication](/http-api/users/mfa/#set-webauthn-two-factor-authentication). A registered credential alone does not set it, except on an account with no password credential, where the passkey is the primary credential and always counts
|
|
|
|
:::note[Backup codes have no completion route of their own]
|
|
[Complete login with TOTP](#complete-login-with-totp) reads a backup code from `code`, which is what `backup_codes` in `allowed_methods` points a client at. An account whose only second factor is passkeys can present one there, so a client offers the code input alongside the passkey prompt.
|
|
:::
|
|
|
|
<a id="webauthn-authentication-options"></a>
|
|
|
|
## WebAuthn authentication options object
|
|
|
|
Both WebAuthn option operations return a PublicKeyCredential request options object that a browser passes straight to its credential request. The [WebAuthn authentication options object](/http-api/users/mfa/#webauthn-authentication-options-object) on the Multi-factor authentication resource defines its fields, together with its [WebAuthn credential descriptor](/http-api/users/mfa/#webauthn-credential-descriptor-object) and [WebAuthn client extension inputs](/http-api/users/mfa/#webauthn-client-extension-inputs-object) objects.
|
|
|
|
A challenge issued for MFA completion is also bound to its ticket and account, so it cannot be replayed against discoverable login. The MFA route lists one group of the resolved account's credentials in `allowCredentials`, and the discoverable route omits the member, because discovery happens at the authenticator. [Passkey domain selection](/http-api/users/mfa/#passkey-domain-selection) decides the group and `rpId` on both routes.
|
|
|
|
<a id="webauthn-assertion"></a>
|
|
|
|
## WebAuthn assertion object
|
|
|
|
A WebAuthn authentication request has the browser credential result as `response` and the original server challenge as `challenge`. Fluxer accepts additional WebAuthn fields. The [WebAuthn assertion object](/http-api/users/mfa/#webauthn-assertion-object) on the Multi-factor authentication resource defines the fields, together with its [WebAuthn assertion response](/http-api/users/mfa/#webauthn-assertion-response-object) and [WebAuthn client extension results](/http-api/users/mfa/#webauthn-client-extension-results-object) objects.
|
|
|
|
## Authentication session object
|
|
|
|
One live session belonging to the authenticated account. The listing is ordered by approximate last activity, newest first.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| id_hash<sup>1</sup> | string | The base64url SHA-256 digest of the session token |
|
|
| client_info? | ?[client info](#client-info-object) object | The parsed client metadata recorded when the session was created |
|
|
| masked_ip | ?string | The semi-redacted IP address recorded for the session |
|
|
| approx_last_used_at? | ?ISO8601 timestamp | The approximate time of the last request that used this session |
|
|
| current<sup>2</sup> | boolean | Whether this session supplied the credential for the current request |
|
|
|
|
<sup>1</sup> This digest is the only session identifier the API exposes, and it is the exact value accepted by [terminate authentication sessions](#terminate-authentication-sessions)
|
|
|
|
<sup>2</sup> Exactly one entry is true, the session whose token authenticated the request
|
|
|
|
A client that needs to identify its own session reads the entry whose `current` is true.
|
|
|
|
## Client info object
|
|
|
|
The parsed device metadata recorded for a session or a pending handoff.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| platform? | ?string | The recorded client platform, resolved from the `User-Agent` |
|
|
| os? | ?string | The recorded operating system |
|
|
| browser?<sup>1</sup> | ?string | The recorded browser |
|
|
| device | string | The device class, either `mobile` or `desktop` |
|
|
| location? | ?[client location](#client-location-object) object | The approximate geolocation derived from the recorded IP address |
|
|
|
|
<sup>1</sup> A native or Electron client reports null, as does a session created by an unparseable request. A handoff omits the member entirely
|
|
|
|
Device metadata is inferred from the client's request headers. Treat `platform` as a display label, not a stable application identifier. Location is approximate and can be unavailable. A session then reports null `location`, while a handoff reports an object with null members.
|
|
|
|
## Client location object
|
|
|
|
The approximate geolocation Fluxer derives from the IP address recorded for a session or a handoff.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| city? | ?string | The approximate city |
|
|
| region? | ?string | The approximate region |
|
|
| country? | ?string | The approximate country |
|
|
|
|
## Password reset validity object
|
|
|
|
The result of checking a password reset token without consuming it.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| valid<sup>1</sup> | boolean | Whether the token is valid and unexpired |
|
|
|
|
<sup>1</sup> A password reset token expires one hour after it is issued
|
|
|
|
## IP authorisation poll object
|
|
|
|
The state of one IP authorisation ticket, as observed by the device whose sign-in was held.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| completed | boolean | Whether the authorisation link has been used and a session has been issued |
|
|
| token?<sup>1</sup> | ?string | The newly issued user session token |
|
|
| user_id?<sup>1</sup> | ?snowflake | The authenticated user ID |
|
|
| user?<sup>1</sup> | ?[partial user](/http-api/users/#partial-user-object) object | The public representation of the authenticated account |
|
|
|
|
<sup>1</sup> The fields are present together only when `completed` is true, and a still-pending ticket returns `completed` alone
|
|
|
|
## Username suggestions object
|
|
|
|
Username candidates derived from a display name.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| suggestions<sup>1</sup> | array[string] | The generated username candidates (max 20 items) |
|
|
|
|
<sup>1</sup> The array has at most one candidate. It is empty when the supplied display name derives no permitted username
|
|
|
|
## Handoff initiation object
|
|
|
|
The code that identifies one pending desktop handoff.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| code<sup>1</sup> | string | The handoff code to present to the approving device |
|
|
| expires_at | ISO8601 timestamp | The time at which the handoff expires, five minutes after creation |
|
|
| poll_secret? | string | The secret the initiating device presents to read the issued token |
|
|
|
|
<sup>1</sup> The code is 12 characters drawn from the alphabet `ABCDEFGHJKMNPQRSTUVWXYZ23456789`, rendered as two groups of six separated by a hyphen
|
|
|
|
:::caution[The code alone reads the device metadata]
|
|
Anyone who learns the code reads the pending device metadata. Reading the token and cancelling the handoff both require `poll_secret`. A client MUST show the code only to the person performing the handoff.
|
|
:::
|
|
|
|
## Handoff information object
|
|
|
|
The device metadata shown to the approving device before it transfers a session.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| status | string | The state, either `pending` or `expired` |
|
|
| client_info?<sup>1</sup> | ?[client info](#client-info-object) object | The initiating device metadata |
|
|
|
|
<sup>1</sup> The value is null whenever the status is `expired`
|
|
|
|
## Handoff status object
|
|
|
|
The state of one desktop handoff as observed by the device that initiated it.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| status<sup>1</sup> | string | The state, one of `pending`, `expired`, or `completed` |
|
|
| token?<sup>2</sup> | ?string | The newly issued user session token |
|
|
| user_id?<sup>2</sup> | ?snowflake | The authenticated user ID |
|
|
| user?<sup>2</sup> | ?[partial user](/http-api/users/#partial-user-object) object | The public representation of the authenticated account |
|
|
|
|
<sup>1</sup> An unknown code and an expired code both report `expired`
|
|
|
|
<sup>2</sup> The fields are present together only when the status is `completed`
|
|
|
|
## Origin handoff object
|
|
|
|
The identifier of one stored origin handoff.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| handoff_id | string | The single-use identifier the receiving origin redeems, 32 random bytes as 43 base64url characters |
|
|
|
|
## Origin handoff payload object
|
|
|
|
The client state released by one redeemed origin handoff.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| payload | string | The base64url string stored by [create origin handoff](#create-origin-handoff), returned unchanged |
|
|
|
|
## Passkey bridge
|
|
|
|
The official instance is moving its web client from `https://web.fluxer.app` to `https://fluxer.com`, and its canary client from `https://web.canary.fluxer.app` to `https://canary.fluxer.com`. A passkey works only under the domain it was created for, so a page on a new origin cannot use a passkey created for `fluxer.app`. The passkey bridge runs that passkey somewhere that can, then hands the result back to the new origin.
|
|
|
|
The bridge exists only on the official instance and only for requests from the two new origins. It keeps working while the [domain migration switch](/http-api/instance/#domain-migration-object) is off, so people already on a new origin can still use their passkeys. Every bridge route on a self-hosted instance returns 403 `INVALID_API_ORIGIN`.
|
|
|
|
A ceremony has one of three purposes. `login` signs in with any passkey the person picks, `login_mfa` finishes a password login whose second factor is passkeys, and `sudo` proves [sudo mode](/http-api/users/mfa/#sudo-mode) through [start passkey bridge sudo verification](/http-api/users/mfa/#start-passkey-bridge-sudo-verification). It also has one of two runners. With `page`, the ceremony runs on the paired legacy origin at `/passkey-bridge`. With `native`, the desktop client on the new origin runs it itself.
|
|
|
|
A ceremony runs in four steps.
|
|
|
|
1. The page on the new origin draws a nonce, keeps it, and starts the ceremony with the nonce's SHA-256 digest.
|
|
2. The runner fetches [passkey bridge options](#get-passkey-bridge-options), asks the authenticator, and calls [complete passkey bridge](#complete-passkey-bridge) or [cancel passkey bridge](#cancel-passkey-bridge).
|
|
3. Completing or cancelling issues a completion code. The `page` runner gets it in the fragment of `return_url` and navigates there. The `native` runner gets it in the response.
|
|
4. The page on the new origin redeems the ceremony once with the nonce and the completion code.
|
|
|
|
Redemption needs both secrets. Someone who starts a ceremony and sends another person the bridge link holds the nonce, but the completion code reaches the other person's browser, which holds no nonce.
|
|
|
|
A ceremony lasts 10 minutes from its start, or 5 minutes for `login_mfa`, which is the life of an MFA ticket. It is `pending` until it completes or is cancelled. Only a passkey the account lists for `fluxer.app` completes a ceremony. A [replaced passkey](/http-api/users/mfa/#replaced-passkeys) never does.
|
|
|
|
When the request comes from a new origin and the instance-wide [domain migration switch](/http-api/instance/#domain-migration-object) is on, a completed redemption also opens a [passkey update](/http-api/users/mfa/#passkey-updates) for the session it signs in, or for the session that proves sudo mode.
|
|
|
|
## Passkey bridge start object
|
|
|
|
One started passkey bridge ceremony.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| ceremony_id | string | The ceremony identifier, 32 random bytes as 43 base64url characters |
|
|
| bridge_url<sup>1</sup> | ?string | The page that runs the ceremony |
|
|
|
|
<sup>1</sup> The paired legacy origin, the path `/passkey-bridge`, and the ceremony ID as the fragment. Null for the `native` runner
|
|
|
|
## Passkey bridge options object
|
|
|
|
The WebAuthn request for one pending ceremony.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| options | [WebAuthn authentication options](#webauthn-authentication-options) object | The request options the runner passes to the authenticator |
|
|
|
|
## Passkey bridge finish object
|
|
|
|
What the runner does after a ceremony completes or is cancelled. Exactly one field is non-null.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| return_url<sup>1</sup> | ?string | Where the `page` runner goes next, null for the `native` runner |
|
|
| completion_code<sup>2</sup> | ?string | The code the `native` runner redeems, null for the `page` runner |
|
|
|
|
<sup>1</sup> Always `/passkey-bridge` on the new origin, whatever the purpose. The fragment is `passkey-bridge=` followed by the ceremony ID, a full stop, and the completion code. The page there decides where the code goes next from what the starting tab kept, so the start request cannot choose where the code lands
|
|
|
|
<sup>2</sup> 32 random bytes as 43 base64url characters. Each call issues a new one
|
|
|
|
## Passkey bridge sign-in redemption object
|
|
|
|
The result of one redeemed `login` or `login_mfa` ceremony.
|
|
|
|
### Structure
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| status | string | The ceremony state, either `completed` or `cancelled` |
|
|
| token?<sup>1</sup> | string | The newly issued user session token |
|
|
| user_id?<sup>1</sup> | snowflake | The authenticated user ID |
|
|
| user?<sup>1</sup> | [partial user](/http-api/users/#partial-user-object) object | The public representation of the authenticated account |
|
|
|
|
<sup>1</sup> Present together only when the status is `completed`
|
|
|
|
## Get SSO status
|
|
|
|
<RouteHeader method="GET" path="/v1/auth/sso/status" unauthenticated />
|
|
|
|
Reads the public single sign-on state of the instance. Authentication is not required. Returns an [SSO status](#sso-status-object) object.
|
|
|
|
The operation stays available while SSO is enforced. It shares the `auth:sso:start` bucket, which permits 10 requests per 10 seconds.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [SSO status](#sso-status-object) object | The public SSO state was read |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
## Start SSO
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/sso/start" unauthenticated />
|
|
|
|
Starts a single sign-on flow. Authentication is not required. Returns an [SSO start](#sso-start-object) object. Send the user to `authorization_url` unchanged.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| redirect_to?<sup>1</sup> | ?string | The post-authentication redirect to bind to the state |
|
|
| redirect_uri?<sup>2</sup> | ?string | The callback URI the client wants the result delivered to |
|
|
|
|
<sup>1</sup> Fluxer sanitises the value before binding it to the state and discards a value that does not survive, which the [SSO completion response](#sso-completion-response-object) reports as the empty string. Sanitisation keeps the trimmed value only when it begins with a single `/`, is at most 2,048 characters, and contains no carriage return or line feed
|
|
|
|
<sup>2</sup> The accepted values are the instance default reported as `redirect_uri` by [get SSO status](#get-sso-status) and the mobile callback `fluxer://auth/sso/callback`, and any other value returns the field code `INVALID_URL_FORMAT`. The provider always receives the instance default. For the mobile callback the state starts with `m.`, and the web callback page forwards the provider's query string to `fluxer://auth/sso/callback` unchanged
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [SSO start](#sso-start-object) object | The state was created |
|
|
| 400 | [error response](/http-api/#error-response) | The body is malformed or the callback URI is not an accepted value |
|
|
| 403 | [error response](/http-api/#error-response) | SSO is disabled or its resolved configuration is incomplete, returning `FEATURE_TEMPORARILY_DISABLED` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The returned state can be completed once and expires after ten minutes. Starting the flow changes no account state and emits no Gateway Dispatch.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per 10 seconds, on the `auth:sso:start` bucket.
|
|
|
|
## Complete SSO
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/sso/complete" unauthenticated />
|
|
|
|
Completes a single sign-on flow and signs in or creates the linked account. Authentication is not required. Returns an [SSO completion response](#sso-completion-response-object).
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| code | string | The authorisation code returned by the provider (1-4096 characters) |
|
|
| state<sup>1</sup> | string | The state returned by [start SSO](#start-sso) (1-4096 characters) |
|
|
|
|
<sup>1</sup> The state is consumed on the first call that resolves it, so a repeated request with the same state returns the field code `INVALID_OR_EXPIRED_SSO_STATE`
|
|
|
|
:::note[Identity binding is one-to-one]
|
|
A provider subject is bound to exactly one Fluxer account, and an account accepts exactly one provider subject. A subject already linked to a different account, or a new subject for an account that already holds a different linked subject, returns the field code `SSO_IDENTITY_MISMATCH`.
|
|
:::
|
|
|
|
:::caution[A verified provider email adopts a local account]
|
|
When the subject is not yet linked, Fluxer adopts any existing account holding the verified email address in the provider claim and binds the subject to it, whether or not that account has a password or an authenticator. A provider that asserts an arbitrary verified address therefore takes over the matching local account. An operator MUST enable SSO only for a provider that controls the addresses it asserts.
|
|
:::
|
|
|
|
Fluxer refuses the adoption only when the account already holds a different provider subject.
|
|
|
|
The provider exchange and claim resolution can return the field codes `INVALID_SSO_AUTHORIZATION_CODE`, `INVALID_SSO_TOKEN`, `FAILED_TO_FETCH_SSO_USER_INFO`, `FAILED_TO_PARSE_SSO_USER_INFO`, `SSO_PROVIDER_DID_NOT_RETURN_EMAIL`, `SSO_IDENTITY_MISMATCH`, `SSO_MISCONFIGURED`, `INVALID_EMAIL_ADDRESS`, `EMAIL_DOMAIN_NOT_ALLOWED_FOR_SSO`, or `SSO_UNABLE_TO_ALLOCATE_DISCRIMINATOR`. An unverified provider email uses the field code `INVALID_SSO_TOKEN`.
|
|
|
|
Closed registration returns 403 `REGISTRATION_CLOSED`, an unknown account on an instance with automatic provisioning disabled returns 403 `SSO_REQUIRED`, an account awaiting approval returns 403 `REGISTRATION_PENDING_APPROVAL`, and account suspension returns 403 `ACCOUNT_SUSPENDED_TEMPORARILY` or 403 `ACCOUNT_SUSPENDED_PERMANENTLY`. A provisioned username or display name containing a blocked substring returns 403 `CONTENT_BLOCKED`.
|
|
|
|
A verified provider email that adopts a bot account returns 403 `BOT_USER_AUTH_SESSION_CREATION_DENIED`, and one that adopts a rejected registration returns 403 `REGISTRATION_REJECTED`.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [SSO completion response](#sso-completion-response-object) | SSO completed and a session was issued |
|
|
| 400 | [error response](/http-api/#error-response) | The body, state, provider exchange, claims, or resolved email is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO, registration policy, a bot account, or account suspension rejects completion |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | The identity provider exchange could not be completed |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
A failed completion requires a fresh SSO flow. A newly created account has a verified email, no local password or authenticator, and default settings. SSO registration accepts no invite and joins no guild.
|
|
|
|
Approval mode registration instead returns 403 `REGISTRATION_PENDING_APPROVAL` and creates no session. Otherwise the operation creates one authentication session and clears an expired temporary suspension.
|
|
|
|
### Rate limit
|
|
|
|
15 requests per 10 seconds, on the `auth:sso:complete` bucket.
|
|
|
|
## Register an account
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/register" unauthenticated />
|
|
|
|
Creates an ordinary account. Returns an [authentication token response](#authentication-token-response) when the instance admits the account immediately and a [registration pending approval response](#registration-pending-approval-response-object) when it does not. Emits a [Guild Member Add](/gateway/events/#guild-member-add) Gateway event for each guild the new account joins through an invite or through the instance's single community guild.
|
|
|
|
Registration verifies a [CAPTCHA](/topics/captcha/). It permits 3 attempts per hour for each client IP address and 15 per hour for each client subnet, which is the IPv4 /24 or IPv6 /48 network. A supplied email address permits 3 attempts per 15 minutes of its own. Those allowances are separate from the route bucket, and only a deployment with `dev.relax_registration_rate_limits` set to true disables them.
|
|
|
|
### Request headers
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
|
|
| Accept-Language?<sup>2</sup> | string | The language tag that selects the locale stored on the new account |
|
|
|
|
<sup>1</sup> A missing token returns 400 `CAPTCHA_REQUIRED` and a rejected token returns 400 `INVALID_CAPTCHA`, each with a new challenge. Verification is skipped entirely while the check is off for the instance
|
|
|
|
<sup>2</sup> The parsed locale becomes the account locale and selects the language of the verification email
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| email?<sup>1</sup> | string | The account email address |
|
|
| username?<sup>2</sup> | string | The account username (1-32 characters, letters, digits, and underscores) |
|
|
| global_name? | string | The display name after normalisation (1-32 characters) |
|
|
| password?<sup>3</sup> | string | The account password (8-256 characters) |
|
|
| date_of_birth?<sup>4</sup> | string | The date of birth in exact `YYYY-MM-DD` form |
|
|
| consent?<sup>5</sup> | boolean | Whether the terms of service and privacy policy are accepted (default false) |
|
|
| invite_code? | ?string | The invite accepted immediately after registration (0-256 characters) |
|
|
| registration_url_code?<sup>6</sup> | ?string | The administrator-issued registration URL code (1-256 characters) |
|
|
| theme? | string | The initial theme preference, one of `dark`, `dark_legacy`, `coal`, `light`, or `system` |
|
|
|
|
<sup>1</sup> Omitting the email address creates an unclaimed account, which has no recovery path until it is claimed. An address whose domain has no usable DNS records, or whose top-level domain the instance blocks, returns the field code `INVALID_EMAIL_ADDRESS`, and an address already in use returns `EMAIL_ALREADY_IN_USE`
|
|
|
|
<sup>2</sup> Omitting the username derives one from `global_name` when that value produces a permitted username, and otherwise allocates a generated username, in both cases with a server-allocated discriminator
|
|
|
|
<sup>3</sup> The password is checked against the public breached-password corpus described under [reset a password](#reset-a-password), and a match returns the field code `PASSWORD_IS_TOO_COMMON`
|
|
|
|
<sup>4</sup> The field is required when the instance [collects date of birth](/http-api/instance/#public-registration-fields-object). An absent or blank value, or a value in `YYYY-MM-DD` shape that is not a real calendar date, returns the field code `INVALID_DATE_OF_BIRTH_FORMAT`. A value that is not ten characters in `YYYY-MM-DD` shape fails schema validation with the field code `STRING_LENGTH_EXACT` or `INVALID_FORMAT`
|
|
|
|
<sup>5</sup> A false or absent value returns the field code `MUST_AGREE_TO_TOS_AND_PRIVACY_POLICY` on the official instance and on any instance that publishes a terms or privacy document
|
|
|
|
<sup>6</sup> A code supplied on an instance with administrator registration URLs disabled, and a code that does not resolve, both return 400 `REGISTRATION_URL_INVALID`. A valid code overrides the closed registration mode and replaces the instance approval mode with its own approval setting
|
|
|
|
Fluxer resolves the region from the client IP address, and an age below the minimum for that region returns the field code `MUST_BE_MINIMUM_AGE`. That minimum is 13 years unless the region sets a different minimum, and the applied minimum appears only in the localised message.
|
|
|
|
Registration returns 403 `REGISTRATION_CLOSED` when the instance is closed and no valid registration URL was supplied. A username whose discriminator space is exhausted returns the field code `TOO_MANY_USERS_WITH_THIS_USERNAME`. A username or display name containing a blocked substring returns 403 `CONTENT_BLOCKED`.
|
|
|
|
:::caution[Registration reports an address already in use]
|
|
[Log in with a password](#log-in-with-a-password) and [request password recovery](#request-password-recovery) are enumeration-safe and registration is not. An address that already holds an account returns the field code `EMAIL_ALREADY_IN_USE`, so an unauthenticated caller learns from the response whether that address is registered.
|
|
:::
|
|
|
|
:::caution[Registration is not idempotent]
|
|
There is no retry key. A repeated request with the same values creates a second account, or fails on the duplicate email address when one was supplied. A client that loses the response MUST resolve the outcome by attempting a login.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [authentication token response](#authentication-token-response) \| [registration pending approval response](#registration-pending-approval-response-object) | Registration completed or entered the approval state |
|
|
| 400 | [error response](/http-api/#error-response) | The body, CAPTCHA, account fields, or registration URL code is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement, registration policy, or content moderation rejects the registration |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route, global, email, IP, or subnet bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The operation applies the instance's registration, email-domain, breached-password, and regional policies before creating the account. It records the accepted terms and privacy policy, authorises the registering client IP address, and sends an email verification message when the instance sends email. An instance that sends no email marks the address verified at creation instead.
|
|
|
|
Registration accepts the `invite_code` from the body, or the instance's configured auto-join invite when the body has none. On an instance with single-community mode enabled, the account also joins the community guild. Each join emits [Guild Member Add](/gateway/events/#guild-member-add) to the affected guild's sessions.
|
|
|
|
Approval mode registration creates no guild membership and no authentication session, and the account cannot sign in until an administrator approves it. Every other successful registration creates one session and returns its token.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per 10 seconds, on the `auth:register` bucket.
|
|
|
|
## Log in with a password
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/login" unauthenticated />
|
|
|
|
Validates local email and password credentials. Authentication is not required. Returns an [authentication token response](#authentication-token-response) when no second factor and no IP approval are outstanding, and an [MFA challenge response](#mfa-challenge-response) when the account has a second factor.
|
|
|
|
Login verifies a [CAPTCHA](/topics/captcha/). It also permits 10 attempts per client IP address in 30 minutes, keyed by the exact IPv4 address or the IPv6 /64 network, and 5 attempts per email address in 15 minutes. Every admitted attempt consumes both allowances, whether or not the credentials turn out to be correct, and a successful login clears neither.
|
|
|
|
### Request headers
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
|
|
| X-Fluxer-Client-Properties?<sup>2</sup> | string | The base64-encoded JSON with the reporting client's `os` |
|
|
| User-Agent?<sup>2</sup> | string | The client string recorded on the created session and shown by [list authentication sessions](#list-authentication-sessions) |
|
|
|
|
<sup>1</sup> A missing token returns 400 `CAPTCHA_REQUIRED` and a rejected token returns 400 `INVALID_CAPTCHA`, each with a new challenge. Verification is skipped entirely while the check is off for the instance
|
|
|
|
<sup>2</sup> Both values feed the [client info](#client-info-object) object recorded on the session, and `X-Fluxer-Client-Properties` is read only for a native Fluxer `User-Agent`
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| email | string | The account email address |
|
|
| password | string | The account password (8-256 characters) |
|
|
| invite_code?<sup>1</sup> | ?string | The invite accepted immediately after a successful login (0-256 characters) |
|
|
|
|
<sup>1</sup> The invite is accepted only on a login that completes without a second factor, and a failure to accept it does not fail the login. A login held for IP authorisation discards the invite
|
|
|
|
:::note[Unknown accounts and unknown passwords are indistinguishable]
|
|
An unknown email address and an incorrect password both return the same paired field codes `INVALID_EMAIL_OR_PASSWORD` on `email` and `password`.
|
|
:::
|
|
|
|
A new client IP address on an account that has neither a second factor nor the app store reviewer flag returns 403 `IP_AUTHORIZATION_REQUIRED`. That error body has `ip_authorization_required` set to true, the `ticket` used by the IP authorisation operations, the account `email`, and `resend_available_in` set to 30 seconds.
|
|
|
|
When the instance has disabled new-IP authorisation or sends no email, Fluxer authorises the client IP address silently and the login continues. An account that already has a second factor never enters IP authorisation.
|
|
|
|
Account policy can return 403 `REGISTRATION_PENDING_APPROVAL`, 403 `REGISTRATION_REJECTED`, 403 `ACCOUNT_SUSPENDED_TEMPORARILY`, or 403 `ACCOUNT_SUSPENDED_PERMANENTLY`. A bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`.
|
|
|
|
:::note[A verified password reactivates the account]
|
|
On an account the user had disabled, a correct password clears the disabled state. It also cancels a self-scheduled deletion if erasure has not started. Both happen before any second factor is requested, without a separate confirmation step. Once erasure starts, login returns `ACCOUNT_SUSPENDED_PERMANENTLY`.
|
|
:::
|
|
|
|
A correct password also clears an expired temporary suspension before any second factor is requested. A live temporary or permanent administrator suspension stays in place, and the login returns its 403.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [authentication token response](#authentication-token-response) \| [MFA challenge response](#mfa-challenge-response) | The credentials are accepted |
|
|
| 400 | [error response](/http-api/#error-response) | The body, CAPTCHA, email, or password is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | Account policy, SSO enforcement, or IP authorisation prevents the login |
|
|
| 409 | [error response](/http-api/#error-response) | `CONFLICT`, because the deletion state changed while cancelling a self-scheduled deletion |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route, global, email, or IP bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
A successful login on an account with a second factor returns an MFA ticket valid for five minutes and issues no session. When IP approval is required, the authorisation ticket remains valid for 15 minutes and the account receives an authorisation message.
|
|
|
|
A successful login with no outstanding MFA or IP approval creates one authentication session. It accepts the supplied invite first, which emits [Guild Member Add](/gateway/events/#guild-member-add) to the guild's sessions.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per 10 seconds, on the `auth:login` bucket.
|
|
|
|
## Complete login with TOTP
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/login/mfa/totp" unauthenticated />
|
|
|
|
Consumes an MFA ticket and validates a time-based one-time password or an unconsumed backup code, then creates the session. Authentication is not required. Returns an [authentication token response](#authentication-token-response).
|
|
|
|
MFA verification also permits 10 failed attempts per account in 15 minutes and destroys the ticket after 5 failed attempts.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| code<sup>1</sup> | string | The authenticator code or an unconsumed backup code |
|
|
| ticket | string | The ticket returned by [password login](#log-in-with-a-password) |
|
|
|
|
<sup>1</sup> A validated authenticator code is claimed for 30 seconds, so the same code cannot be presented twice. A backup code is consumed permanently on the attempt that accepts it
|
|
|
|
An expired or unknown ticket returns the field code `SESSION_TIMEOUT` on `ticket`. An account with no TOTP enrolment reads `code` as a backup code, and it returns the field code `TOTP_NOT_ENABLED` on `code` only when it holds no unconsumed backup code either. Both an incorrect code and any code presented after the per-account or per-ticket attempt allowance is exhausted return the field code `INVALID_CODE` on `code`. A ticket that resolves to a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [authentication token response](#authentication-token-response) | The ticket and code are accepted |
|
|
| 400 | [error response](/http-api/#error-response) | The body, ticket, or code is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation or the ticket resolves to a bot account |
|
|
| 404 | [error response](/http-api/#error-response) | The ticket resolves to an account that no longer exists |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Success consumes the ticket, clears the per-account and per-ticket failed-attempt counters, and creates one authentication session. A failure consumes one attempt from each counter. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
5 requests per minute, on the shared `auth:login:mfa` bucket.
|
|
|
|
## Get WebAuthn MFA options
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/login/mfa/webauthn/authentication-options" unauthenticated />
|
|
|
|
Resolves the account from an MFA ticket and creates a challenge restricted to that account's registered credentials. Authentication is not required. Returns a [WebAuthn authentication options](#webauthn-authentication-options) object.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| ticket<sup>1</sup> | string | The ticket returned by [password login](#log-in-with-a-password) (1-256 characters) |
|
|
|
|
<sup>1</sup> The ticket is read but not consumed
|
|
|
|
A client MUST still complete the returned challenge through [complete login with WebAuthn MFA](#complete-login-with-webauthn-mfa) before the ticket expires. An expired or unknown ticket returns the field code `SESSION_TIMEOUT` on `ticket`. An account with no credential in the group [passkey domain selection](/http-api/users/mfa/#passkey-domain-selection) picks returns 400 `NO_PASSKEYS_REGISTERED`.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [WebAuthn authentication options](#webauthn-authentication-options) object | The ticket resolves to an account with at least one credential |
|
|
| 400 | [error response](/http-api/#error-response) | The body or ticket is invalid, or the account has no registered credential |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The operation issues a one-use WebAuthn challenge valid for five minutes and bound to this MFA flow, account, and ticket. It changes no account state. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
5 requests per minute, on the shared `auth:login:mfa` bucket.
|
|
|
|
## Complete login with WebAuthn MFA
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/login/mfa/webauthn" unauthenticated />
|
|
|
|
Consumes the MFA ticket, verifies the WebAuthn assertion against the challenge, and creates the session. Authentication is not required. Returns an [authentication token response](#authentication-token-response).
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| response | [WebAuthn assertion](#webauthn-assertion) object | The browser credential assertion |
|
|
| challenge<sup>1</sup> | string | The challenge returned by [get WebAuthn MFA options](#get-webauthn-mfa-options) |
|
|
| ticket | string | The ticket returned by [password login](#log-in-with-a-password) |
|
|
|
|
<sup>1</sup> The challenge is consumed before verification and is accepted only when its bound context, account, and ticket all match this request
|
|
|
|
An expired or unknown ticket returns the field code `SESSION_TIMEOUT` on `ticket`. A ticket for an account that does not count passkeys as its second factor returns 400 `TWO_FACTOR_REQUIRED`, which is what a TOTP account holding registered credentials without [Set WebAuthn two-factor authentication](/http-api/users/mfa/#set-webauthn-two-factor-authentication) receives. A challenge mismatch, an unknown credential, a credential the options did not list, a stored public key that cannot be decoded, a signature counter that the authenticator did not advance, and a failed signature verification all return 401 `PASSKEY_AUTHENTICATION_FAILED`. A verified assertion whose reported signature counter cannot be read returns 500 `INVALID_WEBAUTHN_AUTHENTICATION_COUNTER`.
|
|
|
|
This operation shares the MFA attempt allowances of [complete login with TOTP](#complete-login-with-totp). It permits 10 failed attempts per account in 15 minutes and destroys the ticket after 5 failed attempts. An exhausted allowance returns the field code `INVALID_CODE` on `ticket`.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [authentication token response](#authentication-token-response) | The ticket and assertion are accepted |
|
|
| 400 | [error response](/http-api/#error-response) | The body or ticket is invalid, or passkeys are not the account's second factor (`TWO_FACTOR_REQUIRED`) |
|
|
| 401 | [error response](/http-api/#error-response) | Challenge or assertion verification fails |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation or the ticket resolves to a bot account |
|
|
| 404 | [error response](/http-api/#error-response) | The ticket resolves to an account that no longer exists |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | The verified assertion reported no readable signature counter, or the request could not be completed |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Success consumes the challenge and ticket and creates one authentication session. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
5 requests per minute, on the shared `auth:login:mfa` bucket.
|
|
|
|
## Get discoverable WebAuthn options
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/webauthn/authentication-options" unauthenticated />
|
|
|
|
Creates a challenge for passwordless authentication with a discoverable credential. Authentication is not required. Returns a [WebAuthn authentication options](#webauthn-authentication-options) object.
|
|
|
|
The request has no body. The response omits `allowCredentials` and requests `required` user verification. Its `rpId` follows [passkey domain selection](/http-api/users/mfa/#passkey-domain-selection).
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [WebAuthn authentication options](#webauthn-authentication-options) object | A discoverable challenge was created |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The operation issues a one-use discoverable WebAuthn challenge valid for five minutes and bound to the discoverable context alone. It changes no account state. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
20 requests per 10 seconds, on the `auth:webauthn:options` bucket.
|
|
|
|
## Authenticate with WebAuthn
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/webauthn/authenticate" unauthenticated />
|
|
|
|
Resolves the account from the presented credential ID, verifies the assertion with user verification required, applies account suspension policy, and creates the session. Authentication is not required. Returns an [authentication token response](#authentication-token-response).
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| response | [WebAuthn assertion](#webauthn-assertion) object | The browser credential assertion |
|
|
| challenge<sup>1</sup> | string | The challenge returned by [get discoverable WebAuthn options](#get-discoverable-webauthn-options) |
|
|
|
|
<sup>1</sup> The challenge is consumed before verification and is accepted only when it was issued for the discoverable context, so a challenge issued for MFA or for sudo mode cannot be used here
|
|
|
|
An unknown credential ID, a challenge mismatch, a credential for another domain than the challenge, a stored public key that cannot be decoded, a signature counter that the authenticator did not advance, and a failed signature verification all return 401 `PASSKEY_AUTHENTICATION_FAILED`. A verified assertion whose reported signature counter cannot be read returns 500 `INVALID_WEBAUTHN_AUTHENTICATION_COUNTER`. Account suspension returns 403 `ACCOUNT_SUSPENDED_TEMPORARILY` or 403 `ACCOUNT_SUSPENDED_PERMANENTLY`.
|
|
|
|
A bot account returns 403 `BOT_USER_AUTH_SESSION_CREATION_DENIED`. An account that has not been admitted returns 403 `REGISTRATION_PENDING_APPROVAL` or 403 `REGISTRATION_REJECTED`.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [authentication token response](#authentication-token-response) | The assertion and account policy are accepted |
|
|
| 400 | [error response](/http-api/#error-response) | The body is malformed |
|
|
| 401 | [error response](/http-api/#error-response) | Challenge or assertion verification fails |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement, account suspension, a bot account, or an unadmitted registration rejects the login |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | The verified assertion reported no readable signature counter, or the request could not be completed |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Success consumes the challenge, clears an expired temporary suspension, and creates one authentication session. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per 10 seconds, on the `auth:webauthn:authenticate` bucket.
|
|
|
|
## Log out
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/logout" bot />
|
|
|
|
Revokes the session identified by the user session token in the `Authorization` header. Returns 204 with no body.
|
|
|
|
The request has no body. The operation requires a credential that resolves to an account, and it admits one flagged for suspicious activity. An absent, malformed, or unknown token returns 401 `UNAUTHORIZED`. An OAuth2 bearer token returns 403 `ACCESS_DENIED`. A bot holds no session, so a bot token is accepted and revokes nothing.
|
|
|
|
:::note[Logging out revokes exactly the calling session]
|
|
The account's other sessions, its OAuth2 grants, and any outstanding MFA ticket, IP authorisation ticket, or desktop handoff all survive. Use [terminate authentication sessions](#terminate-authentication-sessions) to revoke other sessions.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 204 | empty | The session was revoked, or the credential is a bot token |
|
|
| 401 | [error response](/http-api/#error-response) | The token is absent, malformed, or names no live session |
|
|
| 403 | [error response](/http-api/#error-response) | The credential is an OAuth2 bearer token |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Session revocation fails unexpectedly |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Revoking this session ends its Gateway session as [shared behaviour](#shared-behaviour) states. No Gateway Dispatch is emitted for the revocation.
|
|
|
|
### Rate limit
|
|
|
|
20 requests per 10 seconds, on the `auth:logout` bucket.
|
|
|
|
## Verify an email address
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/verify" unauthenticated />
|
|
|
|
Consumes an email verification token and marks the account's current address verified. Authentication is not required. Returns 204 with no body.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| token | string | The token delivered by email, exactly 64 characters |
|
|
|
|
An unknown, already consumed, or deleted-account token returns the field code `INVALID_OR_EXPIRED_VERIFICATION_TOKEN`.
|
|
|
|
:::caution[Verification marks the account's current address]
|
|
Fluxer never compares the account's address against the one the token was issued for. An account that changes its address while a verification token is outstanding can therefore verify the new address with the old token.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 204 | empty | The address was verified |
|
|
| 400 | [error response](/http-api/#error-response) | The body or token is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation, or the token resolves to a bot account |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The operation marks the current address verified, clears its bounced state, and clears every suspicious activity flag that verifying or reverifying an email address satisfies. It emits a [User Update](/gateway/events/#user-update) Gateway Dispatch to the account's own sessions.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:verify` bucket.
|
|
|
|
## Resend email verification
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/verify/resend" />
|
|
|
|
Issues and sends a new email verification token for the authenticated account. Requires a user session token for an ordinary user. Returns 204 with no body.
|
|
|
|
The request has no body. When the current address is already verified and no reverification suspicious activity flag is set, the operation returns 204 and sends nothing.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 204 | empty | A message was sent or verification was already complete |
|
|
| 401 | [error response](/http-api/#error-response) | The user session credential is missing or invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation, or the credential is a bot token or an OAuth2 bearer token |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route, global, or per-address bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The operation creates one 64-character verification token bound to the account and its current address, valid for 24 hours, and sends the verification message. Previously issued verification tokens remain valid. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:verify:resend` bucket, and the target address permits 3 messages in 15 minutes.
|
|
|
|
## Request password recovery
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/forgot" unauthenticated />
|
|
|
|
Accepts a password recovery request for an email address. Authentication is not required. Returns 204 with no body.
|
|
|
|
Password recovery verifies a [CAPTCHA](/topics/captcha/). Fluxer consumes both the client IP address and email address allowances before it validates the address, and exhausting either returns 429.
|
|
|
|
### Request headers
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
|
|
|
|
<sup>1</sup> A missing token returns 400 `CAPTCHA_REQUIRED` and a rejected token returns 400 `INVALID_CAPTCHA`, each with a new challenge. Verification is skipped entirely while the check is off for the instance
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| email<sup>1</sup> | string | The address that receives the reset link |
|
|
|
|
<sup>1</sup> An address whose domain has no usable DNS records returns the field code `INVALID_EMAIL_ADDRESS`, while an address that passes DNS validation but belongs to no account returns the ordinary success response
|
|
|
|
:::note[Account existence is not disclosed]
|
|
An address that resolves to no account produces the same 204 response as an address that resolves to an ordinary account. Only a DNS validation failure, the route bucket, the client IP address and email address allowances, and an address belonging to a bot account can produce a different outcome.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 204 | empty | The request was accepted regardless of whether the account exists |
|
|
| 400 | [error response](/http-api/#error-response) | The body, CAPTCHA, or email address is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation, or the address belongs to a bot account |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route, global, email, or client IP bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Fluxer sends one 64-character reset token by email to an address that resolves to an account. The token is bound to that account and its current address, and it is valid for one hour. Nothing else changes. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
5 requests per minute, on the `auth:forgot` bucket, and recovery permits 20 attempts per client IP address and 5 attempts per email address in each 30-minute window.
|
|
|
|
## Validate a password reset token
|
|
|
|
<RouteHeader method="GET" path="/v1/auth/reset/{token}" unauthenticated />
|
|
|
|
Checks a reset token without consuming it. Authentication is not required. Returns a [password reset validity](#password-reset-validity-object) object.
|
|
|
|
### Path parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| token | string | The password reset token, exactly 64 characters |
|
|
|
|
A well-formed token that is unknown, already consumed, or bound to a deleted account returns the same object with `valid` set to false.
|
|
|
|
:::caution[Validity does not promise the reset succeeds]
|
|
The check resolves the token and confirms that its account exists and is not deleted. The reset itself still refuses a valid token for a bot account, for an account under a live temporary suspension, or for a replacement password in the breached-password corpus.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [password reset validity](#password-reset-validity-object) object | The well-formed token was checked |
|
|
| 400 | [error response](/http-api/#error-response) | The token path parameter fails validation |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
This read does not consume the token or change account state. No Gateway Dispatch is emitted.
|
|
|
|
:::caution[The token travels in the path]
|
|
A token in the path reaches proxy logs, browser history, and referrer headers. A client SHOULD read the token from the recovery link and send it in this one request, and SHOULD NOT route the user's browser to the API path directly.
|
|
:::
|
|
|
|
### Rate limit
|
|
|
|
20 requests per minute, on the `auth:reset:validate` bucket.
|
|
|
|
## Reset a password
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/reset" unauthenticated />
|
|
|
|
Consumes a valid reset token and replaces the account password, then issues a new session or an MFA challenge. Authentication is not required. Returns an [authentication token response](#authentication-token-response) when the account has no second factor, and an [MFA challenge response](#mfa-challenge-response) when it has one.
|
|
|
|
Fluxer reads the second factor from the account as it stood before the reset. An account that held no password and holds at least one registered WebAuthn credential counts as having one, because the passkey was its primary credential, so it receives an MFA challenge with `webauthn` in `allowed_methods` and MUST prove the passkey before the reset yields a session.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| token | string | The password reset token, exactly 64 characters |
|
|
| password<sup>1</sup> | string | The replacement password (8-256 characters) |
|
|
|
|
<sup>1</sup> The password is checked against the public breached-password corpus before the token is spent, and a match returns the field code `PASSWORD_IS_TOO_COMMON`
|
|
|
|
An unknown or already consumed token, and a token bound to a deleted account, return the field code `INVALID_OR_EXPIRED_RESET_TOKEN`. A live temporary suspension returns 403 `ACCOUNT_SUSPENDED_TEMPORARILY`. A permanent suspension returns the field code `INVALID_OR_EXPIRED_RESET_TOKEN` and never 403 `ACCOUNT_SUSPENDED_PERMANENTLY`. A bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`.
|
|
|
|
Passwords are checked against a breached-password corpus, as they are during [registration](#register-an-account) and [email reversion](#revert-an-email-change).
|
|
|
|
:::caution[Resetting a password revokes every existing session]
|
|
A successful reset terminates every authentication session on the account, including the one any other device is holding. It then issues one fresh session. An account with a second factor receives an MFA ticket, and completing MFA with that ticket creates the session. Setting a password on an account that had none does not release it from its passkey, so that account still completes MFA.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [authentication token response](#authentication-token-response) \| [MFA challenge response](#mfa-challenge-response) | The password was replaced |
|
|
| 400 | [error response](/http-api/#error-response) | The body, token, or replacement password is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement, a bot account, or a live temporary suspension rejects the mutation |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The operation replaces the password, records the change time, clears an expired temporary suspension, terminates every authentication session on the account, and consumes the presented reset token. Every terminated session loses its Gateway session as [shared behaviour](#shared-behaviour) states. Other outstanding reset tokens are not invalidated, so a second recovery link issued earlier still works.
|
|
|
|
An account with no second factor then receives one new session and its token. An account with a second factor receives a five-minute MFA ticket instead, and the MFA completion creates the session. An account that had no password and holds a registered WebAuthn credential is one of those, and its ticket is completed through [complete login with WebAuthn MFA](#complete-login-with-webauthn-mfa), or through [complete login with TOTP](#complete-login-with-totp) when `allowed_methods` lists `backup_codes`.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:reset` bucket.
|
|
|
|
## Revert an email change
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/email-revert" unauthenticated />
|
|
|
|
Consumes the token delivered to the previous email address, restores that address, replaces the password, terminates every authentication session, and clears every second factor. The requesting IP address becomes the only authorised IP address. Returns an [authentication token response](#authentication-token-response). Emits a [User Update](/gateway/events/#user-update) Gateway event.
|
|
|
|
The token is valid for 24 hours after the address change that issued it.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| token | string | The email reversion token, exactly 64 characters |
|
|
| password<sup>1</sup> | string | The replacement password (8-256 characters) |
|
|
|
|
<sup>1</sup> The value becomes the account's new password, and Fluxer checks it against the public breached-password corpus, returning the field code `PASSWORD_IS_TOO_COMMON` on a match
|
|
|
|
:::danger[Reversion destroys every other credential]
|
|
Completing a reversion terminates every authentication session and clears the account's entire second-factor enrolment. The authorised IP address set becomes the requesting IP address alone. Any other device, passkey, or backup code held by whoever made the original email change stops working immediately, and none of it can be restored.
|
|
:::
|
|
|
|
An unknown or already consumed token, and a token bound to an account that no longer exists, return the field code `INVALID_OR_EXPIRED_REVERT_TOKEN`. Account suspension returns 403 `ACCOUNT_SUSPENDED_TEMPORARILY` or 403 `ACCOUNT_SUSPENDED_PERMANENTLY`, and a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`.
|
|
|
|
Fluxer checks the replacement password against the same breached-password corpus described under [reset a password](#reset-a-password) before it spends the token.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [authentication token response](#authentication-token-response) | The address and credential recovery completed |
|
|
| 400 | [error response](/http-api/#error-response) | The body, token, or replacement password is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement, a bot account, or account suspension rejects the mutation |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The previous email address becomes verified and the replacement password becomes the account password. All other sessions, second factors and authorised IP addresses are removed. Only the requesting IP address remains authorised.
|
|
|
|
The account receives [User Update](/gateway/events/#user-update). Existing Gateway sessions end as described under [shared behaviour](#shared-behaviour), and the response returns one new authentication session.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:email_revert` bucket.
|
|
|
|
## List authentication sessions
|
|
|
|
<RouteHeader method="GET" path="/v1/auth/sessions" />
|
|
|
|
Lists every live authentication session belonging to the authenticated account, newest activity first. Requires a user session token for an ordinary user. Returns an array of [authentication session](#authentication-session-object) objects.
|
|
|
|
The request has no body and takes no parameters.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | array[[authentication session](#authentication-session-object) object] | The live sessions were read |
|
|
| 401 | [error response](/http-api/#error-response) | The user session credential is missing or invalid |
|
|
| 403 | [error response](/http-api/#error-response) | The credential is a bot token or an OAuth2 bearer token |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
This read does not change session state. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
40 requests per 10 seconds, on the `auth:sessions` bucket.
|
|
|
|
## Terminate authentication sessions
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/sessions/logout" mfa />
|
|
|
|
Deletes the named authentication sessions. Requires a user session token for an ordinary user, and [sudo mode](/http-api/users/mfa/#sudo-mode). Returns 204 with no body.
|
|
|
|
The caller MUST send a valid sudo token in the `X-Fluxer-Sudo-Mode-JWT` header, or supply a password or MFA proof in the body.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| session_id_hashes<sup>1</sup> | array[string] | The session digests to delete (max 100 entries) |
|
|
| password?<sup>2</sup> | string | The password proof for an account holding no TOTP secret and no registered WebAuthn credential (8-256 characters) |
|
|
| mfa_method?<sup>3</sup> | string | The proof method, either `totp` or `webauthn` |
|
|
| mfa_code? | string | The authenticator code or an unconsumed backup code when the method is `totp` (1-32 characters) |
|
|
| webauthn_response? | [WebAuthn assertion](#webauthn-assertion) object | The assertion when the method is `webauthn` |
|
|
| webauthn_challenge? | string | The challenge bound to the sudo mode assertion |
|
|
|
|
<sup>1</sup> Each value is the base64url `id_hash` from [list authentication sessions](#list-authentication-sessions). Fluxer ignores an unknown identifier, and an empty array deletes nothing and still returns 204
|
|
|
|
<sup>2</sup> The password is accepted only while the account holds neither a TOTP secret nor a registered WebAuthn credential, and a value that does not match returns the field code `INVALID_PASSWORD`
|
|
|
|
<sup>3</sup> The MFA proof is accepted only while the account holds a TOTP secret or a registered WebAuthn credential, which a passkey satisfies whether or not passkeys are enabled as a second factor. Any failure returns the field code `INVALID_MFA_CODE` on `mfa_code`, and a successful proof issues a fresh sudo token
|
|
|
|
A `totp` method reads `mfa_code` as an authenticator code and accepts an unconsumed backup code in its place, and it reads the value as a backup code alone while the account holds no TOTP secret. A `webauthn` method reads `webauthn_response` and `webauthn_challenge` together, and it accepts only a challenge that was issued for the sudo context.
|
|
|
|
:::caution[The caller can delete its own session]
|
|
The operation deletes exactly the sessions identified by the supplied `id_hash` values. A client that wants to keep its own session MUST omit the `id_hash` of the entry whose `current` is true. Revoking the calling credential still returns 204.
|
|
:::
|
|
|
|
Missing or unusable proof returns 403 `SUDO_MODE_REQUIRED`, whose error body has top-level `has_mfa` and `methods` members, and `methods` reports whether `totp`, `webauthn`, and `backup_codes` are available.
|
|
|
|
:::note[An unclaimed account needs no proof]
|
|
An account that has no password, no TOTP secret, and no registered WebAuthn credential satisfies sudo mode with no body proof and no sudo token, and Fluxer issues none. Every other account MUST present a valid sudo token, a password, or an MFA proof.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 204 | empty | Every named session was absent or deleted |
|
|
| 400 | [error response](/http-api/#error-response) | The body or the supplied proof is invalid |
|
|
| 401 | [error response](/http-api/#error-response) | The user session credential is missing or invalid |
|
|
| 403 | [error response](/http-api/#error-response) | The credential is a bot token or an OAuth2 bearer token, or sudo mode is required |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Each named session is deleted and loses its Gateway session as [shared behaviour](#shared-behaviour) states. A fresh MFA proof issues a new sudo token, while an accepted incoming token is echoed unchanged. Fluxer sets whichever token results in the `X-Fluxer-Sudo-Mode-JWT` response header. A password proof, which only an account holding neither a TOTP secret nor a registered WebAuthn credential can give, issues no token, so the header is not set unless the request already had one. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
20 requests per 10 seconds, on the `auth:sessions:logout` bucket.
|
|
|
|
## Authorise an IP address
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/authorize-ip" unauthenticated />
|
|
|
|
Consumes the authorisation token delivered by email, authorises the pending client IP address, and completes the waiting login. Authentication is not required. Returns 204 with no body.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| token | string | The authorisation token delivered by email |
|
|
|
|
An unknown, expired, already consumed, or account-mismatched token returns the field code `INVALID_OR_EXPIRED_AUTHORIZATION_TOKEN`. A token that resolves to an account that no longer exists returns 404 `UNKNOWN_USER`, and one that resolves to a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`. Session creation also rejects an account that has not been admitted or is suspended, with the codes listed under [log in with a password](#log-in-with-a-password).
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 204 | empty | The IP address was authorised and the new session token is readable through poll IP authorisation |
|
|
| 400 | [error response](/http-api/#error-response) | The body or authorisation token is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation or the token resolves to a bot account |
|
|
| 404 | [error response](/http-api/#error-response) | The token resolves to an account that no longer exists |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The operation adds the pending client IP address to the account's authorised set and creates one authentication session. That session takes the IP address, user agent, and reported operating system captured when the login was attempted. It consumes the authorisation token and the ticket for the pending login.
|
|
|
|
The session token is then published against the ticket and remains readable by [poll IP authorisation](#poll-ip-authorisation) for 60 seconds. The caller of this operation receives 204 and no token of its own. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
5 requests per minute, on the `auth:authorize_ip` bucket.
|
|
|
|
## Resend IP authorisation
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/ip-authorization/resend" unauthenticated />
|
|
|
|
Sends the authorisation message for an outstanding IP authorisation ticket again. Authentication is not required. Returns 204 with no body.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| ticket<sup>1</sup> | string | The ticket returned by the `IP_AUTHORIZATION_REQUIRED` login error |
|
|
|
|
<sup>1</sup> The resend reuses the authorisation token already bound to the ticket, so a message delivered by an earlier send remains valid
|
|
|
|
An unknown or expired ticket returns the field code `INVALID_OR_EXPIRED_AUTHORIZATION_TICKET`. A resend less than 30 seconds after the ticket was issued returns 429 `IP_AUTHORIZATION_RESEND_COOLDOWN` with a `Retry-After` header and a top-level `resend_available_in` in seconds. A second resend returns 400 `IP_AUTHORIZATION_RESEND_LIMIT_EXCEEDED`.
|
|
|
|
:::caution[A lost response still spends the resend]
|
|
There is no retry key. A client that loses the response cannot tell whether the message was delivered, and a retry returns 400 `IP_AUTHORIZATION_RESEND_LIMIT_EXCEEDED` once the first request marked the resend used.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 204 | empty | The message was sent again |
|
|
| 400 | [error response](/http-api/#error-response) | The body or ticket is invalid, or the ticket's single resend is already used |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A route or global bucket denies the request, or the resend delay has not elapsed |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Fluxer sends the authorisation message to the address associated with the login attempt and marks the ticket's single resend used whether or not delivery succeeds. No account state changes. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
5 requests per minute, on the `auth:ip_authorization_resend` bucket, and each ticket permits exactly one resend, no earlier than 30 seconds after the ticket was issued.
|
|
|
|
## Poll IP authorisation
|
|
|
|
<RouteHeader method="GET" path="/v1/auth/ip-authorization/poll" unauthenticated />
|
|
|
|
Reads the login result associated with an IP authorisation ticket. Authentication is not required. Returns an [IP authorisation poll](#ip-authorisation-poll-object) object.
|
|
|
|
### Query parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| ticket | string | The ticket returned by the `IP_AUTHORIZATION_REQUIRED` login error |
|
|
|
|
An unknown or expired ticket returns the field code `INVALID_OR_EXPIRED_AUTHORIZATION_TICKET`, which is also the outcome once the 60-second result retention has elapsed.
|
|
|
|
:::note[Reading the completed result does not consume it]
|
|
Repeated polling returns the same token, so anyone holding the ticket within the 60-second retention window can read it. A completed token is a live session credential from the moment it first appears.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [IP authorisation poll](#ip-authorisation-poll-object) object | The ticket was read |
|
|
| 400 | [error response](/http-api/#error-response) | The query is malformed or the ticket is unknown or expired |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation |
|
|
| 404 | [error response](/http-api/#error-response) | The completed result names an account that no longer exists, returning `UNKNOWN_USER` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Rate limit
|
|
|
|
60 requests per minute, on the `auth:ip_authorization_poll` bucket.
|
|
|
|
## Get username suggestions
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/username-suggestions" unauthenticated />
|
|
|
|
Derives username candidates from a display name. Authentication is not required. Returns a [username suggestions](#username-suggestions-object) object.
|
|
|
|
The route shares the `auth:register` bucket, which permits 10 requests per 10 seconds.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| global_name | string | The display name after normalisation (1-32 characters) |
|
|
|
|
:::note[A returned candidate can be taken before use]
|
|
The operation does no availability check and holds nothing. Registration allocates its own discriminator regardless of the suggestion.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [username suggestions](#username-suggestions-object) object | Candidates were generated |
|
|
| 400 | [error response](/http-api/#error-response) | The body or display name is invalid |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
This read reserves no username and changes no account state. No Gateway Dispatch is emitted.
|
|
|
|
## Initiate desktop handoff
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/handoff/initiate" unauthenticated />
|
|
|
|
Creates a pending handoff and returns the code the initiating device shows to an already signed-in device. Authentication is not required. Returns a [handoff initiation](#handoff-initiation-object) object.
|
|
|
|
The request has no body. Fluxer derives the device metadata shown to the approving device from the request itself.
|
|
|
|
### Request headers
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| User-Agent? | string | The client string presented to the approving device and recorded on the session the handoff creates |
|
|
| X-Fluxer-Client-Properties? | string | The base64-encoded JSON with the reporting client's `os`, read only for a native Fluxer `User-Agent` |
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [handoff initiation](#handoff-initiation-object) object | A handoff request was created |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The handoff remains pending for five minutes and records the initiating device's client IP address, user agent, and reported operating system. Initiation creates no session. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:handoff:initiate` bucket.
|
|
|
|
## Get desktop handoff information
|
|
|
|
<RouteHeader method="GET" path="/v1/auth/handoff/{code}/info" unauthenticated />
|
|
|
|
Describes the device that initiated a pending handoff so that the approving device can show it before approving, and marks the code approvable. Authentication is not required. Returns a [handoff information](#handoff-information-object) object.
|
|
|
|
Fluxer counts failed code attempts against the client IP address and blocks it after 5 failures in 15 minutes. A pending result records no failure, while an unknown or expired code records one.
|
|
|
|
### Path parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| code<sup>1</sup> | string | The handoff code |
|
|
|
|
<sup>1</sup> Fluxer normalises the value by removing hyphens and whitespace and upper-casing the rest, and the normalised result is exactly 12 characters from the handoff alphabet
|
|
|
|
:::caution[A successful lookup makes the code completable]
|
|
Reading the handoff information marks the code as inspected but does not approve the transfer. [Complete desktop handoff](#complete-desktop-handoff) rejects a code that has never been inspected. An approving client MUST show the initiating device information and obtain explicit user confirmation before completion.
|
|
:::
|
|
|
|
Anyone who can reach the API and knows the code can mark it inspected. Declining the request in a client discards only what that client is showing, and the inspected state remains until the handoff is completed or the code expires.
|
|
|
|
A code that is not exactly 12 characters from the handoff alphabet after hyphens and whitespace are removed returns 400 `INVALID_HANDOFF_CODE`. Both a code whose three lookups are already spent and a request from a client IP address that has exhausted its failed-attempt allowance return the same error code. An unknown or expired code returns 200 with the `expired` status and records one failed attempt.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [handoff information](#handoff-information-object) object | The pending or expired state was read |
|
|
| 400 | [error response](/http-api/#error-response) | The code is malformed, its lookup budget is spent, or the client IP attempt allowance is exhausted |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
A successful lookup consumes one of the code's three lookups and permits [complete desktop handoff](#complete-desktop-handoff). An expired result counts as a failed attempt. No account state changes or Gateway Dispatch occur.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:handoff:info` bucket, and each code permits at most three successful lookups in total, counted on the code itself.
|
|
|
|
## Complete desktop handoff
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/handoff/complete" />
|
|
|
|
Approves a pending handoff by issuing a new session to the initiating device on the authority of a live one. Requires the approving device's own user session token. Returns 204 with no body.
|
|
|
|
Fluxer reads that token from the `Authorization` header, or from the body `token` field when the body supplies one.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| code<sup>1</sup> | string | The handoff code shown by the initiating device |
|
|
| token?<sup>2</sup> | string | The approving device's own user session token |
|
|
| user_id<sup>3</sup> | snowflake | The account the approving session belongs to |
|
|
|
|
<sup>1</sup> The code uses the same normalisation contract as [get desktop handoff information](#get-desktop-handoff-information), and only a code that operation has already inspected is accepted
|
|
|
|
<sup>2</sup> The body token takes precedence over the `Authorization` header, and a request that supplies neither returns 401 `UNAUTHORIZED`
|
|
|
|
<sup>3</sup> A supplied token that resolves to a different account returns `SESSION_TOKEN_MISMATCH`
|
|
|
|
:::caution[The approving token may travel in the body]
|
|
Sending the token in `token` puts a live credential into a JSON payload. A client SHOULD use the `Authorization` header and omit `token`.
|
|
:::
|
|
|
|
A token that resolves to no live session returns 401 `INVALID_TOKEN`. A malformed, unknown, completed or uninspected code returns 400 `INVALID_HANDOFF_CODE`, as does an exhausted failed-attempt allowance. An expired code returns `INVALID_HANDOFF_CODE` or `HANDOFF_CODE_EXPIRED`. Start a new handoff in either case.
|
|
|
|
Session creation can also return 403 `BOT_USER_AUTH_SESSION_CREATION_DENIED` for a bot account, and 403 `REGISTRATION_PENDING_APPROVAL` or 403 `REGISTRATION_REJECTED` for an account that has not been admitted. A suspended account returns the suspension codes listed under [log in with a password](#log-in-with-a-password).
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 204 | empty | The handoff was completed |
|
|
| 400 | [error response](/http-api/#error-response) | The body, code, expiry state, or session ownership is invalid |
|
|
| 401 | [error response](/http-api/#error-response) | The approving credential is missing, invalid, or revoked |
|
|
| 403 | [error response](/http-api/#error-response) | The approving session belongs to a bot or to an account awaiting approval or rejected |
|
|
| 404 | [error response](/http-api/#error-response) | The approving session resolves to an account that no longer exists |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Completion creates one new authentication session for the initiating device and publishes its token against the code. The token stays readable through [get desktop handoff status](#get-desktop-handoff-status) for whatever remains of the handoff's five minutes. Completion also discards the pending handoff, so the code cannot be completed a second time and a later [get desktop handoff information](#get-desktop-handoff-information) reports `expired`.
|
|
|
|
The approving session remains valid. The new session takes the initiating device's client IP address, user agent, and reported operating system from the handoff record. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:handoff:complete` bucket, and a client IP address is blocked after 5 failed code attempts in 15 minutes.
|
|
|
|
## Get desktop handoff status
|
|
|
|
<RouteHeader method="GET" path="/v1/auth/handoff/{code}/status" unauthenticated />
|
|
|
|
Reports the state of a handoff to the device that initiated it. Authentication is not required. Returns a [handoff status](#handoff-status-object) object.
|
|
|
|
### Path parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| code | string | The handoff code, using the same normalisation contract as [get desktop handoff information](#get-desktop-handoff-information) |
|
|
|
|
:::note[This route never returns the token]
|
|
The token needs `poll_secret`, so a completed handoff reports `pending` here. Read it with [get desktop handoff status with the poll secret](#get-desktop-handoff-status-with-the-poll-secret).
|
|
:::
|
|
|
|
The code is the only credential this route checks. The initiating device MUST poll for the completion itself.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [handoff status](#handoff-status-object) object | The state was read, or an unknown code was reported as expired |
|
|
| 400 | [error response](/http-api/#error-response) | The code is malformed, returning `INVALID_HANDOFF_CODE` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
A poll leaves the handoff unchanged. A poll against a completed handoff records a failed attempt for the polling IP address, because this route presents no `poll_secret`. No account state changes and no Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
60 requests per minute, on the `auth:handoff:status` bucket.
|
|
|
|
## Get desktop handoff status with the poll secret
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/handoff/{code}/status" unauthenticated />
|
|
|
|
Reports the state of a handoff and delivers the new session token once, to a caller that presents the poll secret issued at initiation. Authentication is not required. Returns a [handoff status](#handoff-status-object) object.
|
|
|
|
### Path parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| code | string | The handoff code, using the same normalisation contract as [get desktop handoff information](#get-desktop-handoff-information) |
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| poll_secret | string | The secret returned by [initiate desktop handoff](#initiate-desktop-handoff) |
|
|
|
|
:::caution[The completed status is delivered exactly once]
|
|
The token is returned once. Later polls report `expired`, so the initiating device MUST retain it when first received.
|
|
:::
|
|
|
|
A secret that does not match reports `pending` and records a failed attempt for the polling IP address. The route reports no distinct error for a wrong secret, so a caller cannot tell a wrong secret from a handoff the approving device has not finished.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [handoff status](#handoff-status-object) object | The state was read, or an unknown code was reported as expired |
|
|
| 400 | [error response](/http-api/#error-response) | The code is malformed, returning `INVALID_HANDOFF_CODE` |
|
|
| 404 | [error response](/http-api/#error-response) | The completed handoff names an account that no longer exists, returning `UNKNOWN_USER` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Retrieving the token completes the handoff. Other polls leave it unchanged. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
60 requests per minute, on the `auth:handoff:status` bucket.
|
|
|
|
## Cancel a desktop handoff
|
|
|
|
<RouteHeader method="DELETE" path="/v1/auth/handoff/{code}" unauthenticated />
|
|
|
|
Discards a handoff and everything stored against its code. Authentication is not required. Returns 204 with no body.
|
|
|
|
### Path parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| code | string | The handoff code, using the same normalisation contract as [get desktop handoff information](#get-desktop-handoff-information) |
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| poll_secret | string | The secret returned by [initiate desktop handoff](#initiate-desktop-handoff) |
|
|
|
|
An unknown or already expired code has no stored secret, so it returns 400 `INVALID_HANDOFF_CODE`.
|
|
|
|
:::caution[Cancelling requires the poll secret]
|
|
The route requires the `poll_secret` that [initiate desktop handoff](#initiate-desktop-handoff) returned, sent in the JSON body. A wrong secret returns 400 `INVALID_HANDOFF_CODE`, so a party that knows only the code cannot cancel the handoff. After a cancellation the initiating device reads the `expired` status.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 204 | empty | The handoff was discarded |
|
|
| 400 | [error response](/http-api/#error-response) | The code is malformed, or the secret does not match, returning `INVALID_HANDOFF_CODE` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Cancelling prevents any further token retrieval but does not revoke a session already created by completion. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:handoff:cancel` bucket.
|
|
|
|
## Create origin handoff
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/origin-handoff" />
|
|
|
|
Stores encrypted client state so that another web origin of the same instance can take it over once. The official web client uses it to move a signed-in browser from its legacy origin to a new one. Requires a user session token. Returns an [origin handoff](#origin-handoff-object) object.
|
|
|
|
The sending origin encrypts the state and keeps the key. Fluxer never receives the key and never reads the payload. The receiving origin holds a nonce, and the sender passes only its digest here.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| nonce_hash | string | The SHA-256 digest of the receiving origin's nonce, as 64 lowercase hex characters |
|
|
| payload | string | The encrypted client state as base64url, 1 to 8388608 characters |
|
|
|
|
A bot token and an OAuth2 bearer token both return 403 `ACCESS_DENIED`. A session whose account is flagged for suspicious activity is admitted.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [origin handoff](#origin-handoff-object) object | The state was stored |
|
|
| 400 | [error response](/http-api/#error-response) | The body is invalid, returning `INVALID_FORM_BODY` |
|
|
| 400 | [error response](/http-api/#error-response) | The body is larger than 8 MiB plus 1 KiB and the request returns `FILE_SIZE_TOO_LARGE` |
|
|
| 401 | [error response](/http-api/#error-response) | The user session credential is missing or invalid |
|
|
| 403 | [error response](/http-api/#error-response) | The credential is a bot token or an OAuth2 bearer token |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Fluxer keeps the payload, the nonce digest, and the account ID for two minutes under a key derived from the identifier. Creating a handoff creates no session and changes no account state. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
3 requests per 10 minutes, on the `auth:origin_handoff:create` bucket.
|
|
|
|
## Redeem origin handoff
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/origin-handoff/redeem" unauthenticated />
|
|
|
|
Releases the client state stored by [create origin handoff](#create-origin-handoff) and deletes it in the same step. Authentication is not required. Returns an [origin handoff payload](#origin-handoff-payload-object) object.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| handoff_id | string | The identifier returned by create origin handoff, 43 base64url characters |
|
|
| nonce | string | The nonce whose digest the sender supplied, as base64url, 16 to 256 characters |
|
|
|
|
An unknown, expired, or already redeemed identifier returns 404 `UNKNOWN_ORIGIN_HANDOFF`. A nonce whose SHA-256 digest does not match returns 400 `INVALID_ORIGIN_HANDOFF_NONCE`.
|
|
|
|
:::caution[Every attempt consumes the handoff]
|
|
Fluxer deletes the stored state before it compares the nonce. A wrong nonce leaves nothing to retry, so the sender MUST create a new handoff.
|
|
:::
|
|
|
|
On an instance that is not self-hosted, the request MUST send an `Origin` header naming one of the instance's web app origins. Any other `Origin`, or none, returns 403 `INVALID_API_ORIGIN`. A self-hosted instance skips this check.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [origin handoff payload](#origin-handoff-payload-object) object | The state was released and deleted |
|
|
| 400 | [error response](/http-api/#error-response) | The body is invalid, or the nonce does not match |
|
|
| 403 | [error response](/http-api/#error-response) | The `Origin` is not a web app origin of the instance, returning `INVALID_API_ORIGIN` |
|
|
| 404 | [error response](/http-api/#error-response) | No stored handoff has this identifier, returning `UNKNOWN_ORIGIN_HANDOFF` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
A redemption that finds the handoff deletes it, whether or not the nonce matches. Redemption creates no session. The payload is all the receiving origin gets. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:origin_handoff:redeem` bucket.
|
|
|
|
## Start passkey bridge sign-in
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/passkey-bridge" unauthenticated />
|
|
|
|
Starts a `login` or `login_mfa` [passkey bridge](#passkey-bridge) ceremony. Authentication is not required. Returns a [passkey bridge start](#passkey-bridge-start-object) object.
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| purpose | string | The purpose, either `login` or `login_mfa` |
|
|
| runner | string | Where the ceremony runs, either `page` or `native` |
|
|
| ticket?<sup>1</sup> | string | The ticket returned by [password login](#log-in-with-a-password) (1-256 characters) |
|
|
| nonce_hash | string | The SHA-256 digest of the nonce the new origin keeps, as 64 lowercase hex characters |
|
|
|
|
<sup>1</sup> Required for `login_mfa` and refused for `login`. The ticket is read but not consumed
|
|
|
|
The request MUST send an `Origin` header of `https://fluxer.com` or `https://canary.fluxer.com`. Any other `Origin`, none, or a self-hosted instance returns 403 `INVALID_API_ORIGIN`.
|
|
|
|
For `login_mfa`, an expired or unknown ticket returns the field code `SESSION_TIMEOUT` on `ticket`. A ticket for an account that does not count passkeys as its second factor returns 400 `TWO_FACTOR_REQUIRED`, and one for an account with no passkey listed for `fluxer.app` returns 400 `NO_PASSKEYS_REGISTERED`. A ticket that resolves to a bot account returns 403 `BOT_USER_AUTH_ENDPOINT_ACCESS_DENIED`.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [passkey bridge start](#passkey-bridge-start-object) object | The ceremony was started |
|
|
| 400 | [error response](/http-api/#error-response) | The body or ticket is invalid, passkeys are not the second factor, or the account has no `fluxer.app` passkey |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement rejects the operation, the `Origin` is refused, or the ticket resolves to a bot account |
|
|
| 404 | [error response](/http-api/#error-response) | The ticket resolves to an account that no longer exists |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Fluxer stores the ceremony for 10 minutes, or 5 minutes for `login_mfa`, under a key derived from its identifier. It changes no account state. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
10 requests per minute, on the `auth:passkey_bridge:start` bucket.
|
|
|
|
## Get passkey bridge options
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/passkey-bridge/{ceremony_id}/options" unauthenticated />
|
|
|
|
Issues the WebAuthn request for a pending [passkey bridge](#passkey-bridge) ceremony. Authentication is not required. Returns a [passkey bridge options](#passkey-bridge-options-object) object.
|
|
|
|
### Path parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| ceremony_id | string | The identifier from the start response, 43 base64url characters |
|
|
|
|
The request has no body. Its `Origin` MUST be the origin that runs the ceremony, which is the paired legacy origin for `page` and the new origin for `native`. Any other `Origin` returns 403 `INVALID_API_ORIGIN`. An unknown, expired, completed, or cancelled ceremony returns 404 `UNKNOWN_PASSKEY_BRIDGE`, as does a request that arrives while another request holds the same ceremony.
|
|
|
|
A `login` ceremony gets options with no `allowCredentials` and `required` user verification. A `login_mfa` or `sudo` ceremony gets the account's passkeys for `fluxer.app` in `allowCredentials` and `discouraged` user verification, and an account left with none returns 400 `NO_PASSKEYS_REGISTERED`. `rpId` is always `fluxer.app`.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [passkey bridge options](#passkey-bridge-options-object) object | A challenge was issued |
|
|
| 400 | [error response](/http-api/#error-response) | The ceremony ID is malformed, or the account has no passkey for `fluxer.app` |
|
|
| 403 | [error response](/http-api/#error-response) | The `Origin` is refused, returning `INVALID_API_ORIGIN` |
|
|
| 404 | [error response](/http-api/#error-response) | The ceremony is not pending, returning `UNKNOWN_PASSKEY_BRIDGE` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The operation issues a one-use challenge valid for five minutes and bound to the bridge, so no other WebAuthn route accepts it. It revokes the challenge the ceremony held before, so only the latest options can complete it. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
20 requests per minute, on the `auth:passkey_bridge:ceremony` bucket, shared with complete and cancel.
|
|
|
|
## Complete passkey bridge
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/passkey-bridge/{ceremony_id}/complete" unauthenticated />
|
|
|
|
Verifies the assertion for a pending [passkey bridge](#passkey-bridge) ceremony and marks it completed. Authentication is not required. Returns a [passkey bridge finish](#passkey-bridge-finish-object) object.
|
|
|
|
### Path parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| ceremony_id | string | The identifier from the start response, 43 base64url characters |
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| response | [WebAuthn assertion](#webauthn-assertion) object | The browser credential assertion |
|
|
|
|
The `Origin` rule and the 404 cases are those of [get passkey bridge options](#get-passkey-bridge-options). Fluxer verifies the assertion against the latest challenge and accepts only the ceremony origin in its client data.
|
|
|
|
A ceremony with no options issued yet, a credential that is unknown, belongs to another account, is replaced, or is not for `fluxer.app`, and a failed verification all return 401 `PASSKEY_AUTHENTICATION_FAILED`. A verified assertion whose reported signature counter cannot be read returns 500 `INVALID_WEBAUTHN_AUTHENTICATION_COUNTER`.
|
|
|
|
A `login_mfa` ceremony whose ticket has expired returns the field code `SESSION_TIMEOUT` on `ticket`. It draws one attempt from the [login MFA allowances](/topics/rate-limits/#allowances-answering-400), and an exhausted allowance returns the field code `INVALID_CODE` on `ticket`. A `sudo` ceremony draws one attempt from the sudo allowance, and an exhausted allowance returns the field code `INVALID_MFA_CODE` on `mfa_code`.
|
|
|
|
:::note[A failed completion leaves the ceremony pending]
|
|
The runner can fetch new options and try again, or cancel. The MFA allowances bound the retries.
|
|
:::
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [passkey bridge finish](#passkey-bridge-finish-object) object | The assertion was accepted |
|
|
| 400 | [error response](/http-api/#error-response) | The body, ceremony ID, or ticket is invalid, or an MFA allowance is exhausted |
|
|
| 401 | [error response](/http-api/#error-response) | Challenge or assertion verification fails |
|
|
| 403 | [error response](/http-api/#error-response) | The `Origin` is refused, or the ticket resolves to a bot account |
|
|
| 404 | [error response](/http-api/#error-response) | The ceremony is not pending, returning `UNKNOWN_PASSKEY_BRIDGE` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | The verified assertion reported no readable signature counter, or the request could not be completed |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
Success consumes the challenge, advances the credential's signature counter, sets its last use, and stores the completion code's digest with the ceremony. It creates no session and consumes no MFA ticket. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
20 requests per minute, on the shared `auth:passkey_bridge:ceremony` bucket.
|
|
|
|
## Cancel passkey bridge
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/passkey-bridge/{ceremony_id}/cancel" unauthenticated />
|
|
|
|
Marks a [passkey bridge](#passkey-bridge) ceremony cancelled so the new origin can stop waiting. Authentication is not required. Returns a [passkey bridge finish](#passkey-bridge-finish-object) object.
|
|
|
|
### Path parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| ceremony_id | string | The identifier from the start response, 43 base64url characters |
|
|
|
|
The request has no body, and the `Origin` rule is that of [get passkey bridge options](#get-passkey-bridge-options). Cancelling a cancelled ceremony succeeds again with a new completion code. A completed, unknown, or expired ceremony returns 404 `UNKNOWN_PASSKEY_BRIDGE`.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [passkey bridge finish](#passkey-bridge-finish-object) object | The ceremony is cancelled |
|
|
| 400 | [error response](/http-api/#error-response) | The ceremony ID is malformed |
|
|
| 403 | [error response](/http-api/#error-response) | The `Origin` is refused, returning `INVALID_API_ORIGIN` |
|
|
| 404 | [error response](/http-api/#error-response) | The ceremony is completed or does not exist, returning `UNKNOWN_PASSKEY_BRIDGE` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
The ceremony stores the digest of the new completion code. No account state changes and no Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
20 requests per minute, on the shared `auth:passkey_bridge:ceremony` bucket.
|
|
|
|
## Redeem passkey bridge sign-in
|
|
|
|
<RouteHeader method="POST" path="/v1/auth/passkey-bridge/{ceremony_id}/redeem" unauthenticated />
|
|
|
|
Redeems a finished `login` or `login_mfa` [passkey bridge](#passkey-bridge) ceremony once. Authentication is not required. Returns a [passkey bridge sign-in redemption](#passkey-bridge-sign-in-redemption-object) object.
|
|
|
|
### Path parameters
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| ceremony_id | string | The identifier from the start response, 43 base64url characters |
|
|
|
|
### JSON body
|
|
|
|
| Field | Type | Description |
|
|
| --- | --- | --- |
|
|
| nonce | string | The nonce whose digest started the ceremony, as base64url, 16 to 256 characters |
|
|
| completion_code | string | The completion code from the return fragment or the finish response, 43 base64url characters |
|
|
|
|
The `Origin` MUST be the new origin that started the ceremony, or the request returns 403 `INVALID_API_ORIGIN`. A pending ceremony and a `sudo` ceremony return 404 `UNKNOWN_PASSKEY_BRIDGE` and stay in place, as does an unknown or expired identifier. A wrong nonce or completion code returns 400 `INVALID_PASSKEY_BRIDGE_NONCE`.
|
|
|
|
:::caution[A wrong secret consumes the ceremony]
|
|
Fluxer deletes the ceremony before it answers a wrong nonce or completion code, so the new origin MUST start a new one.
|
|
:::
|
|
|
|
A cancelled ceremony returns `cancelled`. A completed `login` ceremony creates a session under the policy of [authenticate with WebAuthn](#authenticate-with-webauthn), so a suspended, bot, or unadmitted account gets the same 403 codes. A completed `login_mfa` ceremony needs its ticket once more, and an expired one returns the field code `SESSION_TIMEOUT` on `ticket`. An account that no longer counts passkeys as its second factor returns 400 `TWO_FACTOR_REQUIRED`.
|
|
|
|
### Response
|
|
|
|
| Status | Body | Condition |
|
|
| --- | --- | --- |
|
|
| 200 | [passkey bridge sign-in redemption](#passkey-bridge-sign-in-redemption-object) object | The ceremony was redeemed |
|
|
| 400 | [error response](/http-api/#error-response) | The body, ceremony ID, or ticket is invalid, a secret does not match, or passkeys are no longer the second factor |
|
|
| 403 | [error response](/http-api/#error-response) | SSO enforcement, the `Origin`, account suspension, a bot account, or an unadmitted registration rejects the request |
|
|
| 404 | [error response](/http-api/#error-response) | No redeemable sign-in ceremony has this identifier, returning `UNKNOWN_PASSKEY_BRIDGE` |
|
|
| 429 | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | Route or global bucket denies the request |
|
|
| 500 | [error response](/http-api/#error-response) | Unexpected internal failure occurs |
|
|
| 503 | [error response](/http-api/#error-response) | The instance is at its in-flight request ceiling |
|
|
|
|
### Side effects
|
|
|
|
A redemption that gets past the secret check deletes the ceremony, whatever happens next. A completed ceremony creates one authentication session, and for `login_mfa` it also consumes the ticket and clears the login MFA allowances. When the request comes from a new origin and the domain migration switch is on, Fluxer opens a [passkey update](/http-api/users/mfa/#passkey-updates) for the new session. No Gateway Dispatch is emitted.
|
|
|
|
### Rate limit
|
|
|
|
60 requests per minute, on the `auth:passkey_bridge:redeem` bucket.
|