Skip to content

Add Cloudflare Turnstile support - #662

Open
NuLL3rr0r wants to merge 1 commit into
netresearch:mainfrom
NuLL3rr0r:feature/cloudflare-turnstile
Open

Add Cloudflare Turnstile support#662
NuLL3rr0r wants to merge 1 commit into
netresearch:mainfrom
NuLL3rr0r:feature/cloudflare-turnstile

Conversation

@NuLL3rr0r

Copy link
Copy Markdown

Description

Adds optional Cloudflare Turnstile protection to the password change and password reset flows.

When configured, Turnstile is displayed on the change password, forgot password, and reset password forms. The generated token is verified server-side with Cloudflare before the corresponding RPC request is processed.

Turnstile remains disabled when no site key and secret are configured. Verification requests use a configurable timeout and fail closed if verification fails or Cloudflare cannot be reached.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🔧 Configuration change
  • ♻️ Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🔒 Security fix
  • 🎨 UI/UX improvement
  • ✅ Test coverage improvement

Related Issues

N/A

Changes Made

  • Added optional Cloudflare Turnstile widgets to the change password, forgot password, and reset password forms.
  • Added server-side Turnstile token verification before password-related RPC requests are processed.
  • Added configuration for the Turnstile site key, secret, and verification timeout.
  • Added CSP rules required for Cloudflare Turnstile when enabled.
  • Turnstile remains completely disabled when not configured.

Testing

Test Environment

  • OS: FreeBSD 15.1
  • Go version: go version go1.26.6 freebsd/amd64
  • Browser: Chromium-based browser

Test Cases

  • Unit tests pass (go test ./...)
  • Integration tests pass (if applicable)
  • Manual testing completed
  • Tested with different LDAP servers (if applicable):
    • Active Directory
    • OpenLDAP
    • FreeIPA
    • Other:

Test Results

Manually verified:

  • Turnstile widget renders when configured.
  • Password change succeeds with successful Turnstile verification.
  • Forgot-password flow succeeds with successful Turnstile verification.
  • Requests without a Turnstile token are rejected server-side.
  • Direct RPC requests cannot bypass Turnstile verification.
  • Turnstile is not rendered when it is not configured.

Example direct RPC request without a Turnstile token:

$ curl -X POST https://pwd.example.com.com/api/rpc \
  -H 'Content-Type: application/json' \
  -d '{"method":"change-password","params":["foo","bar","baz"]}'

{"success":false,"data":["Turnstile verification failed"]}

$ curl -X POST https://pwd.example.com.com/api/rpc \
  -H 'Content-Type: application/json' \
  -d '{"method":"request-password-reset","params":["foo","bar","baz"]}'

{"success":false,"data":["Turnstile verification failed"]}

$ curl -X POST https://pwd.example.com.com/api/rpc \
  -H 'Content-Type: application/json' \
  -d '{"method":"reset-password","params":["foo","bar","baz"]}'

{"success":false,"data":["Turnstile verification failed"]}

$ curl -X POST https://pwd.example.com.com/api/rpc \
  -H 'Content-Type: application/json' \
  -d '{"method":"change-password","params":["fake-reset-token","fake-new-password"]}'

{"success":false,"data":["Turnstile verification failed"]}

$ curl -X POST https://pwd.example.com.com/api/rpc \
  -H 'Content-Type: application/json' \
  -d '{"method":"request-password-reset","params":["fake-reset-token","fake-new-password"]}'

{"success":false,"data":["Turnstile verification failed"]}

$ curl -X POST https://pwd.example.com.com/api/rpc \
  -H 'Content-Type: application/json' \
  -d '{"method":"reset-password","params":["fake-reset-token","fake-new-password"]}'

{"success":false,"data":["Turnstile verification failed"]}

Signed-off-by: Mamadou Babaei <info@babaei.net>
@NuLL3rr0r
NuLL3rr0r requested a review from CybotTM as a code owner August 21, 2026 13:58

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@NuLL3rr0r NuLL3rr0r changed the title Add Cloudflare Turnstile supportimplement cloudflare turnstile support Add Cloudflare Turnstile support Aug 21, 2026
@NuLL3rr0r
NuLL3rr0r force-pushed the feature/cloudflare-turnstile branch from 75ee3f5 to 2c716ff Compare August 21, 2026 15:57
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 34.24658% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.56%. Comparing base (efe76d0) to head (936a651).

Files with missing lines Patch % Lines
internal/turnstile/turnstile.go 0.00% 29 Missing ⚠️
main.go 23.07% 10 Missing ⚠️
internal/rpchandler/handler.go 12.50% 7 Missing ⚠️
internal/options/app.go 90.90% 2 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
e2e 89.56% <34.24%> (-3.40%) ⬇️
integration 89.56% <34.24%> (-3.40%) ⬇️
unittests 89.56% <34.24%> (-3.40%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
cmd ∅ <ø> (∅)
internal 91.12% <36.66%> (-3.17%) ⬇️
@@            Coverage Diff             @@
##             main     #662      +/-   ##
==========================================
- Coverage   92.95%   89.56%   -3.40%     
==========================================
  Files          21       22       +1     
  Lines        1193     1265      +72     
==========================================
+ Hits         1109     1133      +24     
- Misses         84      132      +48     
Files with missing lines Coverage Δ
internal/web/templates/templates.go 96.49% <100.00%> (+0.06%) ⬆️
internal/options/app.go 97.57% <90.90%> (-0.48%) ⬇️
internal/rpchandler/handler.go 61.76% <12.50%> (-6.57%) ⬇️
main.go 79.65% <23.07%> (-4.73%) ⬇️
internal/turnstile/turnstile.go 0.00% <0.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread internal/rpchandler/handler.go Outdated
h.opts.CfTurnstileSecret,
body.TurnstileToken,
clientIP,
time.Duration(h.opts.CfTurnstileTimeoutSeconds)*time.Second,

@CybotTM CybotTM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — bot protection on a public password-reset flow is a legitimate complement to the per-IP limiter, and the shape here is right: genuinely opt-in (templates, CSP and handler all gate on the keys), fail-closed, and all three RPC methods are covered; the CSP additions match Cloudflare's documented requirements and only apply when enabled. Verdict: request changes — two of them are load-bearing:

  1. Critical — the default verification timeout exceeds the server's write timeout. CF_TURNSTILE_TIMEOUT_SECONDS defaults to 15 (internal/options/app.go:452) but Fiber runs with WriteTimeout: 10s (main.go:36/237). A slow siteverify response means the client gets a dropped connection instead of the intended 403, and each in-flight request pins a handler goroutine for up to 15s. Please default to ≤5s and bound the flag (the repo's checkUintMax pattern — which also resolves the gosec G115 finding at handler.go:112 properly, without a nolint).
  2. Critical — verification runs before rate limiting. turnstile.Verify fires in Handle before method dispatch (handler.go:106–116), while the per-IP limiter lives inside the method handlers — so junk tokens trigger an unmetered outbound HTTPS call to Cloudflare per request, and none of those requests count toward the limit. Check the IP limiter first (or verify inside the already-limited paths).
  3. Required — tests: internal/turnstile has no test file and nothing exercises the feature (codecov gates are red). Make the siteverify endpoint injectable, then cover success / success:false / non-200 / timeout / malformed JSON, plus handler-level 403-when-token-missing for all three methods and disabled-mode passthrough.
  4. Required — observability of fail-closed: the verification error is discarded and Cloudflare's error-codes are never read — a wrong secret is indistinguishable from an outage; every request 403s and the operator is blind. Log the error server-side (error-codes are not PII), keep the client message generic, and document that a Cloudflare outage means password changes are unavailable until Turnstile is disabled.
  5. Required — accessible failure: submitting with the widget present but unsolved silently re-enables the form (app.ts:232–235 and siblings) — nothing announced to screen readers, which breaks WCAG 3.3.1 in an AAA repo. Surface it via setSubmitError/role="alert" (or disable submit until the token callback).
  6. Required — CI/housekeeping: fix the five golangci-lint findings (noctx at turnstile.go:34, package comment, two ST1005 error strings, plus G115 via point 1); extract the triplicated Turnstile block in the three TS files into one shared helper (that alone clears the SonarCloud 12.9%-duplication gate); bunx prettier --write .; reword the commit to Conventional Commits with Signed-off-by (the DCO check is red).
  7. Required — docs in the same PR: README + .env.local.example for the three new env vars, docs/api-reference.md for the new turnstileToken field, the CSP change, a privacy note (the widget sends visitor data to Cloudflare — relevant for self-hosted EU deployments), and the fail-closed behavior. Nit: CF_TURNSTILE_SITEKEY vs cf-turnstile-site-key — one word-splitting convention, CF_TURNSTILE_SITE_KEY.

One point is ours to decide, not yours: internal/web/AGENTS.md currently says "no third-party scripts", and docs/accessibility.md claims WCAG 2.2 AAA — an opt-in Cloudflare iframe needs a documented exception in both. We'll resolve that on our side; the asks above are independent of it.

@CybotTM

CybotTM commented Aug 26, 2026

Copy link
Copy Markdown
Member

Maintainer decision on the policy question from the review (#673): the third-party exception is granted — Turnstile can land. One addition to the review's asks: activation must be an explicit flag, not implied by the keys. Please add CF_TURNSTILE_ENABLED (default false): enabled=true without both keys → startup error; keys present but not enabled → startup warning. Rationale and ready-made wording for the internal/web/AGENTS.md amendment, the docs/accessibility.md scoping, and the README privacy/operational notes are in #673 — please include those doc changes in this PR. Everything else from the review stands unchanged.

Assisted by claude-code:claude-fable-5 — Session

@NuLL3rr0r

Copy link
Copy Markdown
Author

Thank you for the review and the feedback. I'll try to implement the suggested changes to the best of my ability and force push the changes to the same branch for another review once I think it's ready.

@NuLL3rr0r
NuLL3rr0r force-pushed the feature/cloudflare-turnstile branch 3 times, most recently from ab0c03f to a52ff7f Compare August 26, 2026 16:29
@sonarqubecloud

Copy link
Copy Markdown

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests labels Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation frontend tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants