Skip to content

feat: add turnstile, hcaptcha, and recaptcha challenge types - #75

Closed
fionera wants to merge 2 commits into
masterfrom
feat/captcha-validators
Closed

fionera wants to merge 2 commits into
masterfrom
feat/captcha-validators

Conversation

@fionera

@fionera fionera commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

First of three PRs adding captcha challenge types (plan: server → web → integration/e2e).

Adds turnstile, hcaptcha, and recaptcha (v2 checkbox) as additional validation types alongside none and pow, backed by one shared validator_captcha.go since all three providers use the same sitekey → widget token → siteverify flow.

  • Challenge GET: serves a static per-level JSON body {"c","t","k"} with the sitekey, precomputed on first use (no per-request state needed — security binding stays in the existing cookie HMAC).
  • Challenge POST: verifies the widget token against the provider's siteverify endpoint (form-encoded secret/response/remoteip), requires success and a hostname match against the request identity (subdomains accepted, since trusted_domains may collapse the identity host to a suffix), then issues the cookie via the existing ToCookie.
  • Fail closed on provider errors/timeouts, with distinct sentinel errors for logging.
  • New Berghain.HTTPClient (5s default timeout, injectable) — this is the first validator doing network I/O; the hot validate path is untouched.
  • Config: type: turnstile|hcaptcha|recaptcha + sitekey/secret (+ optional verify_url override), validated at startup with Fatal like the existing countdown check.
  • Web protocol numbering skips t:2, which the challenge page capability checks already reserve for worker-based POW; captcha types are t:3/4/5.

The browser side (widget rendering for t:3–5) and the HAProxy example timeout split land in follow-up PRs.

Test plan

  • go test ./... — new tests cover GET body shape, POST happy path against an httptest siteverify stub (asserting all form fields), subdomain hostname match, rejected token, hostname mismatch, provider 5xx + dead provider (fail closed), empty/oversized bodies, and a hostname-matching table test.
  • go vet + staticcheck v0.6.1 (CI version) clean.
  • Manually booted cmd/spop with the updated example config (turnstile level with Cloudflare dummy keys) — starts and listens; a captcha level missing its secret refuses to start with a clear error.

🤖 Generated with Claude Code

Adds three new validation types alongside none and pow, backed by one
shared captcha validator. The challenge GET serves a static per-level
body carrying the sitekey; the POST exchanges the widget response token
for a cookie after verifying it against the provider siteverify
endpoint (fail closed) and binding the reported hostname to the request
identity, accepting subdomains since trusted_domains may collapse the
identity host to a suffix.

The web protocol type numbering skips t:2, which the challenge page
capability checks already reserve for a worker-based POW.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Provider test keys report a fixed hostname unrelated to the page, so
tests cannot pass the hostname binding. Adds an explicit opt-out for
captcha levels, documented as test-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fionera

fionera commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

Consolidated into #78.

@fionera fionera closed this Jul 11, 2026
@fionera
fionera deleted the feat/captcha-validators branch July 11, 2026 01:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant