feat(captcha): make ALTCHA the only captcha (#3035)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-29 17:00:15 +02:00
1 parent d433a039b5
commit 4f968bbc47
209 files changed
+4097 -8692

No files matched your search

@@ -36,7 +36,7 @@ Missing settings use the defaults documented below. Invalid stored configuration
| gateway_rollout | [Gateway rollout configuration](#gateway-rollout-configuration-object) object | Gateway admission and dispatch tuning |
| push_relay | [push relay configuration](#push-relay-configuration-object) object | Operator consent to the Fluxer-run push relay |
| domain_migration | [domain migration configuration](#domain-migration-configuration-object) object | Web domain migration rollout |
| altcha_captcha | [ALTCHA captcha configuration](#altcha-captcha-configuration-object) object | ALTCHA proof-of-work captcha rollout |
| captcha | [captcha configuration](#captcha-configuration-object) object | ALTCHA proof-of-work captcha settings |
| experiment_delivery | [experiment delivery configuration](#experiment-delivery-configuration-object) object | Cadence every client polls the experiments route on |
| registration | [registration configuration](#registration-configuration-object) object | Registration policy, issued URLs, and pending registrations |
| self_hosted | boolean | Whether the deployment runs in self-hosted mode |
@@ -144,34 +144,22 @@ Only the official web client acts on this configuration. On any other instance i
Setting `enabled` to false stops new migrations and also stops forwarding from the legacy origin for clients that already moved. Excluding an account only stops a migration that has not started yet.
:::
## ALTCHA captcha configuration object
## Captcha configuration object
The instance rollout that replaces the configured captcha provider with an ALTCHA proof-of-work challenge the API issues and verifies itself. [Experiments](/http-api/experiments/#altcha-captcha-assignment-object) defines what a signed-in client resolves from it, and [CAPTCHA handling](/topics/captcha/#altcha-proof-of-work) defines the challenge exchange.
The ALTCHA proof-of-work check that the API issues and verifies itself on gated operations. [CAPTCHA handling](/topics/captcha/) defines the challenge exchange and lists the gated operations.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the rollout runs at all (default false) |
| config_version | integer | Revision counter, raised by Fluxer and never accepted from a request |
| rollout_basis_points | integer | Share of accounts the rollout selects, in basis points (0-10000, default 0) |
| rollout_salt | string | Salt of the sampling hash (1-64 printable ASCII characters, default `altcha-captcha-v1`) |
| included_user_ids | array[snowflake] | Accounts the rollout always selects, up to 1000 entries (default empty) |
| included_guild_ids | array[snowflake] | Guilds whose members the rollout always selects, up to 1000 entries (default empty) |
| include_premium_users | boolean | Whether the rollout always selects accounts with active premium (default false) |
| excluded_user_ids | array[snowflake] | Accounts the rollout never selects, up to 1000 entries (default empty) |
| anonymous_enabled | boolean | Whether logged-out requests get ALTCHA (default false) |
| cost | integer | PBKDF2 iterations per solving attempt (1000-100000, default 5000) |
| max_counter | integer | Upper bound of the hidden counter a client searches for (100-1000000, default 10000) |
| enabled | boolean | Whether gated operations require a solved challenge (default true) |
| cost | integer | PBKDF2 iterations per solving attempt (1000-20000, default 5000) |
| max_counter | integer | Upper bound of the hidden counter a client searches for (100-20000, default 1000) |
Every field is present on read. An absent document or missing field uses the defaults above.
`excluded_user_ids` wins over every other rule. Otherwise the rollout selects an account in `included_user_ids`, a member of a guild in `included_guild_ids`, or a premium account while `include_premium_users` is true, whatever `rollout_basis_points` says. `anonymous_enabled` has no effect on signed-in requests, and the account rules have no effect on logged-out ones.
Every field is present on read. An absent document or missing field uses the defaults above, so the check is on until an administrator sets `enabled` to false. Setting `enabled` to false turns the check off for the whole instance. Phone sends that the [phone verification service](/http-api/users/phone-verification/#send-phone-verification) challenges are refused instead.
Each challenge hides its counter between half of `max_counter` and `max_counter`, and a client tries counters from 0 upward. Solve time grows with `cost` times `max_counter`. Fluxer spends one attempt at `cost` to issue each challenge.
The rollout only changes which provider answers a captcha that is already required. While the instance captcha provider is `none`, it has no effect.
## Experiment delivery configuration object
How often a client polls [Get experiment assignments](/http-api/experiments/#get-experiment-assignments), and how widely those polls are spread. The setting is instance-wide and applies to every experiment at once, so adding an experiment adds no second cadence to tune.
@@ -340,26 +328,9 @@ Every provider reports its stored settings, a `_set` boolean in place of each se
| --- | --- | --- |
| gif | object | `klipy_api_key_set` and `effective_available` |
| youtube | object | `api_key_set` and `effective_available` |
| captcha | [captcha integration](#captcha-integration-object) object | CAPTCHA provider settings |
| email | [email integration](#email-integration-object) object | Outbound email settings |
| bluesky | [Bluesky integration](#bluesky-integration-object) object | Bluesky client settings |
## Captcha integration object
CAPTCHA provider settings, and the provider resolved from them.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| provider | ?string | Operator override of `hcaptcha`, `turnstile`, or `none`, or null for no override |
| effective_provider | string | Provider in use, one of `hcaptcha`, `turnstile`, or `none` |
| hcaptcha_site_key | ?string | hCaptcha site key |
| hcaptcha_secret_key_set | boolean | Whether an hCaptcha secret is stored or supplied by deployment configuration |
| turnstile_site_key | ?string | Turnstile site key |
| turnstile_secret_key_set | boolean | Whether a Turnstile secret is stored or supplied by deployment configuration |
| effective_enabled | boolean | Whether CAPTCHA verification is in force |
## Email integration object
Outbound email settings, and the provider resolved from them.
@@ -622,11 +593,11 @@ The body has one optional object for each section. Fluxer leaves an absent secti
| gateway_rollout? | object | Any subset of the [Gateway rollout configuration](#gateway-rollout-configuration-object) fields, each bound as documented there |
| push_relay? | object | `relay_consent_accepted` from the [push relay configuration](#push-relay-configuration-object) |
| domain_migration? | object | Any subset of the [domain migration](#domain-migration-configuration-object) fields |
| altcha_captcha? | object | Any subset of the [ALTCHA captcha](#altcha-captcha-configuration-object) fields |
| captcha? | object | Any subset of the [captcha configuration](#captcha-configuration-object) fields |
| experiment_delivery? | object | Any subset of the [experiment delivery](#experiment-delivery-configuration-object) fields |
| registration? | object | `mode` and `admin_registration_urls_enabled` |
| app_public?<sup>2</sup> | object | `branding`, `setup`, `legal`, and `registration` sub-objects, each merged field by field |
| integrations?<sup>3</sup> | object | `gif`, `youtube`, `captcha`, `email`, and `bluesky` sub-objects, the last of which also has the `keys` array |
| integrations?<sup>3</sup> | object | `gif`, `youtube`, `email`, and `bluesky` sub-objects, the last of which also has the `keys` array |
| media? | object | `attachment_decay` overrides, each nullable to restore the deployment default |
| policy? | [instance policy update](#instance-policy-update-structure) object | Community, direct message, premium, and gating policy |
| billing?<sup>4</sup> | object | Stored [instance billing](#instance-billing-object) fields, with `stripe_secret_key` and `stripe_webhook_secret` for the secrets |
@@ -639,11 +610,11 @@ The body has one optional object for each section. Fluxer leaves an absent secti
`domain_migration` takes every [domain migration configuration](#domain-migration-configuration-object) field except `config_version`, each bound as documented there. Fluxer raises `config_version` by one on each request that supplies at least one of them. A section that is absent, or present with no field set, writes nothing and leaves `config_version` alone.
`altcha_captcha` works the same way, over the [ALTCHA captcha configuration](#altcha-captcha-configuration-object) fields and its own `config_version`.
`captcha` takes any subset of the [captcha configuration](#captcha-configuration-object) fields, each bound as documented there. Fluxer merges the supplied fields over the stored ones. The section has no `config_version`.
`experiment_delivery` takes both [experiment delivery configuration](#experiment-delivery-configuration-object) fields, each bound as documented there. It is a section of its own, so a write to it changes no `config_version` and changes no assignment, only the cadence on which clients ask for one.
<sup>3</sup> A secret such as `klipy_api_key`, `api_key`, `hcaptcha_secret_key`, `turnstile_secret_key`, or the SMTP `password` is written when supplied and left alone when absent. `integrations.bluesky.keys` is the only way to write the Bluesky signing keys counted as `bluesky.key_count`. It takes up to 8 entries of `kid` (1-255 characters) and nullable `private_key` (up to 10000 characters), and replaces the stored key set outright
<sup>3</sup> A secret such as `klipy_api_key`, `api_key`, or the SMTP `password` is written when supplied and left alone when absent. `integrations.bluesky.keys` is the only way to write the Bluesky signing keys counted as `bluesky.key_count`. It takes up to 8 entries of `kid` (1-255 characters) and nullable `private_key` (up to 10000 characters), and replaces the stored key set outright
<sup>4</sup> Accepted only on a self-hosted deployment. A hosted deployment rejects the section, and `app_public.branding.premium_product_name` and `premium_info_url`, with 400 `INVALID_FORM_BODY`. A Stripe secret, `enabled`, `automatic_tax`, `tax_id_collection` and `terms_consent_required` are written when supplied, cleared by null, and left alone when absent. A null `enabled` follows `FLUXER_STRIPE_ENABLED`. `prices`, `country_currencies`, and `legacy_prices` replace the stored value outright, a price slot left out is stored as null, and an empty `prices` object restores the deployment catalogue. A price ID is `price_` followed by letters and digits. A currency is three upper-case letters and a country code is two
@@ -678,7 +649,7 @@ On a self-hosted deployment, billing and the `everyone` premium mode exclude eac
| 400 | [error response](/admin-api/#error-response) | A policy transition is refused, returned as `INSTANCE_POLICY_TRANSITION_NOT_ALLOWED` |
:::caution[Sections are applied one after another]
The order is `gateway_rollout`, `push_relay`, `domain_migration`, `altcha_captcha`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, `billing`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
The order is `gateway_rollout`, `push_relay`, `domain_migration`, `captcha`, `experiment_delivery`, `sso`, `registration`, `app_public` branding, legal, and registration fields, `integrations`, `media`, `policy`, `billing`, and finally `app_public.setup`. A failure part way through leaves the earlier sections written.
:::
### Side effects
@@ -291,12 +291,9 @@ An unclaimed account cannot create an application. An account is unclaimed while
| Field | Type | Description |
| --- | --- | --- |
| X-Captcha-Token?<sup>1</sup> | string | The CAPTCHA proof for the request |
| X-Captcha-Type?<sup>2</sup> | string | The CAPTCHA provider to verify against, either `hcaptcha` or `turnstile` |
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Fluxer skips verification when CAPTCHA is disabled for the instance, when the account has the CAPTCHA exemption flag, or when the caller's email address is on an exempt domain, as described by [CAPTCHA handling](/topics/captcha/)
<sup>2</sup> Fluxer verifies against the instance's configured provider when the header is absent
<sup>1</sup> A missing token returns 400 `CAPTCHA_REQUIRED` and a rejected token returns 400 `INVALID_CAPTCHA`, each with a new challenge. Fluxer skips verification when the check is off for the instance, when the account has the CAPTCHA exemption flag, or when the caller's email address is on an exempt domain
### JSON body
@@ -506,21 +506,18 @@ Approval mode registration instead returns 403 `REGISTRATION_PENDING_APPROVAL` a
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 [CAPTCHA](/topics/captcha/) when CAPTCHA is enabled. 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.
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 proof issued by the CAPTCHA provider |
| X-Captcha-Type?<sup>2</sup> | string | The provider that issued the proof, either `hcaptcha` or `turnstile` |
| Accept-Language?<sup>3</sup> | string | The language tag that selects the locale stored on the new account |
| 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 proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`, and verification is skipped entirely when CAPTCHA is disabled for the instance
<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> Omitting the header selects the instance's configured provider
<sup>3</sup> The parsed locale becomes the account locale and selects the language of the verification email
<sup>2</sup> The parsed locale becomes the account locale and selects the language of the verification email
### JSON body
@@ -589,22 +586,19 @@ Approval mode registration creates no guild membership and no authentication ses
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 CAPTCHA when CAPTCHA is enabled. 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.
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 proof issued by the CAPTCHA provider |
| X-Captcha-Type?<sup>2</sup> | string | The provider that issued the proof, either `hcaptcha` or `turnstile` |
| X-Fluxer-Client-Properties?<sup>3</sup> | string | The base64-encoded JSON with the reporting client's `os` |
| User-Agent?<sup>3</sup> | string | The client string recorded on the created session and shown by [list authentication sessions](#list-authentication-sessions) |
| 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 proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`, and verification is skipped entirely when CAPTCHA is disabled for the instance
<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> Omitting the header selects the instance's configured provider
<sup>3</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`
<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
@@ -935,18 +929,15 @@ The operation creates one 64-character verification token bound to the account a
Accepts a password recovery request for an email address. Authentication is not required. Returns 204 with no body.
Password recovery verifies CAPTCHA when CAPTCHA is enabled. Fluxer consumes both the client IP address and email address allowances before it validates the address, and exhausting either returns 429.
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 proof issued by the CAPTCHA provider |
| X-Captcha-Type?<sup>2</sup> | string | The provider that issued the proof, either `hcaptcha` or `turnstile` |
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`, and verification is skipped entirely when CAPTCHA is disabled for the instance
<sup>2</sup> Omitting the header selects the instance's configured provider
<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
@@ -568,7 +568,7 @@ Adds a user to a group direct message and returns 204 with an empty body. Emits
- The caller must be a current recipient of the group, and must be friends with the target, including when the caller is a bot.
- The target's group direct message admission policy must allow the caller.
- An instance with CAPTCHA enabled also requires a CAPTCHA proof.
- A solved [CAPTCHA](/topics/captcha/) challenge is required while the instance check is on.
Fluxer evaluates the target's admission policy in this order.
@@ -588,12 +588,9 @@ Fluxer evaluates the target's admission policy in this order.
| Field | Type | Description |
| --- | --- | --- |
| X-Captcha-Token?<sup>1</sup> | string | A CAPTCHA proof |
| X-Captcha-Type?<sup>2</sup> | string | The CAPTCHA provider, either `hcaptcha` or `turnstile` |
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`, as does a proof for a provider the instance holds no secret key for. [CAPTCHA](/topics/captcha/) states when an instance skips verification
<sup>2</sup> `hcaptcha` or `turnstile` selects that provider whether or not the instance configured it. The instance's configured provider is used when the header is absent or has any other value
<sup>1</sup> A missing token returns 400 `CAPTCHA_REQUIRED` and a rejected token returns 400 `INVALID_CAPTCHA`, each with a new challenge. [CAPTCHA handling](/topics/captcha/#exemption) states when an instance skips verification
### Response
@@ -20,7 +20,7 @@ An OAuth2 protocol failure raised by the [OAuth2 resource](/http-api/oauth2/) an
## Supplementary members
The error code determines which supplementary members a failure has, and most codes have none. A client reads only the members documented for the code it matched. `errors` is the list of field violations. `retry_after` is the delay before another attempt is admitted. `global` is `true` on a global rate limit denial and `false` on a route one. `required_scope` is the OAuth2 scope the request is missing. `has_mfa` and `methods` are the [sudo mode](/http-api/users/mfa/#sudo-mode) proofs an account can supply.
The error code determines which supplementary members a failure has, and most codes have none. A client reads only the members documented for the code it matched. `errors` is the list of field violations. `retry_after` is the delay before another attempt is admitted. `global` is `true` on a global rate limit denial and `false` on a route one. `required_scope` is the OAuth2 scope the request is missing. `has_mfa` and `methods` are the [sudo mode](/http-api/users/mfa/#sudo-mode) proofs an account can supply. `captcha_provider` and `altcha_challenge` are the challenge described in [CAPTCHA handling](/topics/captcha/).
`GLOBAL_IP_BANNED` and `GLOBAL_IP_TEMPORARILY_BANNED` have their own members:
@@ -286,7 +286,7 @@ Community ownership can't be transferred to a bot
### `CAPTCHA_REQUIRED`
Captcha is required
Verification required. Try again
### `COMMUNICATION_DISABLED`
@@ -506,7 +506,7 @@ Invalid bot flag
### `INVALID_CAPTCHA`
Invalid captcha
Verification failed. Try again
### `INVALID_CHANNEL_TYPE`
@@ -6,7 +6,7 @@ description: The experiment assignments envelope, the revalidation and polling c
import RouteHeader from '@/components/RouteHeader.astro';
An experiment is an instance-wide rollout that an operator configures. For each account, Fluxer works out from that configuration whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `domain_migration` and `altcha_captcha`.
An experiment is an instance-wide rollout that an operator configures. For each account, Fluxer works out from that configuration whether the account is in the rollout and which settings the account receives. The single route on this page resolves every experiment the server defines and returns them in one envelope, together with the polling cadence they share. This server defines `domain_migration`.
Every assignment is advice. A client that ignores one behaves as it does with the rollout off, and no route and no Gateway event reports what a client actually ran.
@@ -33,7 +33,6 @@ One entry per experiment. The envelope reports this object even when it is empty
| Field | Type | Description |
| --- | --- | --- |
| domain_migration? | [domain migration assignment](#domain-migration-assignment-object) object | The caller's web domain migration assignment |
| altcha_captcha? | [ALTCHA captcha assignment](#altcha-captcha-assignment-object) object | The caller's captcha provider assignment |
Ignore unknown experiments and treat a missing experiment as off.
@@ -51,20 +50,6 @@ A caller is drawn by the operator's account or guild allowlist, by having premiu
Only the official web client acts on this assignment, and only on its legacy origins. Every other client ignores it. The logged-out share and the instance-wide switch are published in the [instance discovery document](/http-api/instance/#domain-migration-object) instead, because a client that holds no credential cannot read this route.
## ALTCHA captcha assignment object
One resolution of the instance ALTCHA captcha rollout against one account. This server version writes the key on every response.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| enabled | boolean | Whether the caller's captcha challenges are ALTCHA proof-of-work challenges |
A caller is drawn by the operator's account or guild allowlist, by having premium when the operator includes premium users, or by the sampled share of the account population. `enabled` is false in every other case, the rollout being off included.
The assignment is informational. The server applies the same resolution to every request that needs a captcha and names the provider in the [captcha error](/topics/captcha/#altcha-proof-of-work), so a client needs no copy of this value to answer a challenge.
## Get experiment assignments
<RouteHeader method="GET" path="/v1/experiments" bot />
@@ -131,12 +131,9 @@ One redemption can be in flight for a code across the whole deployment, and a se
| Field | Type | Description |
| --- | --- | --- |
| X-Captcha-Token?<sup>1</sup> | string | The proof issued by the CAPTCHA provider |
| X-Captcha-Type?<sup>2</sup> | string | The CAPTCHA provider, either `hcaptcha` or `turnstile` |
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
<sup>1</sup> A missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`. Verification is skipped when CAPTCHA is disabled, when the account has the exemption flag, or when the account's email address is on an exempt domain, as described by [CAPTCHA handling](/topics/captcha/)
<sup>2</sup> Any other value, including an omitted header, falls back to the instance's configured provider
<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 when the check is off for the instance, when the account has the exemption flag, or when the account's email address is on an exempt domain
### Response
@@ -95,8 +95,7 @@ These headers are accepted across resources. An operation-specific header is doc
| X-Audit-Log-Reason?<sup>3</sup> | string | Free-text reason recorded on the resulting audit log entry |
| X-Fluxer-Client-Properties?<sup>4</sup> | string | Base64-encoded JSON with the native client's `os`, read when an authentication session is created |
| X-Fluxer-Sudo-Mode-JWT?<sup>5</sup> | string | A sudo mode proof previously issued to the authenticated user |
| X-Captcha-Token?<sup>6</sup> | string | The CAPTCHA solution issued by the selected provider |
| X-Captcha-Type?<sup>6</sup> | string | Either `hcaptcha` or `turnstile`, selecting the provider that issued the token |
| X-Captcha-Token?<sup>6</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
| X-Request-ID?<sup>7</sup> | string | A correlation identifier the client chooses, echoed unchanged in the response |
| User-Agent? | string | The originating client description recorded on a new authentication session and on an Admin audit entry |
| Origin?<sup>8</sup> | string | The browser origin used for cross-origin negotiation and for the mutating same-host origin check |
@@ -111,7 +110,7 @@ These headers are accepted across resources. An operation-specific header is doc
<sup>5</sup> A valid token replaces the sudo proof fields in the operation body and is echoed in the response header without extending its lifetime.
<sup>6</sup> Read only while the instance has a provider configured and the operation is gated. The handshake is defined in [CAPTCHA handling](/topics/captcha/)
<sup>6</sup> Read only on a gated operation while the instance CAPTCHA check is on
<sup>7</sup> A supplied value is echoed back unchanged and unvalidated
@@ -106,29 +106,22 @@ A client MUST NOT upgrade a published `http` value to `https`, downgrade a publi
## CAPTCHA configuration object
Which CAPTCHA provider this deployment uses, and the site keys a client needs to render its challenge.
Whether this deployment asks for a CAPTCHA on gated operations.
### Structure
| Field | Type | Description |
| --- | --- | --- |
| provider<sup>1</sup> | string | The selected [CAPTCHA provider](#captcha-providers) |
| hcaptcha_site_key<sup>2</sup> | ?string | The configured hCaptcha site key, or null when none is configured |
| turnstile_site_key<sup>2</sup> | ?string | The configured Turnstile site key, or null when none is configured |
<sup>1</sup> The value is `none` when the configured provider's site key or secret key is missing
<sup>2</sup> Only the key that matches `provider` is non-null, so both are null while `provider` is `none`
| provider | string | The [CAPTCHA provider](#captcha-providers) in use |
### CAPTCHA providers
| Value | Description |
| --- | --- |
| altcha | An ALTCHA proof-of-work challenge gates the operations that require one |
| none | The deployment requires no CAPTCHA |
| hcaptcha | hCaptcha challenges gate the operations that require one |
| turnstile | Cloudflare Turnstile challenges gate the operations that require one |
The complete challenge handshake, including the `X-Captcha-Token` and `X-Captcha-Type` request headers, is defined in [CAPTCHA handling](/topics/captcha/).
The complete challenge handshake, including the `X-Captcha-Token` request header, is defined in [CAPTCHA handling](/topics/captcha/).
## Instance features object
@@ -169,7 +169,15 @@ Starts verification of the submitted number. Requires an account satisfying [eli
The instance can route the attempt to an inbound challenge instead of outbound SMS, and that overrides a requested `sms`. Requesting `inbound_challenge` always yields a challenge.
The number then passes the [number checks](#number-checks). A number that has reached its verification limit is refused with 400 `PHONE_ALREADY_USED`. Sends are limited for each account and for each number, and a denial returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. A request can also return 400 `CAPTCHA_REQUIRED`. This route reads no [CAPTCHA](/topics/captcha/) solution header, so the attempt cannot be retried with a solution. Every other refusal returns 400 `SMS_VERIFICATION_UNAVAILABLE`, as does an instance with no phone verification service.
The number then passes the [number checks](#number-checks). A number that has reached its verification limit is refused with 400 `PHONE_ALREADY_USED`. Sends are limited for each account and for each number, and a denial returns 429 `PHONE_RATE_LIMIT_EXCEEDED`. The phone verification service can also ask for a solved [CAPTCHA](/topics/captcha/) challenge. The route then answers 400 `CAPTCHA_REQUIRED` with a challenge, and a retry with the solved challenge in `X-Captcha-Token` continues the send. When the instance has the check turned off, or the account is exempt from it, that request returns 429 `PHONE_RATE_LIMIT_EXCEEDED` instead. Every other refusal returns 400 `SMS_VERIFICATION_UNAVAILABLE`, as does an instance with no phone verification service.
### Request headers
| Field | Type | Description |
| --- | --- | --- |
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
<sup>1</sup> Read only when the phone verification service asks for a CAPTCHA. A missing token then returns 400 `CAPTCHA_REQUIRED` and a rejected token returns 400 `INVALID_CAPTCHA`, each with a new challenge
### JSON body
@@ -185,7 +193,7 @@ The number then passes the [number checks](#number-checks). A number that has re
| Status | Body | Condition |
| --- | --- | --- |
| 200 | [SMS delivery](#sms-delivery-object) object \| [inbound challenge](#inbound-challenge-object) object | Verification was started on the channel named by the response |
| 400 | [error response](/http-api/#error-response) | The number is refused with `INVALID_PHONE_NUMBER`, has reached its verification limit and returns `PHONE_ALREADY_USED`, the attempt returns `CAPTCHA_REQUIRED`, or the send could not start and returns `SMS_VERIFICATION_UNAVAILABLE` |
| 400 | [error response](/http-api/#error-response) | The number is refused with `INVALID_PHONE_NUMBER`, has reached its verification limit and returns `PHONE_ALREADY_USED`, the attempt asks for a CAPTCHA with `CAPTCHA_REQUIRED` or `INVALID_CAPTCHA`, or the send could not start and returns `SMS_VERIFICATION_UNAVAILABLE` |
| 403 | [error response](/http-api/#error-response) | The account is not eligible and the request returns `PHONE_ADD_NOT_ELIGIBLE` |
| 429<sup>1</sup> | [rate limit response](/topics/rate-limits/#rate-limit-response-object) | A send limit or a provider throttle denies the request, which returns `PHONE_RATE_LIMIT_EXCEEDED` |
@@ -120,12 +120,9 @@ Fluxer holds a bot caller to no friendship. A recipient that has blocked the bot
| Field | Type | Description |
| --- | --- | --- |
| X-Captcha-Token?<sup>1</sup> | string | The CAPTCHA proof |
| X-Captcha-Type?<sup>2</sup> | string | The CAPTCHA provider, either `hcaptcha` or `turnstile` |
| X-Captcha-Token?<sup>1</sup> | string | The solved ALTCHA challenge, see [CAPTCHA handling](/topics/captcha/) |
<sup>1</sup> Read only on a request supplying `recipients`, where a missing proof returns 400 `CAPTCHA_REQUIRED` and a rejected proof returns 400 `INVALID_CAPTCHA`, as does a proof for a provider the instance holds no secret key for. [CAPTCHA](/topics/captcha/) states when an instance skips verification
<sup>2</sup> The value `hcaptcha` or `turnstile` selects that provider whether or not the instance configured it. The instance's configured provider is used when the header is absent or has any other value
<sup>1</sup> Read only on a request supplying `recipients`, where a missing token returns 400 `CAPTCHA_REQUIRED` and a rejected token returns 400 `INVALID_CAPTCHA`, each with a new challenge. [CAPTCHA handling](/topics/captcha/#exemption) states when an instance skips verification
### JSON body
@@ -760,14 +760,7 @@ Only `api` and `worker` build the mail service, so recreating those two is enoug
## CAPTCHA
All are optional.
| Variable | Value in `.env.example` | Controls |
| --- | --- | --- |
| FLUXER_CAPTCHA_ENABLED | `false` | The [CAPTCHA](/topics/captcha/) switch. Startup fails when it is `true` without a provider and that provider's keys |
| FLUXER_CAPTCHA_PROVIDER | `none` | The provider. `hcaptcha`, `turnstile`, or `none`. Anything else fails startup |
`FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY`, `FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY`, `FLUXER_CAPTCHA_TURNSTILE_SITE_KEY`, and `FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY` ship empty in `.env.example` and the shipped Compose file forwards them all. Set the pair the selected provider needs. The admin dashboard's Runtime Integrations panel sets the same provider and keys, and a value stored there wins over the environment. An instance that configures CAPTCHA only there must leave `FLUXER_CAPTCHA_ENABLED` at `false`, because the boot check reads the environment alone.
[ALTCHA](/topics/captcha/) proof-of-work is on by default. Turn it off or tune it under Instance Config, Bot protection in the admin panel. No environment variables configure it.
## Single sign-on and passkeys
@@ -1677,13 +1670,17 @@ Product name, client-visible brand assets, and the setup state in the instance d
Registration mode of `open`, `approval`, or `closed`, admin-issued registration URLs, and pending approval requests.
#### Instance Config, Bot protection
The [ALTCHA](/topics/captcha/) proof-of-work check on sign-up, login, password recovery, and a few other gated operations. It is on by default. Turn it off here, or change its cost and maximum counter.
#### Instance Config, Community & Policy
Single-community mode, direct messages and friends, the premium model, and optional embed services.
#### Instance Config, Runtime Integrations
The Klipy GIF key, the YouTube Data API key, the CAPTCHA provider and its keys, email delivery with an SMTP connection test, and Bluesky OAuth.
The Klipy GIF key, the YouTube Data API key, email delivery with an SMTP connection test, and Bluesky OAuth.
#### Instance Config, Premium & billing
+34 -51
View File
@@ -1,20 +1,43 @@
---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: CAPTCHA handling
description: How a client discovers the CAPTCHA provider and answers a challenge.
description: How a client answers the ALTCHA proof-of-work check.
---
An instance can require a CAPTCHA solution on a small set of abuse-sensitive operations. A client reads the selected provider and its site key from [instance discovery](/http-api/instance/#instance-discovery-object), renders that provider's widget, and sends the solution on the gated request.
Fluxer asks for a CAPTCHA on a small set of abuse-sensitive operations. The CAPTCHA is an [ALTCHA](https://altcha.org) proof-of-work challenge that the API issues and verifies itself. No third-party service or widget is involved, and a client solves the challenge without user input.
## Discovering the provider
## Overview
`GET /.well-known/fluxer` publishes the [CAPTCHA configuration object](/http-api/instance/#captcha-configuration-object) inside the [instance discovery object](/http-api/instance/#instance-discovery-object). Its `provider` field names the selected [CAPTCHA provider](/http-api/instance/#captcha-providers), and `hcaptcha_site_key` or `turnstile_site_key` is that provider's site key. The other key is null, and both are null when `provider` is `none`.
The check is on by default. An administrator can turn it off, or change its `cost` and `max_counter`, in the [captcha configuration](/admin-api/instance/#captcha-configuration-object). While it is off, a gated operation proceeds with no CAPTCHA header.
The value `none` means the instance challenges no operation. A gated operation then proceeds with no CAPTCHA header. The values `hcaptcha` and `turnstile` name the provider whose widget a client renders.
The `captcha` object in [instance discovery](/http-api/instance/#captcha-configuration-object) reports `altcha` while the check is on and `none` while it is off. A client does not need it, because every challenge arrives in the error response that asks for it.
## The retry handshake
Send the request without a CAPTCHA header. A gated operation answers 400 `CAPTCHA_REQUIRED`, and the [error response](/http-api/#error-response) has two more fields.
| Field | Type | Description |
| --- | --- | --- |
| captcha_provider | string | Always `altcha` |
| altcha_challenge | object | An ALTCHA v2 challenge, with `parameters` and `signature` |
The challenge uses `PBKDF2/SHA-256`. Solve it with an ALTCHA v2 solver, then retry the same request with `X-Captcha-Token` set to the [token](#token-format). An accepted token lets the operation proceed.
A rejected token returns 400 `INVALID_CAPTCHA` with a new challenge in the same two fields. A client solves that challenge and retries again.
:::caution[A challenge is single-use]
Each challenge is accepted once and expires 10 minutes after it is issued. A replayed, expired or wrong token returns `INVALID_CAPTCHA`. A client MUST NOT resend a token after `INVALID_CAPTCHA`.
:::
The route rate limit on registration, login and password recovery runs before the check. The challenge response and the retry each use one request from that allowance.
## Token format
The token is the base64 encoding of the UTF-8 JSON object `{"challenge": {"parameters": ..., "signature": ...}, "solution": {"counter": ..., "derivedKey": ...}}`. The solution can also have `time`. Copy `parameters` and `signature` unchanged from `altcha_challenge`. A token longer than 4096 characters, or one that does not decode to this shape, returns `INVALID_CAPTCHA`.
## Gated operations
The following operations verify a CAPTCHA while discovery reports a `provider` other than `none`.
The following operations verify a CAPTCHA while the check is on.
| Method | Route | Operation |
| --- | --- | --- |
@@ -28,57 +51,17 @@ The following operations verify a CAPTCHA while discovery reports a `provider` o
Create private channel is gated only on the group direct message path, where the request body has a `recipients` member. A one-to-one direct message request omits the field and is never gated.
[Send phone verification](/http-api/users/phone-verification/#send-phone-verification) verifies a CAPTCHA only when the phone verification service asks for one. It then answers and accepts the handshake the same way. When the check is off or the account is exempt, that send is refused with `PHONE_RATE_LIMIT_EXCEEDED` instead.
## Exemption
Fluxer skips the check in three cases, and the operation then proceeds with no CAPTCHA header. The authenticated account's email address is on an exempt domain. The authenticated account holds the [`APP_STORE_REVIEWER`](/admin-api/users/#account-flags) flag. The request body has an `email` that belongs to an account holding that flag. Discovery does not report exemptions, so clients must handle a challenge on every gated operation.
## Request headers
| Field | Type | Description |
| --- | --- | --- |
| X-Captcha-Token?<sup>1</sup> | string | The solution issued by the provider widget |
| X-Captcha-Type?<sup>2</sup> | string | The provider that produced the solution, accepting `hcaptcha`, `turnstile` or `altcha` |
<sup>1</sup> An absent or empty value on a gated operation returns 400 `CAPTCHA_REQUIRED`
<sup>2</sup> An absent value selects the instance's configured provider, and so does any value other than `hcaptcha`, `turnstile` or `altcha`. Naming a provider the instance holds no secret key for returns 400 `INVALID_CAPTCHA`. The value `altcha` is accepted only from a requester the [ALTCHA rollout](#altcha-proof-of-work) selects.
## The retry handshake
Send the request without CAPTCHA headers. On 400 `CAPTCHA_REQUIRED`, obtain a solution through the selected provider's widget using its advertised site key. Retry the same request with `X-Captcha-Token` set to the solution and, optionally, `X-Captcha-Type` set to the provider.
An accepted solution allows the operation to proceed. A rejected solution returns 400 `INVALID_CAPTCHA`.
:::caution[A solution is single-use]
The provider treats an already redeemed solution as invalid. A client obtains a new solution before retrying after `INVALID_CAPTCHA` and MUST NOT replay the previous `X-Captcha-Token` value.
:::
## ALTCHA proof-of-work
An operator can move selected requesters from the configured provider to an [ALTCHA](https://altcha.org) proof-of-work challenge that the API issues and verifies itself. The [ALTCHA captcha configuration](/admin-api/instance/#altcha-captcha-configuration-object) selects signed-in accounts by rollout share and allowlist, and logged-out requests with one switch. The check applies only where a captcha is already required, so an instance whose `provider` is `none` never serves it.
For a selected requester, the `CAPTCHA_REQUIRED` and `INVALID_CAPTCHA` bodies have two more fields.
| Field | Type | Description |
| --- | --- | --- |
| captcha_provider | string | Always `altcha` |
| altcha_challenge | object | An ALTCHA v2 challenge, with `parameters` and `signature` |
The challenge uses `PBKDF2/SHA-256` and expires 10 minutes after it is issued. Solve it with an ALTCHA v2 solver, then retry with `X-Captcha-Type` set to `altcha` and `X-Captcha-Token` set to the base64 encoding of the JSON object `{"challenge": <the challenge>, "solution": <the solution>}`. Each challenge is accepted once. A replayed, expired or wrong solution returns 400 `INVALID_CAPTCHA` with a new challenge.
A selected requester can still answer with the configured provider, so a client that does not read these fields keeps working.
## Provider verification
A rejected solution or unavailable provider returns 400 `INVALID_CAPTCHA`. The response does not distinguish between these causes.
## Error codes
| Code | Status | Description |
| --- | --- | --- |
| CAPTCHA_REQUIRED<sup>1</sup> | 400 | The operation is gated and the request has no solution |
| INVALID_CAPTCHA | 400 | The provider rejected the solution, or verification could not be completed |
| CAPTCHA_REQUIRED | 400 | The operation is gated and the request has no token |
| INVALID_CAPTCHA | 400 | The token is malformed, expired, already used, or wrong |
<sup>1</sup> [Send phone verification](/http-api/users/phone-verification/#send-phone-verification) can also answer this code. That operation is not gated and accepts no solution, so retrying it with `X-Captcha-Token` never helps
Both codes are defined in the [API error code registry](/http-api/errors/#api-error-code-registry), and the body of each is the ordinary [error response](/http-api/#error-response) envelope.
Both codes are defined in the [API error code registry](/http-api/errors/#api-error-code-registry). Both bodies are the ordinary [error response](/http-api/#error-response) envelope with `captcha_provider` and `altcha_challenge` added.