AnnouncementsIntroducing Arkoren

    Menu

    CAPTCHA

    Verify browser tokens with Turnstile or Google reCAPTCHA using trusted hostname, action, and score policies before protected effects.

    What's on this page
    1. Choose a provider and configure reusable verifiers with server-owned credentials and hostnames.
    2. Obtain single-use browser tokens for the action your server expects.
    3. Verify typed JSON submissions once and apply an explicit Google risk-score policy.
    4. Use header middleware as an alternative and distinguish rejection from provider failure.
    On this page

    A feedback form can receive automated submissions even when every field passes validation. CAPTCHA adds a provider-issued browser token that your server verifies before accepting the feedback. An accepted token is an abuse signal, not proof of identity or permission; it does not replace CSRF protection.

    Arkoren ships vendor-local verifiers and optional route middleware. There is no core CAPTCHA contract, and the two providers’ tokens are not interchangeable.

    Provider Choice

    Choose Cloudflare Turnstile for an accepted-or-rejected token check. Choose Google reCAPTCHA when your application needs a risk assessment and owns the decision to accept, deny, or require more verification. A valid Google token can have a low score: direct Verify returns that assessment successfully. Only the optional middleware applies a configured minimum score automatically.

    Compatibility Matrix

    This comparison covers Arkoren’s server-side adapters, not every vendor feature or a guarantee that a visitor is human.

    Provider Hostname Allowlist Expected Action Risk Score Route Middleware Middleware Score Policy
    Cloudflare Turnstile Yes Yes No Yes No
    Google reCAPTCHA Yes Yes Yes Yes

    Both adapters require an allowlist from trusted deployment configuration. Supply bare hostnames, not URLs, wildcards, or a list derived from the incoming Host header. Neither implicitly trusts subdomains. Turnstile compares case-sensitively; Google compares case-insensitively.

    Choose an action literal on the server, such as feedback, and use it in the browser integration too. Empty ExpectedAction or middleware Action skips the local action comparison. Do not copy the expected value from the submitted body.

    Create one verifier during application setup, handle construction errors before mounting routes, and reuse it concurrently. Neither verifier has lifecycle or readiness hooks. Keep secrets and API credentials on the server; browser site keys are public.

    Cloudflare Turnstile

    Create a Turnstile widget for your deployment’s hostnames. Store its secret in server configuration and use its public site key in the browser. From your application module, install:

    go get arkoren.dev/driver/cloudflare

    Server Setup

    Import arkoren.dev/driver/cloudflare/cloudflarecaptcha. With secret loaded from server configuration, construct the verifier:

    verifier, err := cloudflarecaptcha.New(secret, []string{"app.example.com"})

    Construction validates local settings without network I/O. A blank secret, empty allowlist, or invalid hostname returns ErrConfiguration; handle err before using verifier. Verification uses Turnstile’s HTTPS Siteverify endpoint.

    The default verification deadline is ten seconds, including response reading. If feedback needs a shorter budget, import time and use this alternative construction:

    verifier, err := cloudflarecaptcha.New(secret, []string{"app.example.com"},
    	func(connection *cloudflarecaptcha.Builder) {
    		connection.Timeout(2 * time.Second)
    	})

    An earlier caller deadline still wins. Zero restores ten seconds; negative timeouts fail construction. Builder.HTTPClient can borrow an existing *http.Client: construction snapshots its fields, disables redirects in the snapshot, and retains its transport and cookie jar without modifying your client. A shorter native timeout still applies. Configure shared objects before concurrent use; the verifier closes neither client nor transport.

    Verification makes one attempt and does not send Siteverify’s optional idempotency key. Do not add a retrying transport: a timeout can mean the token was consumed. Empty tokens or tokens over 2,048 bytes reject before I/O. Nil error means provider success, a nonzero challenge timestamp, an allowed hostname, and a matching action when requested. Returned CData remains untrusted, potentially sensitive client-originated metadata, not a permission claim or risk score.

    Formatting a verifier value or pointer with %v, %+v, or %#v emits [REDACTED Turnstile verifier] without traversing its borrowed objects. This is not a general sanitization boundary for reflection, serialization, invalid format verbs, or native objects, and does not apply to the Google verifier. There is no explicit secret-zeroization operation.

    Browser Token

    Define the token callback before loading the widget script. Replace TURNSTILE_SITE_KEY with your public key:

    <script>
      function feedbackToken(token) {
        // Attach token as captcha_token to the pending feedback submission.
      }
    </script>
    <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
    <div class="cf-turnstile" data-sitekey="TURNSTILE_SITE_KEY"
         data-action="feedback" data-callback="feedbackToken"></div>

    The callback receives a credential, not verification success. Attach it to your submission and wait for the server’s result. Clear stored tokens on widget error or expiry and reset the widget before another attempt. Traditional forms instead receive a cf-turnstile-response field; decode that representation explicitly rather than treating it as JSON.

    Turnstile tokens expire after five minutes and are single-use. Continue with JSON verification, or choose header middleware instead; do not verify the same token in both places.

    For constructor and evidence details, see the Cloudflare CAPTCHA API reference.

    Google reCAPTCHA

    Create a Google Cloud project, enable the reCAPTCHA Enterprise API, and create a score-based website key for your hostnames. Use that project and site key on the server. Install:

    go get arkoren.dev/driver/gcp

    Server Setup

    Configure Application Default Credentials (ADC) for assessment creation, with roles/recaptchaenterprise.agent on the project. Import arkoren.dev/driver/gcp/gcpcaptcha. With the setup context ctx, trusted projectID, and public siteKey, construct the verifier:

    verifier, err := gcpcaptcha.New(ctx, projectID, siteKey, []string{"app.example.com"})

    Construction validates settings and loads credentials; it does not create an assessment or perform readiness checks. ADC discovery can do credential-loading I/O, and its synchronous discovery call cannot be interrupted by ctx. The constructor checks cancellation around discovery and uses ctx for quota-project lookup, but does not retain it. Later native token refresh uses each verification request’s bounded context; asynchronous background refresh is disabled.

    If you use a restricted server API key instead of ADC, pass it in this alternative construction. apiKey is not the public browser site key:

    verifier, err := gcpcaptcha.New(ctx, projectID, siteKey, []string{"app.example.com"},
    	func(access *gcpcaptcha.Builder) {
    		access.APIKey(apiKey)
    	})

    This bypasses ADC and sends the key as X-Goog-Api-Key, never in the URL. Keep the key and credential-bearing construction draft out of logs.

    Borrowed Clients

    If your application already owns an authenticated *http.Client named client, select it instead of ADC:

    verifier, err := gcpcaptcha.New(ctx, projectID, siteKey, []string{"app.example.com"},
    	func(access *gcpcaptcha.Builder) {
    		access.HTTPClient(client)
    	})

    A nonnil client bypasses ADC completely. Without APIKey, its transport must authenticate requests. If you intentionally configure both, the adapter adds the API-key header without removing transport authentication. A nil client restores normal ADC/API-key selection.

    Construction snapshots the client, discards its jar, and disables redirects. Its transport remains borrowed and is never closed. Positive client timeouts shorter than ten seconds are retained; zero, negative, or longer timeouts become ten seconds. Each verification, including ADC token refresh, also has a ten-second context limit and honors an earlier caller deadline. Custom transports must be concurrency-safe, honor cancellation, and never retry tokens. The builder has no timeout or score-threshold setting; use a shorter caller context and choose score policy separately.

    This adapter uses projects.assessments.create (CreateAssessment), not legacy Siteverify or a shared-secret flow. It makes one attempt. Empty tokens and tokens over 64 KiB reject locally; that size is an application guard, not a documented Google maximum. Successful assessments expose Score, Name, and reason/evidence fields. They do not authenticate a visitor or expose other Fraud Defense APIs.

    Browser Token

    Load the Enterprise script with your public score-based site key:

    <script src="https://www.google.com/recaptcha/enterprise.js?render=RECAPTCHA_SITE_KEY"></script>

    In your existing feedback-submit callback, execute the matching action. Replace the key here with the same value:

    grecaptcha.enterprise.ready(async () => {
      const token = await grecaptcha.enterprise.execute("RECAPTCHA_SITE_KEY", {
        action: "feedback",
      });
      // Attach token as captcha_token to this feedback submission.
    });

    Handle script-load and execution errors in your UI; do not submit as though verification succeeded. Generate the token at the interaction, not when the page first opens. Google tokens expire after two minutes and can be assessed only once. Obtain a fresh token after expiry or a failed submission. Continue with JSON verification or the alternative header middleware.

    For authentication and assessment fields, see the Google CAPTCHA API reference.

    JSON Verification

    For a token inside JSON, let the handler decode the body once and verify the token before its effect. Apply body limits, content-type checks, and rate limiting before external verification; request.JSON does not enforce Content-Type itself. See middleware ordering when placing those checks.

    Use one provider branch below, not both. Neither adapter retries automatically, and custom HTTP transports must not replay a verification. If a response is lost, the token might already be spent: stop the effect and ask for a fresh token on a later attempt. A fresh token does not deduplicate feedback or any other business operation; application-owned idempotency remains separate.

    Map Failures

    Direct Verify returns plain classified errors, not HTTP problems. Before registering a Turnstile handler, map them with arkoren.dev/core/http/middleware/errormap, arkoren.dev/foundation/problem, and net/http. Here routes is your existing router:

    routes.Use(errormap.Middleware(
    	errormap.Map(cloudflarecaptcha.ErrRejected, problem.Forbidden()),
    	errormap.Map(cloudflarecaptcha.ErrUnavailable, problem.New(func(details *problem.Builder) {
    		details.Status(http.StatusServiceUnavailable)
    	})),
    	errormap.Map(cloudflarecaptcha.ErrConfiguration, problem.InternalServerError()),
    ))

    For Google, replace the three sentinel qualifiers with gcpcaptcha. Rejection then returns 403, provider unavailability 503, and configuration failure 500, without publishing provider diagnostics. Without mapping, a plain rejection gets the router’s generic 500, not 403. Cancellation and deadline causes remain identifiable with errors.Is; they are operational failures, not bad tokens.

    Both adapters keep native diagnostics out of their Error() text. Original causes remain inspectable with errors.Is and errors.As, however, and can contain confidential transport or provider data. Do not dump unwrapped causes or assume arbitrary formatting and serialization of wrappers sanitizes them.

    Decode Input

    Define the feedback request separately from provider input. JSON tags describe the browser’s representation, not verification policy:

    type FeedbackInput struct {
    	Message      string `json:"message"`
    	CAPTCHAToken string `json:"captcha_token"`
    }

    With request imported from arkoren.dev/core/http/request and incoming the handler’s *http.Request, decode once:

    input, err := request.JSON[FeedbackInput](incoming)
    if err != nil {
    	return err
    }

    Validate the message using your application’s input rules before verification. Keep CAPTCHAToken out of request-body logging, traces, and validation diagnostics.

    Verify Turnstile

    Using the Turnstile verifier from setup, require the server-owned feedback action and stop on every verification error:

    _, err = verifier.Verify(incoming.Context(), cloudflarecaptcha.Input{
    	Token:          input.CAPTCHAToken,
    	ExpectedAction: "feedback",
    })
    if err != nil {
    	return err
    }
    // Accept the validated feedback here.

    Only nil error permits the feedback operation to proceed. The hostname check always applies; the action check applies because you supplied a nonempty action.

    Evaluate Scores

    For Google, choose a finite feedbackMinScore in [0, 1] from your site’s observations and validate it when loading server configuration. There is no universal safe threshold. Higher scores indicate lower assessed risk. Replace the Turnstile block with:

    assessment, err := verifier.Verify(incoming.Context(), gcpcaptcha.Input{
    	Token:          input.CAPTCHAToken,
    	ExpectedAction: "feedback",
    })
    if err != nil {
    	return err
    }
    if assessment.Score < feedbackMinScore {
    	return problem.Forbidden()
    }
    // Accept the validated feedback here.

    The operation runs only after token verification and score policy succeed. Explicit zero accepts every valid score, but still requires valid token evidence, an allowed hostname, the requested action, and a present risk-analysis message. An omitted or null score inside a present message means zero under ProtoJSON; a positive threshold denies it as policy, not a verification outage. A missing risk-analysis message instead fails verification.

    For login, direct verification can support a step-up decision instead of a flat denial. After checking credentials and verifying once with ExpectedAction: "login", a low score can start an application-owned pending factor challenge. That branch must not create an authenticated session. Verify the extra factor on a separate endpoint before signing in. A high score never replaces factors your authentication policy always requires.

    Optional Input.RemoteIP is a netip.Addr; supply it only from your trusted client-address resolver, never directly from arbitrary forwarded headers. Google also accepts optional Input.UserAgent. Sending these fields discloses visitor data to the provider; omit them unless your privacy policy permits and needs that disclosure. The examples omit both.

    Header Middleware

    If the token travels separately in a header, use middleware instead of handler verification. It extracts and verifies once, then calls the handler only on success. Neither adapter automatically parses JSON or forms, stores evidence in request context, or infers client IP; Google middleware also infers no user agent. Use direct verification when you need an assessment for a step-up decision.

    Keep body limits, content-type checks, and rate budgets outside CAPTCHA middleware. For cross-origin browser requests, CORS must permit the token header. With net/http imported, define a body-preserving extractor:

    extract := func(incoming *http.Request) (string, error) {
    	return incoming.Header.Get("X-CAPTCHA-Token"), nil
    }

    For an existing Turnstile verifier and application handler feedback, mount:

    routes.With(cloudflarecaptcha.Middleware(verifier, extract,
    	func(policy *cloudflarecaptcha.MiddlewareBuilder) {
    		policy.Action("feedback")
    	})).Post("/feedback", feedback)

    For Google, use this alternative and explicitly configure the minimum score:

    routes.With(gcpcaptcha.Middleware(verifier, extract,
    	func(policy *gcpcaptcha.MiddlewareBuilder) {
    		policy.Action("feedback")
    		policy.MinScore(feedbackMinScore)
    	})).Post("/feedback", feedback)

    The handler must not verify that token again. Google requires a finite minimum in [0, 1]; omission is a configuration error, not a default threshold.

    Both middleware adapters return safe HTTP problems: rejection is 403, including a Google score below the minimum; provider unavailability is 503; configuration failure is 500. Ordinary extractor errors reject with 403. Extractor errors wrapping cancellation, deadline, or unavailability return 503 without adding a token-rejection identity. Configuration takes precedence over other classes. Original causes remain inspectable, but their text is not rendered to the client.

    Turnstile panics during wiring for a nil verifier, extractor, next handler, or construction callback. A zero verifier instead fails requests with configuration errors. Google reports a nil verifier/extractor or missing/invalid score policy as 500 on requests without extracting or verifying. Nil construction callbacks panic in both adapters. These are configuration faults, not permission to bypass the check.

    Rejection Tests

    Inject a controlled *http.Client through each provider’s Builder.HTTPClient for deterministic responses. Google tests with a supplied client bypass ADC; they do not verify live credentials or IAM permissions. Neither adapter has a memory driver or local emulator. A stub proves application decisions, not a genuine browser challenge.

    Test accepted and missing tokens, rejection, hostname/action mismatches, unavailability, and cancellation. For Google, include the exact score boundary, a valid score below it, ProtoJSON’s zero score, and missing middleware MinScore. Assert that every failure leaves the feedback effect untouched. If login uses a step-up branch, assert that it creates only a pending challenge, never a session. Keep real-browser and live-provider checks separate from deterministic tests.

    For complete contracts, see the Cloudflare CAPTCHA API reference and Google CAPTCHA API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive