A universal Go library for CAPTCHA verification supporting Google reCAPTCHA (v2 and v3) and Cloudflare Turnstile. Swap providers without changing application code.
- Unified interface —
captcher.Verifierabstracts over all providers - Google reCAPTCHA v2 — checkbox and invisible modes
- Google reCAPTCHA v3 — score-based verification with configurable threshold and action validation
- Cloudflare Turnstile — with action, hostname, and customer data (CData) support
- HTTP middleware for net/http, Gin, and Echo
- Functional options for clean configuration
- Context propagation — verification results available downstream via
captcher.FromContext()
go get github.com/leodeim/captcherpackage main
import (
"context"
"fmt"
"github.com/leodeim/captcher"
"github.com/leodeim/captcher/recaptcha"
"github.com/leodeim/captcher/turnstile"
)
func main() {
// reCAPTCHA v2
v2 := recaptcha.NewV2("YOUR_SECRET_KEY")
resp, err := v2.Verify(context.Background(), captcher.VerifyRequest{
Token: "token-from-client",
RemoteIP: "1.2.3.4", // optional
})
fmt.Println(resp.Success, err)
// reCAPTCHA v3 with score threshold
v3 := recaptcha.NewV3("YOUR_SECRET_KEY",
captcher.WithScoreThreshold(0.7),
captcher.WithExpectedAction("login"),
)
resp, err = v3.Verify(context.Background(), captcher.VerifyRequest{
Token: "token-from-client",
})
fmt.Println(resp.Success, resp.Score, err)
// Cloudflare Turnstile
ts := turnstile.New("YOUR_SECRET_KEY",
captcher.WithExpectedHostname("example.com"),
)
resp, err = ts.Verify(context.Background(), captcher.VerifyRequest{
Token: "token-from-client",
})
fmt.Println(resp.Success, resp.CData, err)
}All providers implement captcher.Verifier, so you can switch based on configuration:
func newVerifier(provider, secret string) captcher.Verifier {
switch provider {
case "recaptcha_v2":
return recaptcha.NewV2(secret)
case "recaptcha_v3":
return recaptcha.NewV3(secret, captcher.WithScoreThreshold(0.5))
case "turnstile":
return turnstile.New(secret)
default:
panic("unknown provider: " + provider)
}
}All middleware packages extract the token from (in order): HTTP header, form field, query parameter. The verification result is stored in the request context and accessible via captcher.FromContext().
import (
"github.com/leodeim/captcher"
"github.com/leodeim/captcher/middleware/stdhttp"
"github.com/leodeim/captcher/turnstile"
)
verifier := turnstile.New("YOUR_SECRET_KEY")
cfg := captcher.DefaultMiddlewareConfig(verifier)
cfg.SkipPaths = []string{"/health", "/ready"}
cfg.IPHeader = "X-Forwarded-For"
mux := http.NewServeMux()
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
resp := captcher.FromContext(r.Context())
// use resp.Success, resp.Score, etc.
})
handler := stdhttp.Middleware(cfg)(mux)
http.ListenAndServe(":8080", handler)import (
"github.com/gin-gonic/gin"
"github.com/leodeim/captcher"
"github.com/leodeim/captcher/middleware/ginmw"
"github.com/leodeim/captcher/recaptcha"
)
verifier := recaptcha.NewV3("YOUR_SECRET_KEY", captcher.WithScoreThreshold(0.5))
cfg := captcher.DefaultMiddlewareConfig(verifier)
r := gin.Default()
r.Use(ginmw.Middleware(cfg))
r.POST("/submit", func(c *gin.Context) {
resp := ginmw.VerifyResponseFromContext(c)
// or: resp := captcher.FromContext(c.Request.Context())
})import (
"github.com/labstack/echo/v4"
"github.com/leodeim/captcher"
"github.com/leodeim/captcher/middleware/echomw"
"github.com/leodeim/captcher/turnstile"
)
verifier := turnstile.New("YOUR_SECRET_KEY")
cfg := captcher.DefaultMiddlewareConfig(verifier)
e := echo.New()
e.Use(echomw.Middleware(cfg))
e.POST("/submit", func(c echo.Context) error {
resp := echomw.VerifyResponseFromContext(c)
// or: resp := captcher.FromContext(c.Request().Context())
return c.JSON(200, resp)
})| Field | Default | Description |
|---|---|---|
TokenHeader |
"X-Captcha-Token" |
HTTP header to read the token from |
TokenFormField |
"captcha_token" |
Form/query field to read the token from |
TokenQueryParam |
"" (disabled) |
Dedicated query parameter for the token |
IPHeader |
"" |
Header for client IP (e.g. "X-Forwarded-For") |
SkipPaths |
nil |
Exact paths to skip verification for |
Optional |
false |
If true, failed verification doesn't block the request |
Options are shared across all providers via functional options:
captcher.WithHTTPClient(client) // custom *http.Client
captcher.WithTimeout(30 * time.Second) // request timeout (default: 10s)
captcher.WithScoreThreshold(0.7) // reCAPTCHA v3 minimum score (default: 0.5)
captcher.WithExpectedAction("login") // reCAPTCHA v3 / Turnstile action validation
captcher.WithExpectedHostname("example.com") // hostname validationAll errors are sentinel values and can be checked with errors.Is():
resp, err := verifier.Verify(ctx, req)
if errors.Is(err, captcher.ErrScoreTooLow) {
// reCAPTCHA v3 score below threshold — resp.Score has the actual score
}
if errors.Is(err, captcher.ErrVerifyFailed) {
// verification failed — resp.ErrorCodes has provider-specific details
}| Error | Meaning |
|---|---|
ErrMissingToken |
Empty token provided |
ErrVerifyFailed |
Provider rejected the token (or hostname/action mismatch) |
ErrScoreTooLow |
reCAPTCHA v3 score below threshold |
ErrHTTPRequest |
HTTP-level failure (network error, non-200 status) |
ErrInvalidResponse |
Provider returned unparseable JSON |
ErrTimeout |
Request context was cancelled or timed out |
Unit tests use mock HTTP servers and run without network access:
go test ./...Integration tests hit the real provider APIs using official test credentials:
- Cloudflare Turnstile: dummy sitekeys and secret keys with deterministic pass/fail/duplicate outcomes
- Google reCAPTCHA v2: public test keys that always pass verification
- Google reCAPTCHA v3: uses the v2 test secret (same endpoint) to validate the HTTP flow end-to-end (score is not meaningful)
Integration tests require network access and are gated behind a build tag:
go test -tags integration ./...To run only integration tests:
go test -tags integration -run Integration ./...| Package | Unit Tests | Integration Tests |
|---|---|---|
captcher |
7 | — |
recaptcha |
15 | 11 (v2: 6, v3: 5) |
turnstile |
10 | 10 |
middleware/stdhttp |
9 | 6 |
middleware/ginmw |
10 | 6 |
middleware/echomw |
10 | 6 |
| Total | 61 | 39 |
captcher/
├── captcher.go # Verifier interface, types, errors, options
├── middleware.go # MiddlewareConfig, context helpers
├── internal/verify/ # Shared HTTP verification logic
├── recaptcha/ # Google reCAPTCHA v2 + v3
├── turnstile/ # Cloudflare Turnstile
├── middleware/
│ ├── stdhttp/ # net/http middleware
│ ├── ginmw/ # Gin middleware
│ └── echomw/ # Echo middleware
└── example/ # Usage examples
MIT