Files
accounted/docs/security/auth-abuse-protection.md
T
Mattsson 328ccda10d fix: protect public Auth flows from automated abuse (#1904)
* fix: add Turnstile to public auth flows

* test: isolate Turnstile auth tests
2026-08-25 19:14:06 +02:00

5.5 KiB

Auth Bot Protection Runbook

Accounted's password login, email registration, password recovery, and anonymous sandbox signup support Cloudflare Turnstile through Supabase Auth. The browser integration is dependency-free and loads Cloudflare's official api.js directly. Supabase Auth validates each token with the Turnstile secret configured in the project.

The Turnstile site key is public. The matching secret must exist only in Cloudflare and Supabase Auth. Do not add the secret to this repository, Vercel application variables, Docker images, browser code, logs, or analytics.

Rollout states

The browser exposes the current client state on every protected form through data-turnstile-rollout-state:

  • disabled: NEXT_PUBLIC_TURNSTILE_SITE_KEY is absent. The widget is not loaded and existing Auth calls continue without a CAPTCHA token.
  • client-enabled: the public site key is present. Protected submit buttons remain disabled until the widget returns a token, and the token is passed to Supabase Auth. This state does not prove that provider-side enforcement is active.

When client-enabled, data-turnstile-widget-state reports loading, ready, verified, or error without exposing the token. Never record or inspect the token value itself.

This split keeps login available during rollout. It also means merging this code alone does not activate bot protection.

The hosted rollout in this runbook is scoped to Accounted's main Supabase project. A separate tenant backend needs its own reviewed widget, hostname allowlist, keys, test identities, and activation record. Do not copy the main project's secret into another project.

Hosted activation order

Use separate Turnstile widgets and key pairs for staging and production.

  1. In Cloudflare, create a managed Turnstile widget and allow only the exact hostnames served by that environment. Include each approved white-label hostname explicitly. Do not allow arbitrary hostnames.
  2. Set NEXT_PUBLIC_TURNSTILE_SITE_KEY for the matching Vercel environment and redeploy. The CSP is widened to https://challenges.cloudflare.com only in hosted builds that have this variable.
  3. Verify all four protected forms reach client-enabled and verified on staging with a controlled test account. Confirm Auth requests include a CAPTCHA token, but do not copy or log it. Confirm the canonical callback URL and every approved white-label callback URL still complete Auth.
  4. In the matching Supabase project, open Authentication, Bot and Abuse Protection. Select Cloudflare Turnstile, enter the matching secret, and enable CAPTCHA protection.
  5. Verify on staging that requests without a token, with an invalid token, with an expired token, and with an already-used token are rejected. Verify normal login, registration, recovery, and sandbox entry still work.
  6. Repeat the same ordered rollout for production. Do not use real customer addresses for verification and do not run password or mail-volume tests against production.

Rollback uses the reverse safety order: disable Supabase CAPTCHA enforcement first, then remove the public site key and redeploy. Removing the site key while provider enforcement remains active would block every protected flow.

Self-hosted activation

The Docker image accepts the same optional NEXT_PUBLIC_TURNSTILE_SITE_KEY. Its entrypoint substitutes the public value into the built client. The generic image always permits the Cloudflare Turnstile script and frame origins in CSP so the optional runtime key can work.

Configure the matching Turnstile secret and provider in the installation's GoTrue or Supabase Auth service. Keep the same activation and rollback order as hosted. Leaving the public key unset preserves the existing self-hosted flows.

Controlled testing

Cloudflare publishes dummy site keys and secrets for automated tests. Use the official always-pass or always-fail pair only in a non-production environment. A production secret rejects dummy tokens.

Turnstile tokens expire after five minutes and are single-use. Every Auth attempt resets the widget, including failed attempts, so a retry obtains a new token.

Remaining operational controls

These settings are not controlled by repository code and remain deployment tasks:

  • Record the current Supabase Auth rate-limit values from Authentication, Rate Limits before activation. Exercise sustained limits only on staging and record the expected 429 threshold.
  • Enable leaked-password protection in Supabase Auth when the project plan supports it. A read-only Security Advisor check on 2026-08-25 reported Leaked Password Protection Disabled for the main project.
  • Monitor Supabase Auth logs and Turnstile Analytics for challenge failures, Auth 429 responses, recovery or signup volume, and anonymous-user growth. Alerting must not include email addresses, passwords, CAPTCHA tokens, or other credentials.
  • Anonymous-user retention and deletion require a separate approved retention policy. This integration does not delete Auth users.

References