Google reCAPTCHA v3 provider

Google reCAPTCHA v3 provider implementing captcha.Provider against the Google siteverify API.

Overview

reCAPTCHA v3 is invisible. There is no checkbox and no image grid. It runs in the page, bound to the site key, and returns a risk score between 0.0 (likely bot) and 1.0 (likely human) on every verification. Piko reads that score, compares it against a threshold, and rejects the action when the score falls short. The default threshold is 0.5.

The provider implements the same captcha.Provider port as hCaptcha and Turnstile. Swapping to reCAPTCHA v3 changes only the registration. The action code stays the same, because the score and the pass/fail decision come from the captcha service, not from the provider.

Verification calls hit www.google.com/recaptcha/api/siteverify with a 10-second timeout and a 64 KiB response cap. The provider checks the response content type, rejects an over-limit body, and traces every call through OpenTelemetry. When a verification succeeds but reports a zero score, the provider normalises it to 1.0, because a confirmed pass cannot also be a confirmed bot. All public methods are safe for concurrent use.

How the score becomes a decision

The browser binds the action label, and the server does not forward it. The injected init script calls grecaptcha.execute(siteKey, {action: ...}), and Google echoes that label back in the siteverify response. Piko compares the echoed action against the action it expected for the request. When they differ, verification fails.

The score drives the gate. Piko's captcha service compares the returned score against the threshold. A score below the threshold fails the action with ErrScoreBelowThreshold before your Call method runs. This is the defining concern for a score-based provider. You can register it correctly and a threshold set too high for your traffic still turns legitimate users away. Tune the default across the deployment with piko.WithCaptcha, or override it per action with CaptchaConfig.ScoreThreshold.

The accepted score is available to the action through Request().CaptchaScore, so you can branch on the confidence level instead of treating verification as a single boolean.

Frontend wiring

Piko handles the browser side. The provider returns the script URLs, the Content Security Policy domains, the invisible-widget flag, and an init script, so you write no JavaScript. Piko injects the reCAPTCHA SDK, sets the required CSP entries, and registers the init script that reads data-captcha-provider, data-captcha-sitekey, and data-captcha-action off the hidden token input. The script writes the token back into that input and refreshes it every 90 seconds, because reCAPTCHA v3 tokens are short-lived.

To score the same site key differently per call site, set a distinct data-captcha-action value on each form (login, signup, submit-comment). The label flows to Google in the browser and back to piko for the action check.

Requirements

  • A Google reCAPTCHA v3 site registered in the reCAPTCHA admin console. Site key and secret key come from the same console.
  • Server egress to www.google.com for token verification.
  • Browser reach to www.google.com, www.gstatic.com, and www.recaptcha.net for the script, supporting assets, and frames. reCAPTCHA does not work behind hard egress restrictions to Google services.

Configuration

import (
    "piko.sh/piko/wdk/captcha/captcha_provider_recaptcha_v3"
)

provider, err := captcha_provider_recaptcha_v3.NewProvider(captcha_provider_recaptcha_v3.Config{
    SiteKey:   "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI", // required, public site key
    SecretKey: "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe", // required, server-side secret
})
if err != nil {
    return err
}

NewProvider fails fast when either key is empty, so a misconfiguration surfaces at boot instead of on the first request. The pair above are Google's published test keys. Their verification always succeeds. Swap for production keys before deploying.

Bootstrap

ssr := piko.New(
    piko.WithCaptchaProvider("recaptcha", provider),
    piko.WithDefaultCaptchaProvider("recaptcha"),
)

To set the deployment-wide score threshold, add the captcha settings:

ssr := piko.New(
    piko.WithCaptchaProvider("recaptcha", provider),
    piko.WithDefaultCaptchaProvider("recaptcha"),
    piko.WithCaptcha(piko.CaptchaOptions{ScoreThreshold: 0.7}),
)

A ScoreThreshold of 0 keeps the 0.5 default.

Usage

Mark an action as captcha-protected by implementing CaptchaConfig:

func (SubmitAction) CaptchaConfig() *piko.CaptchaConfig {
    return &piko.CaptchaConfig{Provider: "recaptcha"}
}

Override the threshold for one sensitive action without changing the global default:

func (SubmitAction) CaptchaConfig() *piko.CaptchaConfig {
    return &piko.CaptchaConfig{Provider: "recaptcha", ScoreThreshold: 0.8}
}

The captcha service runs verification before the action executes. Read the accepted score inside Call through Request().CaptchaScore, which is a *float64 and nil when no score applies.

See also

Other captcha providers:

Framework docs:

External: