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

111 lines
5.5 KiB
Markdown

# 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
- [Supabase CAPTCHA protection](https://supabase.com/docs/guides/auth/auth-captcha)
- [Supabase Auth rate limits](https://supabase.com/docs/guides/auth/rate-limits)
- [Cloudflare explicit rendering](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/)
- [Cloudflare CSP requirements](https://developers.cloudflare.com/turnstile/reference/content-security-policy/)
- [Cloudflare test keys](https://developers.cloudflare.com/turnstile/troubleshooting/testing/)