Help us shape the future of Go! Take the 2026 Go Developer Survey

challenge

package
v0.36.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package challenge stops bots on form routes (signup, login, contact) with a challenge the browser solves: Cloudflare Turnstile and hCaptcha through SiteVerify, or any Verifier of your own.

Middleware verifies unsafe requests only. A missing or rejected answer is a validation error on the "challenge" field (422, or a redirect back with errors for HTML forms); a provider that cannot be reached denies with 503 (fail closed). Both log a security event. Pass When to ask only suspicious clients, for example after a soft rate limit trips.

TurnstileConfig reads TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY (off while unset) and renders the widget, its nonce'd script and CSP sources. StepUp challenges a client mid-browse with an interstitial page and a signed clearance cookie.

ts, _ := config.Load[challenge.TurnstileConfig](src)
g.Use(ts.Protect("register", nil)) // on the group holding POST /register

See https://github.com/AlexGalhardo/vukem/blob/main/docs/subsystems.md#bot-challenge.

Index

Examples

Constants

View Source
const Field = "challenge"

Field is the validation-error field of a failed challenge.

View Source
const Header = "X-Challenge-Token"

Header carries the answer for JSON and Inertia requests, which do not post the widget's hidden form field.

View Source
const ReturnField = "return"

ReturnField is the interstitial form field holding the page to go back to.

View Source
const TurnstileOrigin = "https://challenges.cloudflare.com"

TurnstileOrigin serves the Turnstile script and widget iframe; the CSP must allow it in script-src and frame-src.

View Source
const TurnstileScriptURL = TurnstileOrigin + "/turnstile/v0/api.js"

TurnstileScriptURL is the Turnstile client script. Append "?render=explicit" to render widgets from your own JavaScript.

Variables

View Source
var ErrRejected = errors.New("challenge: rejected")

ErrRejected is returned by a Verifier when the answer is missing or wrong. Any other error means the check could not run.

View Source
var ErrUnavailable = errors.New("challenge: provider unavailable")

ErrUnavailable wraps the errors of a provider that could not answer (network, timeout, non-200, unreadable answer, its own internal-error), as opposed to our misconfiguration (a missing or rejected secret).

Functions

func LocalPath added in v0.36.0

func LocalPath(raw string) string

LocalPath returns raw when it is a path on this site ("/items?page=2"), else "/": never another host ("//evil.example", "/\evil.example", "https://evil.example"), so a return parameter cannot redirect away.

func Middleware

func Middleware(v Verifier, when func(c *web.Ctx) bool) func(http.Handler) http.Handler

Middleware verifies the challenge on POST, PUT, PATCH and DELETE requests for which when returns true (nil: all of them); safe methods pass, so the page that renders the widget is not challenged.

Example
package main

import (
	"net/http"

	"github.com/AlexGalhardo/vukem/challenge"
	"github.com/AlexGalhardo/vukem/config"
	"github.com/AlexGalhardo/vukem/router"
	"github.com/AlexGalhardo/vukem/web"
)

func main() {
	verify := challenge.Turnstile(config.NewSecret("turnstile-secret"))
	verify.Hostname = "example.com"

	r := router.New(&web.Services{})
	r.Group("", func(g *router.Group) {
		g.Use(challenge.Middleware(verify, nil))
		g.Post("/register", func(c *web.Ctx) error { return c.String(http.StatusCreated, "welcome") })
	})
}

Types

type SiteVerify

type SiteVerify struct {
	// URL is the siteverify endpoint.
	URL string
	// Secret is the server-side secret key.
	Secret config.Secret
	// FormField is the form field the widget fills in (the Header is also
	// read, for JSON requests).
	FormField string
	// Hostname, when set, must equal the hostname the provider reports the
	// widget was solved on, so a token solved on another site is refused.
	Hostname string
	// Action, when set, must equal the action the provider reports (the
	// widget's data-action, a Turnstile feature), so a token solved on the
	// newsletter form is refused on the login form.
	Action string
	// Client sends the request; nil uses httpclient.Safe with a 5s timeout.
	Client *http.Client
	// InsecureFailOpen lets requests through while the provider is unavailable
	// (ErrUnavailable), logging a "challenge.fail_open" security event.
	// The default (false) denies with 503: no bot gets in during an
	// outage, and neither does anyone else. A misconfigured secret never
	// fails open.
	InsecureFailOpen bool
}

SiteVerify verifies tokens with a siteverify endpoint, the protocol of Cloudflare Turnstile and hCaptcha: the secret, the token and the client IP are posted, and the JSON answer must say success.

func HCaptcha

func HCaptcha(secret config.Secret) *SiteVerify

HCaptcha returns an hCaptcha verifier.

func Turnstile

func Turnstile(secret config.Secret) *SiteVerify

Turnstile returns a Cloudflare Turnstile verifier.

func (*SiteVerify) Verify

func (s *SiteVerify) Verify(c *web.Ctx) error

Verify implements Verifier.

type StepUp added in v0.36.0

type StepUp struct {
	// Turnstile holds the keys; StepUp is off (Require passes) when it is.
	Turnstile TurnstileConfig
	// Verifier checks the token; nil uses Turnstile.Verifier(Action).
	Verifier Verifier
	// Action is the widget's data-action, checked by the default
	// Verifier ("step-up").
	Action string
	// Path is the route Handle is mounted on ("/challenge").
	Path string
	// TTL is how long a clearance lasts (30 minutes).
	TTL time.Duration
	// Cookie is the clearance cookie name ("vukem_clearance").
	Cookie string
	// Skip exempts a request from Require (verified search crawlers,
	// signed-in users): the app decides, StepUp never guesses.
	Skip func(c *web.Ctx) bool
	// Now is the clock (time.Now).
	Now func() time.Time
	// contains filtered or unexported fields
}

StepUp challenges a client in the middle of browsing: a middleware that finds a client suspicious (over a soft rate limit) calls Require, which serves an interstitial page with the widget. The page posts to Handle, which verifies the token, sets a short-lived signed clearance cookie bound to the client's IP prefix and User-Agent, and redirects back.

Build it with NewStepUp; set the fields before serving requests.

func NewStepUp added in v0.36.0

func NewStepUp(t TurnstileConfig, key []byte) *StepUp

NewStepUp returns a StepUp signing clearances with key; derive it with app.DeriveKey("challenge.clearance", 32). It panics on a key shorter than signed.MinKeyLen.

func (*StepUp) Handle added in v0.36.0

func (s *StepUp) Handle(c *web.Ctx) error

Handle verifies the interstitial's token; mount it on POST Path. A valid token sets the clearance cookie and redirects (303) to the return field when it is a local path, else to "/". A rejected token shows the page again (403); a provider outage is a 503 unless InsecureFailOpen.

func (*StepUp) Require added in v0.36.0

func (s *StepUp) Require(c *web.Ctx) bool

Require reports whether the request may go on: Turnstile is off, Skip says so, or the client holds a valid clearance. Otherwise it writes the interstitial (403, noindex, no-store) and returns false; the caller must then stop.

type TurnstileConfig added in v0.36.0

type TurnstileConfig struct {
	// SiteKey is the public widget key (Cloudflare dashboard > Turnstile).
	SiteKey string `env:"TURNSTILE_SITE_KEY"`
	// SecretKey is the server-side key; it prints as "[redacted]".
	SecretKey config.Secret `env:"TURNSTILE_SECRET_KEY"`
	// Hostname, when set, must equal the hostname the widget was solved
	// on, so a token solved on another site is refused.
	Hostname string `env:"TURNSTILE_HOSTNAME"`
	// Timeout bounds each siteverify call (one retry on a network error
	// or a 5xx answer).
	Timeout time.Duration `env:"TURNSTILE_TIMEOUT" default:"5s"`
	// InsecureFailOpen lets requests through while Cloudflare cannot
	// answer; see SiteVerify.InsecureFailOpen. Off: an outage denies (503).
	InsecureFailOpen bool `env:"TURNSTILE_INSECURE_FAIL_OPEN"`
}

TurnstileConfig is the Cloudflare Turnstile setup, read from the environment with config.Load[challenge.TurnstileConfig]. Turnstile is off until both keys are set: every helper then renders nothing, adds no CSP source and lets requests through.

func (TurnstileConfig) CSPSources added in v0.36.0

func (t TurnstileConfig) CSPSources() map[string][]string

CSPSources returns the sources Turnstile needs, for middleware.SecureHeadersConfig.CSPSources, or nil when it is off.

func (TurnstileConfig) Enabled added in v0.36.0

func (t TurnstileConfig) Enabled() bool

Enabled reports whether both keys are set.

func (TurnstileConfig) Protect added in v0.36.0

func (t TurnstileConfig) Protect(action string, when func(*web.Ctx) bool) func(http.Handler) http.Handler

Protect is Middleware with a Verifier for action, or a pass-through when Turnstile is off. Put it on the routes that receive the form.

func (TurnstileConfig) Script added in v0.36.0

func (t TurnstileConfig) Script(nonce string) template.HTML

Script returns the Turnstile script tag carrying the request's CSP nonce ({{ turnstileScript cspNonce }}), or "" when Turnstile is off.

func (TurnstileConfig) Validate added in v0.36.0

func (t TurnstileConfig) Validate() error

Validate refuses a half-configured Turnstile (one key without the other), which would otherwise be silently off.

func (TurnstileConfig) Verifier added in v0.36.0

func (t TurnstileConfig) Verifier(action string) *SiteVerify

Verifier returns a SiteVerify for widgets rendered with action (empty: no action check), or nil when Turnstile is off.

func (TurnstileConfig) Widget added in v0.36.0

func (t TurnstileConfig) Widget(action string) template.HTML

Widget returns the implicit-render widget markup for a form, with data-action set when action is not empty, or "" when Turnstile is off. The widget adds the cf-turnstile-response field to the form.

type Verifier

type Verifier interface {
	// Verify returns nil for a valid answer, an error wrapping ErrRejected
	// for a missing or wrong one, and any other error when it cannot tell.
	Verify(c *web.Ctx) error
}

Verifier checks the challenge answer carried by the request.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL