package auth import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" "time" "github.com/google/uuid" ) const defaultTurnstileSiteVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" type TurnstileVerifierOptions struct { SecretKey string SiteVerifyURL string Timeout time.Duration HTTPClient *http.Client } type TurnstileVerifier struct { secretKey string siteVerifyURL string timeout time.Duration client *http.Client } type turnstileVerifyRequest struct { Secret string `json:"secret"` Response string `json:"response"` IdempotencyKey string `json:"idempotency_key,omitempty"` } type turnstileVerifyResponse struct { Success bool `json:"success"` ErrorCodes []string `json:"error-codes"` } func NewTurnstileVerifier(opts TurnstileVerifierOptions) (*TurnstileVerifier, error) { secretKey := strings.TrimSpace(opts.SecretKey) if secretKey == "" { return nil, ErrProviderNotReady } siteVerifyURL := strings.TrimSpace(opts.SiteVerifyURL) if siteVerifyURL == "" { siteVerifyURL = defaultTurnstileSiteVerifyURL } timeout := opts.Timeout if timeout <= 0 { timeout = 5 * time.Second } client := opts.HTTPClient if client == nil { client = &http.Client{Timeout: timeout} } return &TurnstileVerifier{ secretKey: secretKey, siteVerifyURL: siteVerifyURL, timeout: timeout, client: client, }, nil } func (v *TurnstileVerifier) Verify(ctx context.Context, token string) error { token = strings.TrimSpace(token) if token == "" { return ErrHumanVerificationRequired } ctx, cancel := context.WithTimeout(ctx, v.timeout) defer cancel() body, err := json.Marshal(turnstileVerifyRequest{ Secret: v.secretKey, Response: token, IdempotencyKey: uuid.NewString(), }) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, v.siteVerifyURL, bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") resp, err := v.client.Do(req) if err != nil { return fmt.Errorf("%w: turnstile verification request failed", ErrHumanVerificationInvalid) } defer resp.Body.Close() var result turnstileVerifyResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return fmt.Errorf("%w: turnstile verification response is invalid", ErrHumanVerificationInvalid) } if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices || !result.Success { if len(result.ErrorCodes) > 0 { return fmt.Errorf("%w: %s", ErrHumanVerificationInvalid, strings.Join(result.ErrorCodes, ",")) } return ErrHumanVerificationInvalid } return nil }