Skip to content

leodeim/captcher

Repository files navigation

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

Quick Start

Direct Verification

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

Swap Providers at Runtime

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

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

Middleware Configuration

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

Verifier Options

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 validation

Error Handling

All 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

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

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

Project Structure

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

License

MIT

About

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

Topics

Resources

License

Stars

Watchers

Forks

Contributors