* fix: add Turnstile to public auth flows * test: isolate Turnstile auth tests
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_KEYis 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.
- 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.
- Set
NEXT_PUBLIC_TURNSTILE_SITE_KEYfor the matching Vercel environment and redeploy. The CSP is widened tohttps://challenges.cloudflare.comonly in hosted builds that have this variable. - Verify all four protected forms reach
client-enabledandverifiedon 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. - In the matching Supabase project, open Authentication, Bot and Abuse Protection. Select Cloudflare Turnstile, enter the matching secret, and enable CAPTCHA protection.
- 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.
- 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
429threshold. - 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 Disabledfor the main project. - Monitor Supabase Auth logs and Turnstile Analytics for challenge failures,
Auth
429responses, 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.