Serve the Turnstile/reCAPTCHA siteverify contract, check the secret, bind tokens to their origin - #28
Merged
Merged
Conversation
…ecret, bind tokens to their origin
Adds POST /turnstile/v0/siteverify, /recaptcha/api/siteverify and /siteverify to
all three servers, accepting form-encoded and JSON bodies with secret, response,
remoteip and idempotency_key, and answering the upstream shape and error-code
vocabulary. It is an adapter over the existing verifyToken in each server, not a
second verification path.
The reason to have it is adoption: every backend SDK, CMS plugin and snippet
written for those services speaks this contract already, so pointing an existing
integration at FCaptcha becomes a base-URL change rather than a rewrite.
Three things the native endpoint did not do, which the contract requires:
- `secret` is now checked. All three servers read it out of the body and
dropped it, so anyone who could reach /api/token/verify could spend a token
— and since tokens are single-use, burn a real visitor's. The README has
always documented sending it, which made the omission worse: integrators
believed they were authenticated. FCAPTCHA_VERIFY_SECRET splits the verify
credential from the signing key; FCAPTCHA_LEGACY_UNAUTH_VERIFY restores the
old behaviour for one release.
- Tokens carry a signed hostname, action and cdata. The hostname is derived
server-side from Origin (then Referer), never from the request body, so a
caller that could state its own hostname cannot. This is what lets a backend
reject a token minted on a site that lifted the key, or minted for a
different action. FCAPTCHA_ALLOWED_HOSTNAMES optionally enforces it at mint
time; a request with no derivable origin still passes, because a native or
server-side client legitimately has none and anyone able to forge an Origin
would forge a listed one.
- idempotency_key makes a retry safe. Keyed on the key and the token together:
reusing one key across different tokens is a caller bug, and answering the
second from the first's cache would report success for a token nobody
validated.
Fixes a cross-server token format divergence found while building this. Go
encoded padded base64url, Node unpadded, and Python signed a payload with
json.dumps' default separators — a space after every ':' and ','. No two
implementations could verify each other's tokens. Nothing caught it because each
server only ever verified its own, and no test crossed the boundary; siteverify
is exactly where it would have started biting, since a backend may validate
against an instance that did not mint the token. All three now emit unpadded
base64url over compact sorted-key JSON and accept the old encodings, so rolling
deploys and in-flight tokens survive. Byte-identical fixtures in the three test
files pin the shared format.
Old four-key tokens still verify: the signature covers whatever keys a token
carries, so this is additive rather than a format break.
Tests: 34 Node, 14 Go, 30 Python covering hostname derivation, the allowlist,
label sanitising, the secret gate, error-code mapping, idempotent replay and the
token round-trip. The E2E suite now sends the secret and asserts a wrong one is
refused. Verified live that all three servers return identical siteverify
responses for valid tokens, every error case, replay, idempotent retry and
legacy tokens.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
POST /turnstile/v0/siteverify,/recaptcha/api/siteverifyand/siteverifyto all three servers, accepting form-encoded and JSON bodies and answering the upstream response shape and error-code vocabulary. An existing Turnstile/reCAPTCHA/hCaptcha backend integration now works against FCaptcha by changing the base URL.Three changes the contract requires:
secretis checked. All three servers previously read it out of the body and ignored it, so any caller who could reach/api/token/verifycould spend a token.FCAPTCHA_VERIFY_SECRETsplits the verify credential from the signing key;FCAPTCHA_LEGACY_UNAUTH_VERIFYrestores the old behaviour for one release.hostname,actionandcdata. The hostname is derived server-side fromOrigin(thenReferer), so a backend can reject a token minted on a site that lifted the key or minted for a different action.FCAPTCHA_ALLOWED_HOSTNAMESoptionally enforces it at mint time.idempotency_keylets a retried validation return the first answer instead of tripping the single-use guard.Also fixes a cross-server token format divergence found while building this: Go encoded padded base64url, Node unpadded, and Python signed a payload with different JSON separators, so no two implementations could verify each other's tokens. All three now emit unpadded base64url over compact sorted-key JSON and accept the old encodings, so rolling deploys and in-flight tokens are unaffected. Byte-identical fixtures in the three test files pin the format.
Old four-key tokens still verify — the signature covers whatever keys a token carries.
Breaking
/api/token/verifynow requiressecret. See Upgrading to 1.22.0.Tests
34 Node, 14 Go, 30 Python covering hostname derivation, the allowlist, sanitising, the secret gate, error-code mapping, idempotent replay and the token round-trip. E2E suite updated and passing (104/104). Verified live that all three servers return identical siteverify responses across valid tokens, every error case, replay, idempotent retry and legacy tokens.