Bot Challenge Verifier

Server-side verifier for the modern privacy-preserving bot- challenge widgets: Cloudflare Turnstile, hCaptcha, and Google reCAPTCHA v3. The client-side widget produces a short-lived token; the server POSTs that token (along with the operator's secret + optionally the remote IP) to the provider's siteverify endpoint and inspects the verdict.

Why a verifier and not a heuristic — b.middleware.botGuard inspects User-Agent / Accept-Language / fetch-metadata for stale crawlers, but a determined adversary forges those bytes trivially. A widget-issued token is a cryptographic claim from the provider that the request originated from a human (or a passable approximation under reCAPTCHA-v3's score model).

The verifier:

- POSTs the token via b.httpClient — every outbound hop goes through b.ssrfGuard + the framework's DNS pinning, so a redirect to a cloud-metadata endpoint can't smuggle past the first-hop gate. Raw node:http / node:https / global fetch is never used. - Sends the secret in the POST body as application/x-www-form-urlencoded (Cloudflare's documented shape). The secret never appears in the URL, query string, headers, log lines, or audit metadata. - Refuses a token that is not a non-empty string under MAX_TOKEN_BYTES (4 KiB) — Cloudflare tokens cap around 2 KiB; a 1 MiB "token" is operator misuse or an attack. - Validates success === true AND (when configured) hostname-in-allowlist AND action-in-allowlist before returning. The provider's hostname / action fields are embedded in the token by the widget; operators using multi-domain or multi-action deployments allowlist the expected values to refuse cross-site token replay. - For reCAPTCHA-v3, exposes the score (0.0–1.0) on the success shape so the operator can threshold per-route. - Audits every verify call drop-silent via b.audit.safeEmit (action auth.bot_challenge.verify, outcome success / failure, metadata { provider, hostname?, ok, errorCodes? }). The token and secret NEVER appear in audit metadata; only the token's 8-char prefix surfaces, and only when the operator has opted into trace-level metadata.

Compose with b.authBotChallenge (the adaptive staircase gate) by passing the verifier's verify function as the staircase's challengeFn — failed-auth attempts ride the staircase up to the challenge stage, the operator renders the Turnstile widget, and the verifier validates the resulting token. The two primitives are deliberately separate concerns.

References: - Cloudflare Turnstile siteverify https://developers.cloudflare.com/turnstile/get-started/server-side-validation/ - hCaptcha siteverify https://docs.hcaptcha.com/#verify-the-user-response-server-side - reCAPTCHA v3 siteverify https://developers.google.com/recaptcha/docs/v3 - OWASP ASVS v5 §11.5 (bot-defense controls) - RFC 6749 §4.1.3 (application/x-www-form-urlencoded body conventions for OAuth-style endpoints)

b.auth.botChallenge.create(opts) #

stable0.11.25gdprsoc2
{
  secret:            string,    // provider-issued site secret — preserved verbatim
  provider:          string,    // "turnstile" | "hcaptcha" | "recaptcha-v3" (default "turnstile")
  httpClient:        Object,    // b.httpClient-shaped { request } — default: framework http-client
  timeoutMs:         number,    // wall-clock cap for the siteverify call (default 5_000; minimum 500)
  allowedHostnames:  string[],  // optional hostname allowlist — verify refuses tokens whose embedded hostname is absent
  allowedActions:    string[],  // optional action allowlist — verify refuses tokens whose embedded action is absent
  audit:             Object,    // optional b.audit-shaped sink; defaults to framework global b.audit
}

Build a server-side verifier for a bot-challenge widget token. Returns { verify(token, verifyOpts?) }. The factory throws on malformed opts; verify throws a typed BotChallengeError on any verification failure and resolves on success.

var verifier = b.auth.botChallenge.create({
  secret:            process.env.TURNSTILE_SECRET,
  provider:          "turnstile",
  allowedHostnames:  ["app.example.com"],
  allowedActions:    ["login", "signup"],
});

// In a login handler:
try {
  var verdict = await verifier.verify(req.body["cf-turnstile-response"], {
    remoteIp:         b.requestHelpers.clientIp(req),
    expectedAction:   "login",
  });
  // verdict.ok === true; verdict.hostname / verdict.action / verdict.challengeTs populated.
} catch (e) {
  // e instanceof b.auth.botChallenge.BotChallengeError
  // e.code === "bot-challenge/invalid-token" (or hostname-mismatch / timeout / etc.)
}

Last updated 2026-08-08T16:39:49.652Z by seeder.