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 ¶
- Constants
- Variables
- func LocalPath(raw string) string
- func Middleware(v Verifier, when func(c *web.Ctx) bool) func(http.Handler) http.Handler
- type SiteVerify
- type StepUp
- type TurnstileConfig
- func (t TurnstileConfig) CSPSources() map[string][]string
- func (t TurnstileConfig) Enabled() bool
- func (t TurnstileConfig) Protect(action string, when func(*web.Ctx) bool) func(http.Handler) http.Handler
- func (t TurnstileConfig) Script(nonce string) template.HTML
- func (t TurnstileConfig) Validate() error
- func (t TurnstileConfig) Verifier(action string) *SiteVerify
- func (t TurnstileConfig) Widget(action string) template.HTML
- type Verifier
Examples ¶
Constants ¶
const Field = "challenge"
Field is the validation-error field of a failed challenge.
const Header = "X-Challenge-Token"
Header carries the answer for JSON and Inertia requests, which do not post the widget's hidden form field.
const ReturnField = "return"
ReturnField is the interstitial form field holding the page to go back to.
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.
const TurnstileScriptURL = TurnstileOrigin + "/turnstile/v0/api.js"
TurnstileScriptURL is the Turnstile client script. Append "?render=explicit" to render widgets from your own JavaScript.
Variables ¶
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.
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
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 ¶
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") })
})
}
Output:
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.
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
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.
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.