Documentation
¶
Overview ¶
Package captcha refuses chosen operations unless the request carries a captcha token a provider accepts, so a bot cannot sign up, sign in or ask for a reset link at the rate the rate rules allow a human to.
The plugin is one operation hook. The client solves the challenge in the browser and sends the provider's token in the TokenHeader header; the hook reads it, asks the provider's siteverify endpoint once, and refuses the call before the operation runs when the token is missing, rejected or cannot be checked. Cloudflare Turnstile, Google reCAPTCHA (v2 and v3) and hCaptcha speak the same siteverify protocol and are built in; any other provider is a Verifier.
A direct Go call has no headers and no browser, so it is never checked: the app's own code is not the bot the captcha is for. Every transport call — HTTP, GraphQL, an adapter — is.
Index ¶
Constants ¶
const ( TurnstileEndpoint = "https://challenges.cloudflare.com/turnstile/v0/siteverify" ReCAPTCHAEndpoint = "https://www.google.com/recaptcha/api/siteverify" HCaptchaEndpoint = "https://api.hcaptcha.com/siteverify" // CaptchaFoxEndpoint speaks the hCaptcha-compatible siteverify protocol: // the same form fields, success and hostname in the verdict, no score. CaptchaFoxEndpoint = "https://api.captchafox.com/siteverify" )
Siteverify endpoints of the built-in providers. Each takes a form POST of secret, response and remoteip and answers JSON with success, hostname, action, score and error-codes; WithEndpoint points at a mirror or a test server.
const AnnotationVerified = "captcha.verified"
AnnotationVerified is the auth.Call annotation the plugin sets once the call's token was accepted. A captcha token is single-use at the provider, so a call whose token already passed a rate challenge must not send it again to the operation guard, which would see it rejected as a replay; the guard reads this instead. The value is "true", never the token.
const DefaultMinScore = 0.5
DefaultMinScore is the reCAPTCHA v3 score a token must reach. Google's own recommended starting point: 0.5 splits "likely a bot" from "likely human" on its 0 to 1 scale, and a lower value lets more bots through than it recovers humans.
const DefaultTimeout = 10 * time.Second
DefaultTimeout bounds the siteverify call. Ten seconds is long enough for a provider having a slow day and short enough that a provider outage fails requests instead of holding every connection open until the client gives up.
const PluginName = "captcha"
PluginName is the plugin's permanent identity.
const TokenHeader = "X-Captcha-Response"
TokenHeader is the request header the client sends the provider's token in. It is the header better-auth's clients already use, so a frontend written for one works against the other unchanged; WithHeader renames it.
Variables ¶
var ( // ErrMissing refuses a protected call that sent no token. ErrMissing = auth.NewError("captcha.missing", http.StatusBadRequest, "Complete the captcha and try again.") // ErrFailed refuses a token the provider rejected: expired, already // used, solved on another site, minted for another action, or scored // below the threshold. The client gets one answer for all of them so a // bot cannot learn which check it tripped. ErrFailed = auth.NewError("captcha.failed", http.StatusForbidden, "The captcha could not be verified. Try again.") // answers something that is not a verdict: an unreachable provider must // not become an open door. ErrUnavailable = auth.NewError("captcha.unavailable", http.StatusServiceUnavailable, "The captcha service is not available. Try again in a moment.") // ErrNoVerifier fails auth.New when New was given no Verifier: a captcha // plugin that cannot verify would refuse every protected call. ErrNoVerifier = errors.New("captcha: pass a Verifier such as captcha.Turnstile(secret)") // ErrNothingProtected fails auth.New when no protected operation is // registered: a captcha plugin that guards nothing is a configuration // mistake, usually a plugin list without the plugin it was meant for. ErrNothingProtected = errors.New("captcha: none of the protected operations is registered; pass captcha.Protect with the ids to guard") // ErrNoSecret fails the provider constructors' Validate when the secret // is empty, because every provider answers "invalid-input-secret" for it // and the symptom would be every sign-in failing in production. ErrNoSecret = errors.New("captcha: the provider secret key is empty") )
Errors. The first three carry stable codes a client checks for; the message of each is the same whatever the operation and whoever the account is.
var DefaultProtected = []auth.EndpointID{
"password.sign-up", "password.sign-in", "password.forgot", "password.reset",
"email.send-code", "email.sign-in-code",
"phone.send-code", "phone.sign-in",
"magiclink.send",
}
Default protected operations: every way an anonymous request can create an account, prove a password, or make the app send a message. The ids are spelled out because their plugins live in other modules (password) or would make this plugin depend on every identifier plugin; the ones whose plugin is not registered are dropped at auth.New, so the default list is safe in any plugin set. Protect replaces it.
Functions ¶
Types ¶
type Option ¶
type Option func(*Plugin)
Option configures the plugin.
func ChallengeOnly ¶
func ChallengeOnly() Option
ChallengeOnly makes the plugin guard no operation on every call and serve only as the core's rate challenger: a call is asked for a captcha when an auth.RateChallenge rule is over its limit, and never before. Without it the plugin does both, guarding its protected operations always and passing rate challenges anywhere. It cannot be combined with Protect or ProtectPaths, which would say the opposite.
func Protect ¶
func Protect(ids ...auth.EndpointID) Option
Protect replaces DefaultProtected with the operations to guard. Unlike the default list, every id named here must be registered, so a typo fails auth.New (auth.ErrUnknownHookTarget) instead of leaving an operation open.
func ProtectPaths ¶
ProtectPaths guards every operation whose route matches one of patterns, better-auth's endpoints option with wildcards: "*" matches within one path segment and "**" across segments, so "/password/*" guards /password/sign-in and /password/sign-up but not /oauth/google/start, and "/oauth/**" guards every OAuth route. A pattern with no wildcard is one exact route. Routes are the operation's own (Operation.Route, "/password/ sign-in"), without authhttp's mount prefix.
Like Protect it replaces DefaultProtected, and the two add up when both are given. The safety Protect has stays: a pattern that matches no registered operation fails auth.New with auth.ErrUnknownHookTarget naming the pattern, so a typo or a renamed route can never leave a form open in silence. It is matched once, at auth.New.
func WithHeader ¶
WithHeader renames the header the token arrives in, for a frontend that already sends it under another name.
func WithTimeout ¶
WithTimeout bounds each verification; see DefaultTimeout. A zero or negative value keeps the default, because a captcha with no timeout is a captcha that can hang every protected request.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the captcha hook. Build it with New.
func New ¶
New builds the plugin around v. auth.New fails with ErrNoVerifier for a nil v and with ErrNothingProtected when no protected operation exists.
func (*Plugin) Init ¶
Init narrows the default list to the ids whose plugin is registered (an id is "<plugin>.<name>", so the prefix is the plugin) and refuses a plugin set in which none is: that is the "forgot the password plugin" mistake, and it must fail here, loudly, not guard nothing. An explicit list is kept whole and auth.New checks each id.
func (*Plugin) OperationHooks ¶
func (p *Plugin) OperationHooks() []auth.OperationHook
OperationHooks is the one hook: verify before the protected operations.
auth.New asks for the hooks once every operation is registered, so the default list is narrowed here to the ids that exist (or were switched off by WithDisabledOperations): a plugin that is registered but, like a custom one reusing the name, lacks one of the operations does not fail New. When nothing would remain, the list is handed over whole so New refuses it with auth.ErrUnknownHookTarget rather than a hook with an empty Match, which would guard every operation.
func (*Plugin) PassChallenge ¶
PassChallenge is auth.RateChallenger: a call over an auth.RateChallenge rule passes with a token the verifier accepts. A Go call passes, as it does the operation guard; a call without a token is auth.ErrChallengeRequired (428), so the client knows to show the widget and send the call again; a rejected token is ErrFailed and an unreachable provider ErrUnavailable, as for a protected operation.
func (*Plugin) Protected ¶
func (p *Plugin) Protected() []auth.EndpointID
Protected lists the operations the plugin guards, settled at auth.New, for a page or a test that reports them.
type ProviderOption ¶
type ProviderOption func(*SiteVerify)
ProviderOption configures a built-in provider.
func WithAllowedHostnames ¶
func WithAllowedHostnames(hosts ...string) ProviderOption
WithAllowedHostnames rejects a token solved on a site other than the ones listed, compared exactly against the hostname the provider reports.
func WithEndpoint ¶
func WithEndpoint(u string) ProviderOption
WithEndpoint replaces the provider's siteverify URL: a regional mirror, or a fake server in a test. It must be absolute.
func WithExpectedAction ¶
func WithExpectedAction(action string) ProviderOption
WithExpectedAction rejects a token the widget minted for another action (reCAPTCHA v3 and Turnstile report it), so a token solved on the newsletter form cannot be replayed at sign-in.
func WithHTTPClient ¶
func WithHTTPClient(c *http.Client) ProviderOption
WithHTTPClient replaces the client the verification uses, for a proxy or a test transport. The plugin's timeout still applies through the context.
func WithMinScore ¶
func WithMinScore(score float64) ProviderOption
WithMinScore sets the score a reCAPTCHA v3 token must reach; see DefaultMinScore. Out of the 0 to 1 range it keeps the default.
func WithSiteKey ¶
func WithSiteKey(key string) ProviderOption
WithSiteKey sends the site key with the verification, so the provider rejects a token solved under another key of the same account. hCaptcha and CaptchaFox support it; the others ignore the field.
type SiteVerify ¶
type SiteVerify struct {
// contains filtered or unexported fields
}
SiteVerify is a Verifier over the siteverify protocol: Turnstile, reCAPTCHA, hCaptcha and CaptchaFox differ only in endpoint and in which optional fields they return. Build one with Turnstile, ReCAPTCHA, HCaptcha or CaptchaFox.
func CaptchaFox ¶
func CaptchaFox(secret string, opts ...ProviderOption) *SiteVerify
CaptchaFox verifies CaptchaFox tokens with the site's secret key. The verdict carries no score, so a token is judged by success, hostname and the site key.
func HCaptcha ¶
func HCaptcha(secret string, opts ...ProviderOption) *SiteVerify
HCaptcha verifies hCaptcha tokens with the site's secret key.
func ReCAPTCHA ¶
func ReCAPTCHA(secret string, opts ...ProviderOption) *SiteVerify
ReCAPTCHA verifies Google reCAPTCHA tokens, v2 and v3, with the site's secret key. A v3 token carries a score and must reach WithMinScore; a v2 token carries none and is judged by the provider's success flag.
func Turnstile ¶
func Turnstile(secret string, opts ...ProviderOption) *SiteVerify
Turnstile verifies Cloudflare Turnstile tokens with the site's secret key.
func (*SiteVerify) Provider ¶
func (s *SiteVerify) Provider() string
Provider names the built-in provider, for logs and pages.
func (*SiteVerify) Validate ¶
func (s *SiteVerify) Validate(*auth.Config) error
Validate refuses an empty secret and an endpoint that is not an absolute URL, at auth.New rather than at the first sign-in.
func (*SiteVerify) Verify ¶
func (s *SiteVerify) Verify(ctx context.Context, token, remoteIP string) error
Verify posts the token to the siteverify endpoint and applies the hostname, action and score checks to the verdict. Transport and decoding failures are returned as they are, for the plugin to fail closed on; a rejection is ErrFailed with the provider's reason wrapped for the log.
type Verifier ¶
Verifier checks one token. It returns nil for a token the provider accepts, ErrFailed (or an error wrapping it) for one it rejects, and any other error when no verdict could be had; the plugin reports that as ErrUnavailable and refuses the call. remoteIP is the client address, "" when unknown, for providers that score it.