Documentation
¶
Overview ¶
Package turnstile puts Cloudflare Turnstile in front of a form: a view component that draws the challenge, the client behaviour that renders it and renews its token, the Content-Security-Policy the page needs to load it, and the server-side verification that decides whether the submission came from a person.
The four pieces are used together, and each one is a line somebody writes:
// bootstrap/app.go: one Verifier for the application.
captcha, err := turnstile.New(turnstile.Config{
SiteKey: cfg.Turnstile.SiteKey,
SecretKey: cfg.Turnstile.SecretKey,
})
// routes: the pages that draw the widget carry the policy that loads it.
r = r.Group("", turnstile.Headers(middleware.DefaultContentSecurityPolicy()))
// view: the component goes inside the form.
{!! turnstile.Widget(turnstile.WidgetProps{
SiteKey: .TurnstileSiteKey, Action: "signup", Page: .,
}) !!}
// handler: the token is read like any other field, and checked before
// anything else is done.
var challenge struct {
Token string `form:"cf-turnstile-response"` // turnstile.FieldName
}
if err := ctx.Bind(&challenge); err != nil {
return err
}
if err := captcha.Check(ctx.Ctx(), challenge.Token, ctx.IP(), "signup"); err != nil {
return err
}
A rejected token comes back as validation.Errors under FieldName, so the form is drawn again with the message beside the widget, exactly as a field that failed its rule would be.
The verification fails closed. A token that is missing, malformed, spent, expired, solved on another hostname or for another action is refused, and so is a submission made while Cloudflare cannot be reached: a check that let a request through whenever it could not answer is a check an attacker can turn off by making it slow.
Index ¶
Constants ¶
const ( // TestSiteKey draws a visible widget that always passes. TestSiteKey = "1x00000000000000000000AA" // TestSecretKey verifies TestToken as a success. TestSecretKey = "1x0000000000000000000000000000000AA" // TestSecretKeyFails verifies every token as a failure. TestSecretKeyFails = "2x0000000000000000000000000000000AA" // TestSecretKeySpent answers every token as already spent. TestSecretKeySpent = "3x0000000000000000000000000000000AA" // TestToken is the token a widget drawn with a test site key produces. TestToken = "XXXX.DUMMY.TOKEN.XXXX" )
The keys Cloudflare publishes for testing. They work on any hostname, including localhost, and only with each other: a test secret accepts only TestToken, and a production secret refuses it.
They are for development and for test suites. An application configured with TestSecretKey in production accepts every submission.
const ( DefaultRejectedMessage = "Confirm that you are not a robot, then send the form again." )
The sentences a refused submission is answered with when Config leaves Messages empty.
const DefaultEndpoint = Origin + "/turnstile/v0/siteverify"
DefaultEndpoint is where a token is verified when Config leaves Endpoint empty.
const DefaultTimeout = 10 * time.Second
DefaultTimeout bounds one verification when Config leaves Client nil.
const FieldName = "cf-turnstile-response"
FieldName is the form field the widget writes its token into, and the field a rejection is reported under.
It is Cloudflare's own default and is not configurable here: the widget, the handler that reads the field and the view that draws the message all name it, and one constant is what keeps the three agreeing.
const MaxTokenLength = 2048
MaxTokenLength is the longest token Cloudflare issues. A longer value is refused without asking Cloudflare, because it cannot be a token.
const Origin = "https://challenges.cloudflare.com"
Origin is the one address Cloudflare serves Turnstile from: the script, the frame the challenge is drawn in, and the verification endpoint.
The script is loaded from it directly and never copied to this origin. Cloudflare changes the file without changing its address, and a copy served from anywhere else stops working on the day it does.
const ScriptAsset = "arandu-turnstile.js"
ScriptAsset is the name the client behaviour is served under, from this origin, by the application's asset handler. Widget writes the script tag that loads it.
Variables ¶
var ( // ErrMissingToken is a submission that carried no token: the widget was // not solved, or the form was sent by something that never drew it. ErrMissingToken = errors.New("turnstile: the submission carried no token") // ErrRejected is a token Cloudflare refused, or one it accepted for // another hostname or another action. ErrRejected = errors.New("turnstile: the token was refused") // did not answer, answered with an error, or answered something that is // not a verification. ErrUnavailable = errors.New("turnstile: the token could not be verified") // ErrMisconfigured is Cloudflare refusing the secret key. It is the // operator's to fix, and no visitor can do anything about it. ErrMisconfigured = errors.New("turnstile: Cloudflare refused the secret key") )
The reasons a verification does not succeed. Every error Verify returns wraps exactly one of them, so a caller tells them apart with errors.Is.
Functions ¶
func Headers ¶
func Headers(base middleware.ContentSecurityPolicy) hhttp.Middleware
Headers is the middleware for the routes whose pages draw the widget. It writes Policy(base) on the answer, replacing the policy SecurityHeaders wrote, so the widening reaches those pages and no other.
base is the policy the application sends everywhere else, built the way the application builds it -- middleware.DefaultContentSecurityPolicy with the same image origins -- so the pages with the widget lose nothing the rest of the application allows.
A boosted navigation is answered with HX-Redirect to the same address. The policy that governs a document is the one its first load carried, and a boosted link swaps the body without loading a document: a page with the widget reached that way would run under the policy of the page the link was on, and Cloudflare's script would be refused. The redirect makes the browser load the page, and the page arrives with its own policy. It costs one round trip, on the boosted navigations that reach these routes and on nothing else.
func Policy ¶
func Policy(base middleware.ContentSecurityPolicy) middleware.ContentSecurityPolicy
Policy is base widened by what the widget needs: Cloudflare's script, and the frame the challenge is drawn in. Nothing else is added, and no other directive changes.
When base has no frame-src of its own, frames were governed by default-src, so 'self' is kept beside Cloudflare: a frame-src that named only Cloudflare would refuse the application's own frames on the pages that draw the widget.
Types ¶
type Challenge ¶
type Challenge struct {
// Token is the value the widget wrote into FieldName.
Token string
// RemoteIP is the address of the visitor, as the application's proxy
// configuration decided it. It is a hint to Cloudflare and may be empty;
// a value that is not an address is not sent.
RemoteIP string
// Action, when set, is the action the widget had to be drawn with. A token
// solved for another action is refused, which is what stops a token taken
// from the newsletter form from being spent on the sign-up form.
Action string
}
Challenge is one token to verify, with what it is expected to prove.
type Config ¶
type Config struct {
// SiteKey identifies the widget. It is public and is written into the page.
SiteKey string
// SecretKey authenticates the verification. It never reaches a page, a
// log line or an error message.
SecretKey string
// Hostnames, when set, are the only hostnames a token may have been solved
// on. Empty accepts the hostnames the widget is configured for in the
// Cloudflare dashboard, which is where that list belongs; set this when one
// widget serves several applications and each should accept only its own.
Hostnames []string
// Client sends the verification. Nil is a client that gives up after
// DefaultTimeout.
Client *http.Client
// Endpoint is the verification address. Empty is DefaultEndpoint. It has
// to be https, because the request carries the secret key.
Endpoint string
// Messages are the sentences a refused submission is answered with.
Messages Messages
}
Config is what the application passes when it builds the Verifier.
The keys arrive as values the application read from its own configuration. Nothing here reads the environment: a key read at call time is a key whose absence is discovered by the first visitor rather than at boot.
type Messages ¶
type Messages struct {
// Rejected is said when the token was missing or refused: the person has
// to solve the challenge again.
Rejected string
// nothing wrong and can send the form again.
Unavailable string
}
Messages are the sentences a refused submission is answered with, in the language of the application. An empty one is the English default.
type Result ¶
type Result struct {
// Success is whether Cloudflare accepted the token.
Success bool
// ChallengeTS is when the challenge was solved.
ChallengeTS time.Time
// Hostname is the hostname of the page the challenge was solved on.
Hostname string
// Action and CData are what the widget was drawn with.
Action string
CData string
// ErrorCodes are Cloudflare's reasons for refusing the token.
ErrorCodes []string
}
Result is what Cloudflare answered about a token.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier checks tokens against Cloudflare. One is built at boot and shared: it holds no state between calls and is safe for concurrent use.
func (*Verifier) Check ¶
Check verifies the token of one form submission and answers the way a handler returns a failed rule.
It returns nil when the token is good. A token that is missing or refused, and a verification Cloudflare could not complete, return validation.Errors with one message under FieldName, which the router answers by sending the visitor back to the form with the message beside the widget. The two cases carry different messages, because only the first asks the person to do anything differently.
ErrMisconfigured is returned as it is: a secret key Cloudflare refuses is a fault of the deployment, and it belongs on the error page and in the error report rather than in front of a visitor who can do nothing about it.
Verify is the call for a caller that needs the Result or the reason.
func (*Verifier) Verify ¶
Verify asks Cloudflare about one token.
A token is spent by asking: Cloudflare answers each token once, and a second verification of the same token is refused as a duplicate. So Verify is called once per submission, by the handler that acts on it, and never by middleware that runs before a decision about whether the request will be acted on at all.
It returns the Result whenever Cloudflare answered, including alongside an error, so a caller can record why a token was refused.