A simple, thread-safe Go client library for verifying Cloudflare Turnstile CAPTCHA tokens.
Cloudflare Turnstile is an alternative to traditional CAPTCHAs that helps websites protect against bots and automated traffic while providing a better user experience. This library provides a clean, idiomatic Go interface for server-side token verification.
- âś… Thread-safe: Safe for concurrent use by multiple goroutines
- âś… Automatic retries: Built-in retry logic for transient network failures
- âś… Idempotency protection: Automatic unique idempotency keys prevent replay attacks
- âś… Structured error handling: Specific error types for different failure scenarios
- âś… Flexible configuration: Customizable timeouts, endpoints, and HTTP clients
- âś… Request helpers: Easy extraction of tokens from HTTP requests
- âś… Zero dependencies: Only uses Go standard library (except for UUID generation)
go get github.com/tordrt/go-turnstilepackage main
import (
"fmt"
"log"
"net/http"
"github.com/tordrt/go-turnstile"
)
func main() {
// Create a new Turnstile client
turnstileClient, err := turnstile.New("your-site-key", "your-secret-key")
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/verify", func(w http.ResponseWriter, r *http.Request) {
// Verify the token from the HTTP request
response, err := turnstileClient.VerifyRequest(r.Context(), r)
if err != nil {
http.Error(w, "CAPTCHA verification failed", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "Verification successful! Hostname: %s", response.Hostname)
})
log.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}// Verify a token directly
response, err := turnstileClient.VerifyToken(context.TODO(), clientToken, clientIP)
if err != nil {
// Handle verification error
return
}
// Verification successfulThe VerifyRequest method automatically extracts the token from the cf-turnstile-response form field and determines the client IP:
func handlerFunction(w http.ResponseWriter, r *http.Request) {
response, err := turnstileClient.VerifyRequest(r.Context(), r)
if err != nil {
// Handle verification failure
return
}
// Verification successful
}turnstileClient, err := turnstile.New(
"your-site-key",
"your-secret-key",
turnstile.WithMaxRetries(3),
turnstile.WithRetryDelay(200*time.Millisecond),
turnstile.WithHTTPClient(&http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 10,
},
}),
)The library provides specific error types for different failure scenarios:
response, err := turnstileClient.VerifyToken(r.Context(), token, clientIP)
if err != nil {
// Check for specific error types
var timeoutErr turnstile.ErrTimeoutOrDuplicate
if errors.As(err, &timeoutErr) {
log.Println("Token timeout or duplicate submission:", err)
return
}
var invalidTokenErr turnstile.ErrInvalidInputResponse
if errors.As(err, &invalidTokenErr) {
log.Println("Invalid token provided:", err)
return
}
// Handle other verification errors
log.Println("Verification failed:", err)
return
}| Option | Description | Default |
|---|---|---|
WithHTTPClient(client) |
Custom HTTP client | 3 second timeout |
WithVerifyEndpoint(url) |
Custom verification endpoint | Cloudflare's official endpoint |
WithMaxRetries(n) |
Maximum retry attempts | 2 |
WithRetryDelay(duration) |
Initial retry delay (exponential backoff) | 100ms |
const (
DefaultVerifyEndpoint = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
DefaultTimeout = 3 * time.Second
DefaultMaxRetries = 2
DefaultRetryDelay = 100 * time.Millisecond
)The library provides structured error handling with specific error types:
ErrInvalidSiteKey: Site key is empty or invalidErrInvalidSecretKey: Secret key is empty or invalidErrEmptyToken: Token is empty or whitespaceErrMissingInputSecret: Secret key missing in requestErrInvalidInputSecret: Secret key is invalidErrMissingInputResponse: Token missing in requestErrInvalidInputResponse: Token is invalid or expiredErrBadRequest: Malformed requestErrTimeoutOrDuplicate: Token timeout or duplicate submissionErrInternalError: Cloudflare internal error
The Response struct contains the complete verification result:
type Response struct {
Success bool `json:"success"`
ChallengeTS string `json:"challenge_ts,omitempty"`
Hostname string `json:"hostname,omitempty"`
ErrorCodes []string `json:"error-codes,omitempty"`
Action string `json:"action,omitempty"`
CData string `json:"cdata,omitempty"`
Metadata *ResponseMetadata `json:"metadata,omitempty"` // Enterprise only
}Here's a complete HTML form example:
<!DOCTYPE html>
<html>
<head>
<title>Turnstile Demo</title>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>
<body>
<form action="/verify" method="POST">
<div class="cf-turnstile" data-sitekey="your-site-key"></div>
<input type="submit" value="Submit">
</form>
</body>
</html>This library provides convenient test helpers that make it easy to test your Turnstile integration without setting up real Cloudflare keys or widgets. The test clients use Cloudflare's official dummy keys under the hood.
Testing handlers that use VerifyRequest() is simple with the provided test helpers:
import (
"testing"
"context"
"github.com/tordrt/go-turnstile"
)
func TestMyHandler(t *testing.T) {
// Create a test client that always passes verification
client := turnstile.NewTestClient()
// Create a test request with the Turnstile token already included
req := turnstile.NewTestRequest(map[string]string{
"username": "testuser",
})
// Verify the request
response, err := client.VerifyRequest(context.Background(), req)
if err != nil {
t.Fatalf("Expected successful verification: %v", err)
}
// Test your handler logic with a valid token
if !response.Success {
t.Error("Expected successful response")
}
}Or if you're testing direct token verification:
func TestTokenVerification(t *testing.T) {
client := turnstile.NewTestClient()
response, err := client.VerifyToken(context.Background(), turnstile.TestToken)
if err != nil {
t.Fatalf("Expected successful verification: %v", err)
}
}The library provides three test client constructors for different testing scenarios:
Creates a client that always passes verification. Use this to test your happy path logic.
func TestHandlerSuccess(t *testing.T) {
client := turnstile.NewTestClient()
response, err := client.VerifyToken(context.Background(), turnstile.TestToken)
// err will be nil, response.Success will be true
}Creates a client that always fails verification. Use this to test your error handling.
func TestHandlerFailure(t *testing.T) {
client := turnstile.NewTestClientAlwaysFail()
_, err := client.VerifyToken(context.Background(), turnstile.TestToken)
// err will not be nil - test your error handling here
if err == nil {
t.Fatal("Expected verification to fail")
}
}Creates a client that returns a "token already spent" error. Use this to test replay attack protection.
func TestHandlerDuplicateToken(t *testing.T) {
client := turnstile.NewTestClientTokenSpent()
_, err := client.VerifyToken(context.Background(), turnstile.TestToken)
var timeoutErr turnstile.ErrTimeoutOrDuplicate
if !errors.As(err, &timeoutErr) {
t.Fatal("Expected timeout-or-duplicate error")
}
// Test your duplicate submission handling
}The library provides two ways to add the test token to HTTP requests:
Creates a complete HTTP request with the test token already included:
func TestHTTPHandler(t *testing.T) {
client := turnstile.NewTestClient()
// Create a test request with the Turnstile token and additional form data
req := turnstile.NewTestRequest(map[string]string{
"username": "testuser",
"email": "[email protected]",
})
// Verify using VerifyRequest - the test token is already in the request
response, err := client.VerifyRequest(context.Background(), req)
if err != nil {
t.Fatalf("Expected successful verification: %v", err)
}
// Now test your handler logic
// ...
}You can also create a request without additional form data:
req := turnstile.NewTestRequest() // Just includes the Turnstile tokenIf you already have a request object, use AddTestToken() to add the token:
func TestHTTPHandler(t *testing.T) {
client := turnstile.NewTestClient()
// You already have a request from your test setup
form := url.Values{}
form.Set("username", "testuser")
req := httptest.NewRequest(http.MethodPost, "/submit", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// Add the test token to the existing request
err := turnstile.AddTestToken(req)
if err != nil {
t.Fatalf("Failed to add test token: %v", err)
}
// Verify the request
response, err := client.VerifyRequest(context.Background(), req)
if err != nil {
t.Fatalf("Expected successful verification: %v", err)
}
// Test your handler logic...
}The library exports the following test constants for use in your tests:
| Constant | Description |
|---|---|
turnstile.TestToken |
Dummy response token accepted by all test clients |
turnstile.TestSiteKeyAlwaysPass |
Dummy sitekey that always passes (visible) |
turnstile.TestSiteKeyAlwaysBlock |
Dummy sitekey that always blocks (visible) |
turnstile.TestSiteKeyAlwaysPassInvisible |
Dummy sitekey that always passes (invisible) |
turnstile.TestSiteKeyAlwaysBlockInvisible |
Dummy sitekey that always blocks (invisible) |
turnstile.TestSiteKeyForceChallenge |
Dummy sitekey that forces interactive challenge |
turnstile.TestSecretKeyAlwaysPass |
Dummy secret key that always passes |
turnstile.TestSecretKeyAlwaysFail |
Dummy secret key that always fails |
turnstile.TestSecretKeyTokenSpent |
Dummy secret key that returns "token spent" error |
func TestTurnstileHandler(t *testing.T) {
tests := []struct {
name string
client *turnstile.Client
expectError bool
}{
{
name: "successful verification",
client: turnstile.NewTestClient(),
expectError: false,
},
{
name: "failed verification",
client: turnstile.NewTestClientAlwaysFail(),
expectError: true,
},
{
name: "duplicate token",
client: turnstile.NewTestClientTokenSpent(),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
response, err := tt.client.VerifyToken(
context.Background(),
turnstile.TestToken,
)
if tt.expectError && err == nil {
t.Error("Expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
}
}For more examples, see the example_test.go file.
The test clients above (NewTestClient, etc.) use Cloudflare's test keys but still make real HTTP requests to Cloudflare's servers. For true unit testing with zero network calls, use the mock client:
func TestMyHandler(t *testing.T) {
// Create a mock client - NO HTTP requests are made
client, mock := turnstile.NewMockClient()
// Any token works - responses are fully mocked
response, err := client.VerifyToken(context.Background(), "any-token")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Verify the mock was called
if mock.CallCount() != 1 {
t.Errorf("Expected 1 call, got %d", mock.CallCount())
}
// Inspect the request that was made
body := mock.LastRequestBody()
// ... assertions on the request
}| Function | Description |
|---|---|
NewMockClient() |
Returns successful verification |
NewMockClientAlwaysFail() |
Returns ErrInvalidInputResponse |
NewMockClientTokenSpent() |
Returns ErrTimeoutOrDuplicate |
NewMockClientWithResponse(resp) |
Returns custom MockResponse |
NewMockClientWithHTTPError(code, status) |
Simulates HTTP errors (500, 429, etc.) |
func TestCustomScenario(t *testing.T) {
client, _ := turnstile.NewMockClientWithResponse(turnstile.MockResponse{
Success: true,
ChallengeTS: "2024-01-01T00:00:00Z",
Hostname: "myapp.example.com",
Action: "login",
})
response, _ := client.VerifyToken(context.Background(), "token")
if response.Action != "login" {
t.Errorf("Expected action 'login', got %q", response.Action)
}
}func TestDynamicBehavior(t *testing.T) {
client, mock := turnstile.NewMockClient()
// First call succeeds
_, err := client.VerifyToken(context.Background(), "token")
if err != nil {
t.Fatal("Expected success")
}
// Change to failure mode
mock.SetResponse(turnstile.MockResponse{
Success: false,
ErrorCodes: []string{"timeout-or-duplicate"},
})
// Second call fails
_, err = client.VerifyToken(context.Background(), "token")
if err == nil {
t.Fatal("Expected failure")
}
}func TestServerError(t *testing.T) {
client, _ := turnstile.NewMockClientWithHTTPError(500, "500 Internal Server Error")
_, err := client.VerifyToken(context.Background(), "token")
if err == nil {
t.Fatal("Expected error for 500 response")
}
}| Scenario | Recommended Client |
|---|---|
| Unit tests (fast, isolated, no network) | NewMockClient() |
| Integration tests (real Cloudflare validation) | NewTestClient() |
| CI/CD pipelines with network restrictions | NewMockClient() |
| Testing with real Cloudflare error responses | NewTestClient() |
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.