Skip to content

Repository files navigation

captcher logo

captcher

A universal Go library for CAPTCHA verification supporting Google reCAPTCHA (v2 and v3) and Cloudflare Turnstile. Swap providers without changing application code.

Features

  • Unified interfacecaptcher.Verifier abstracts 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()

Install

go get github.com/leodeim/captcher

The core library (verifiers, options, context helpers, and the net/http middleware) has zero third-party dependencies. The Gin and Echo adapters live in separate modules so you only pull in a web framework if you actually use its adapter:

# only if you use the Gin adapter
go get github.com/leodeim/captcher/middleware/ginmw
# only if you use the Echo adapter
go get github.com/leodeim/captcher/middleware/echomw

Quick Start

Direct Verification

package main
import (
"context""fmt""github.com/leodeim/captcher""github.com/leodeim/captcher/recaptcha""github.com/leodeim/captcher/turnstile"
)
funcmain() {
// reCAPTCHA v2v2:=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 thresholdv3:=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 Turnstilets:=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)
}

Swap Providers at Runtime

All providers implement captcher.Verifier, so you can switch based on configuration:

funcnewVerifier(provider, secretstring) captcher.Verifier {
switchprovider {
case"recaptcha_v2":
returnrecaptcha.NewV2(secret)
case"recaptcha_v3":
returnrecaptcha.NewV3(secret, captcher.WithScoreThreshold(0.5))
case"turnstile":
returnturnstile.New(secret)
default:
panic("unknown provider: "+provider)
}
}

Middleware

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().

net/http

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)

Gin

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())
})

Echo

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())returnc.JSON(200, resp)
})

Middleware Configuration

FieldDefaultDescription
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")
SkipPathsnilExact paths to skip verification for
OptionalfalseIf true, failed verification doesn't block the request

Verifier Options

Options are shared across all providers via functional options:

captcher.WithHTTPClient(client) // custom *http.Clientcaptcher.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 validationcaptcher.WithExpectedHostname("example.com") // hostname validation

Error Handling

All errors are sentinel values and can be checked with errors.Is():

resp, err:=verifier.Verify(ctx, req)
iferrors.Is(err, captcher.ErrScoreTooLow) {
// reCAPTCHA v3 score below threshold — resp.Score has the actual score
}
iferrors.Is(err, captcher.ErrVerifyFailed) {
// verification failed — resp.ErrorCodes has provider-specific details
}
ErrorMeaning
ErrMissingTokenEmpty token provided
ErrVerifyFailedProvider rejected the token (or hostname/action mismatch)
ErrScoreTooLowreCAPTCHA v3 score below threshold
ErrHTTPRequestHTTP-level failure (network error, non-200 status)
ErrInvalidResponseProvider returned unparseable JSON
ErrTimeoutRequest context was cancelled or timed out

Testing

Unit Tests

Unit tests use mock HTTP servers and run without network access:

go test ./...

Integration Tests

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 ./...

Test Coverage Summary

PackageUnit TestsIntegration Tests
captcher7
recaptcha1511 (v2: 6, v3: 5)
turnstile1010
middleware/stdhttp96
middleware/ginmw106
middleware/echomw106
Total6139

Project Structure

captcher/ # core module (go.mod) — zero third-party deps
├── 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 (in core module)
│ ├── ginmw/ # Gin middleware — separate module (go.mod)
│ └── echomw/ # Echo middleware — separate module (go.mod)
└── example/ # Runnable example — separate module (go.mod)

The Gin and Echo adapters are independent Go modules, so the core module's dependency graph stays free of gin, echo, and their transitive trees. The example depends on every adapter, so it is its own module too.

Run the example and pick a framework with the FRAMEWORK env var:

cd example
go run .# net/http (default)
FRAMEWORK=gin go run .# Gin
FRAMEWORK=echo go run .# Echo

It first runs direct (no-middleware) verification against all three providers, then starts an HTTP server on :8080 using the selected framework's middleware.

License

MIT

About

CAPTCHA verification supporting Google reCAPTCHA (v2 and v3) and Cloudflare Turnstile

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages