INTEGRITY Dokumentace

Ověření tokenu

Jak bezpečně ověřovat tokeny Turnstile na svém serveru pomocí Siteverify API.

Postup

  1. Klient vygeneruje token: návštěvník na vaší stránce dokončí ověření Turnstile.
  2. Odeslání tokenu na server: token Turnstile je součástí odeslaného formuláře.
  3. Server ověří token: váš server zavolá Siteverify API od Cloudflare.
  4. Odpověď Cloudflare: vrátí success nebo failure a další data.
  5. Server jedná: podle výsledku ověření původní požadavek povolí, nebo odmítne.

Přehled Siteverify API

Endpoint
POST https://challenges.cloudflare.com/turnstile/v0/siteverify

Formát požadavku

Rozhraní API přijímá application/x-www-form-urlencoded a application/json a vždy vrací odpovědi ve formátu JSON.

Povinné parametry

Parametr Povinné Popis
secret Ano Secret key vašeho widgetu z Cloudflare dashboardu
response Ano Token z widgetu na straně klienta
remoteip Ne IP adresa návštěvníka
idempotency_key Ne UUID, které vygenerujete, abyste mohli bezpečně opakovat ověřovací požadavky

Vlastnosti tokenu

Ověřovací token vydaný službou Turnstile platí pět minut. Pokud uživatel odešle formulář později, token se považuje za vypršený. Ověřovací API na straně serveru pak vrátí chybu a error-codes v odpovědi bude obsahovat timeout-or-duplicate.

Aby ověření proběhlo úspěšně, musí návštěvník požadavek zahájit a odeslat token na váš backend během pětiminutového okna. Jinak je potřeba widget Turnstile obnovit, aby vygeneroval nový token. To zajistíte pomocí turnstile.reset .


Základní příklady ověření

JSON

const SECRET_KEY = "your-secret-key";

async function validateTurnstile(token, remoteip) {
	try {
		const response = await fetch(
			"https://challenges.cloudflare.com/turnstile/v0/siteverify",
			{
				method: "POST",
				headers: {
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					secret: SECRET_KEY,
					response: token,
					remoteip: remoteip,
				}),
			},
		);

		const result = await response.json();
		return result;
	} catch (error) {
		console.error("Turnstile validation error:", error);
		return { success: false, "error-codes": ["internal-error"] };
	}
}

Data formuláře

const SECRET_KEY = "your-secret-key";

async function validateTurnstile(token, remoteip) {
	const formData = new FormData();
	formData.append("secret", SECRET_KEY);
	formData.append("response", token);
	formData.append("remoteip", remoteip);

	try {
		const response = await fetch(
			"https://challenges.cloudflare.com/turnstile/v0/siteverify",
			{
				method: "POST",
				body: formData,
			},
		);

		const result = await response.json();
		return result;
	} catch (error) {
		console.error("Turnstile validation error:", error);
		return { success: false, "error-codes": ["internal-error"] };
	}
}

// Usage in form handler
async function handleFormSubmission(request) {
	const body = await request.formData();
	const token = body.get("cf-turnstile-response");
	const ip =
		request.headers.get("CF-Connecting-IP") ||
		request.headers.get("X-Forwarded-For") ||
		"unknown";

	const validation = await validateTurnstile(token, ip);

	if (validation.success) {
		// Token is valid - process the form
		console.log("Valid submission from:", validation.hostname);
		return processForm(body);
	} else {
		// Token is invalid - reject the submission
		console.log("Invalid token:", validation["error-codes"]);
		return new Response("Invalid verification", { status: 400 });
	}
}
<?php
function validateTurnstile($token, $secret, $remoteip = null) {
    $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';

    $data = [
        'secret' => $secret,
        'response' => $token
    ];

    if ($remoteip) {
        $data['remoteip'] = $remoteip;
    }

    $options = [
        'http' => [
            'header' => "Content-type: application/x-www-form-urlencoded\r\n",
            'method' => 'POST',
            'content' => http_build_query($data)
        ]
    ];

    $context = stream_context_create($options);
    $response = file_get_contents($url, false, $context);

    if ($response === FALSE) {
        return ['success' => false, 'error-codes' => ['internal-error']];
    }

    return json_decode($response, true);

}

// Usage
$secret_key = 'your-secret-key';
$token = $_POST['cf-turnstile-response'] ?? '';
$remoteip = $\_SERVER['HTTP_CF_CONNECTING_IP'] ??
$\_SERVER['HTTP_X_FORWARDED_FOR'] ??
$\_SERVER['REMOTE_ADDR'];

$validation = validateTurnstile($token, $secret_key, $remoteip);

if ($validation['success']) {
// Valid token - process form
echo "Form submission successful!";
// Process your form data here
} else {
// Invalid token - show error
echo "Verification failed. Please try again.";
error_log('Turnstile validation failed: ' . implode(', ', $validation['error-codes']));
}
?>
import requests

def validate_turnstile(token, secret, remoteip=None):
    url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'

    data = {
        'secret': secret,
        'response': token
    }

    if remoteip:
        data['remoteip'] = remoteip

    try:
        response = requests.post(url, data=data, timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.RequestException as e:
        print(f"Turnstile validation error: {e}")
        return {'success': False, 'error-codes': ['internal-error']}

# Usage with Flask
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET_KEY = 'your-secret-key'

@app.route('/submit-form', methods=['POST'])
def submit_form():
    token = request.form.get('cf-turnstile-response')
    remoteip = request.headers.get('CF-Connecting-IP') or \
               request.headers.get('X-Forwarded-For') or \
               request.remote_addr

    validation = validate_turnstile(token, SECRET_KEY, remoteip)

    if validation['success']:
        # Valid token - process form
        return jsonify({'status': 'success', 'message': 'Form submitted successfully'})
    else:
        # Invalid token - reject submission
        return jsonify({
            'status': 'error',
            'message': 'Verification failed',
            'errors': validation['error-codes']
        }), 400
import org.springframework.web.client.RestTemplate;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

@Service
public class TurnstileService {
private static final String SITEVERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
private final String secretKey = "your-secret-key";
private final RestTemplate restTemplate = new RestTemplate();

    public TurnstileResponse validateToken(String token, String remoteip) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("secret", secretKey);
        params.add("response", token);
        if (remoteip != null) {
            params.add("remoteip", remoteip);
        }

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

        try {
            ResponseEntity<TurnstileResponse> response = restTemplate.postForEntity(
                SITEVERIFY_URL, request, TurnstileResponse.class);
            return response.getBody();
        } catch (Exception e) {
            TurnstileResponse errorResponse = new TurnstileResponse();
            errorResponse.setSuccess(false);
            errorResponse.setErrorCodes(List.of("internal-error"));
            return errorResponse;
        }
    }

}

// Controller usage
@PostMapping("/submit-form")
public ResponseEntity<?> submitForm(
@RequestParam("cf-turnstile-response") String token,
HttpServletRequest request) {

    String remoteip = request.getHeader("CF-Connecting-IP");
    if (remoteip == null) {
        remoteip = request.getHeader("X-Forwarded-For");
    }
    if (remoteip == null) {
        remoteip = request.getRemoteAddr();
    }

    TurnstileResponse validation = turnstileService.validateToken(token, remoteip);

    if (validation.isSuccess()) {
        // Valid token - process form
        return ResponseEntity.ok("Form submitted successfully");
    } else {
        // Invalid token - reject submission
        return ResponseEntity.badRequest()
            .body("Verification failed: " + validation.getErrorCodes());
    }

}
using System.Text.Json;

public class TurnstileService
{
    private readonly HttpClient _httpClient;
    private readonly string _secretKey = "your-secret-key";
    private const string SiteverifyUrl = "https://challenges.cloudflare.com/turnstile/v0/siteverify";

    public TurnstileService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<TurnstileResponse> ValidateTokenAsync(string token, string remoteip = null)
    {
        var parameters = new Dictionary<string, string>
        {
            { "secret", _secretKey },
            { "response", token }
        };

        if (!string.IsNullOrEmpty(remoteip))
        {
            parameters.Add("remoteip", remoteip);
        }

        var postContent = new FormUrlEncodedContent(parameters);

        try
        {
            var response = await _httpClient.PostAsync(SiteverifyUrl, postContent);
            var stringContent = await response.Content.ReadAsStringAsync();

            return JsonSerializer.Deserialize<TurnstileResponse>(stringContent);
        }
        catch (Exception ex)
        {
            return new TurnstileResponse
            {
                Success = false,
                ErrorCodes = new[] { "internal-error" }
            };
        }
    }
}

// Controller usage
[HttpPost("submit-form")]
public async Task<IActionResult> SubmitForm([FromForm] string cfTurnstileResponse)
{
    var remoteip = HttpContext.Request.Headers["CF-Connecting-IP"].FirstOrDefault() ??
                   HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault() ??
                   HttpContext.Connection.RemoteIpAddress?.ToString();

    var validation = await _turnstileService.ValidateTokenAsync(cfTurnstileResponse, remoteip);

    if (validation.Success)
    {
        // Valid token - process form
        return Ok("Form submitted successfully");
    }
    else
    {
        // Invalid token - reject submission
        return BadRequest($"Verification failed: {string.Join(", ", validation.ErrorCodes)}");
    }
}

Pokročilé techniky ověřování

Klíče idempotence pro opakování operace
const crypto = require("crypto");

async function validateWithRetry(token, remoteip, maxRetries = 3) {
	const idempotencyKey = crypto.randomUUID();

	for (let attempt = 1; attempt <= maxRetries; attempt++) {
		try {
			const formData = new FormData();
			formData.append("secret", SECRET_KEY);
			formData.append("response", token);
			formData.append("remoteip", remoteip);
			formData.append("idempotency_key", idempotencyKey);

			const response = await fetch(
				"https://challenges.cloudflare.com/turnstile/v0/siteverify",
				{
					method: "POST",
					body: formData,
				},
			);

			const result = await response.json();

			if (response.ok) {
				return result;
			}

			// If this is the last attempt, return the error
			if (attempt === maxRetries) {
				return result;
			}

			// Wait before retrying (exponential backoff)
			await new Promise((resolve) =>
				setTimeout(resolve, Math.pow(2, attempt) * 1000),
			);
		} catch (error) {
			if (attempt === maxRetries) {
				return { success: false, "error-codes": ["internal-error"] };
			}
		}
	}
}
Rozšířená validace s vlastními kontrolami
async function validateTurnstileEnhanced(
	token,
	remoteip,
	expectedAction = null,
	expectedHostname = null,
) {
	const validation = await validateTurnstile(token, remoteip);

	if (!validation.success) {
		return {
			valid: false,
			reason: "turnstile_failed",
			errors: validation["error-codes"],
		};
	}

	// Check if action matches expected value (if specified)
	if (expectedAction && validation.action !== expectedAction) {
		return {
			valid: false,
			reason: "action_mismatch",
			expected: expectedAction,
			received: validation.action,
		};
	}

	// Check if hostname matches expected value (if specified)
	if (expectedHostname && validation.hostname !== expectedHostname) {
		return {
			valid: false,
			reason: "hostname_mismatch",
			expected: expectedHostname,
			received: validation.hostname,
		};
	}

	// Check token age (warn if older than 4 minutes)
	const challengeTime = new Date(validation.challenge_ts);
	const now = new Date();
	const ageMinutes = (now - challengeTime) / (1000 * 60);

	if (ageMinutes > 4) {
		console.warn(`Token is ${ageMinutes.toFixed(1)} minutes old`);
	}

	return {
		valid: true,
		data: validation,
		tokenAge: ageMinutes,
	};
}

// Usage
const result = await validateTurnstileEnhanced(
	token,
	remoteip,
	"login", // expected action
	"example.com", // expected hostname
);

if (result.valid) {
	// Process the request
	console.log("Validation successful:", result.data);
} else {
	// Handle validation failure
	console.log("Validation failed:", result.reason);
}

Formát odpovědi API

Příklad
{
  "success": true,
  "challenge_ts": "2022-02-28T15:14:30.096Z",
  "hostname": "example.com",
  "error-codes": [],
  "action": "login",
  "cdata": "sessionid-123456789",
  "metadata": {
    "ephemeral_id": "x:9f78e0ed210960d7693b167e"
  }
}
Příklad
{
  "success": false,
  "error-codes": ["invalid-input-response"]
}

Pole odpovědi

Pole Popis
success Boolean udávající, zda ověření proběhlo úspěšně
challenge_ts Časové razítko ve formátu ISO 8601, kdy byla výzva vyřešena
hostname Hostname, na kterém byla výzva zobrazena
error-codes Pole kódů chyb (pokud ověření selhalo)
action Vlastní identifikátor akce ze strany klienta
cdata Vlastní payload ze strany klienta
metadata.ephemeral_id ID otisku zařízení (pouze Enterprise)

Přehled kódů chyb

Kód chyby Popis Nutný zásah
missing-input-secret Parametr secret nebyl zadán Ověřte, že je secret key součástí požadavku
invalid-input-secret Tajný klíč je neplatný nebo mu vypršela platnost Zkontrolujte svůj tajný klíč v Cloudflare dashboardu
missing-input-response Parametr response nebyl předán Ověřte, že je token součástí požadavku
invalid-input-response Token je neplatný, poškozený nebo vypršel Uživatel by měl výzvu zkusit znovu
bad-request Požadavek má chybný formát Zkontrolujte formát požadavku a parametry
timeout-or-duplicate Token už byl ověřen Každý token lze použít pouze jednou
internal-error Došlo k interní chybě Opakujte požadavek

Implementace

Ukázková implementace
class TurnstileValidator {
	constructor(secretKey, timeout = 10000) {
		this.secretKey = secretKey;
		this.timeout = timeout;
	}

	async validate(token, remoteip, options = {}) {
		// Input validation
		if (!token || typeof token !== "string") {
			return { success: false, error: "Invalid token format" };
		}

		if (token.length > 2048) {
			return { success: false, error: "Token too long" };
		}

		// Prepare request
		const controller = new AbortController();
		const timeoutId = setTimeout(() => controller.abort(), this.timeout);

		try {
			const formData = new FormData();
			formData.append("secret", this.secretKey);
			formData.append("response", token);

			if (remoteip) {
				formData.append("remoteip", remoteip);
			}

			if (options.idempotencyKey) {
				formData.append("idempotency_key", options.idempotencyKey);
			}

			const response = await fetch(
				"https://challenges.cloudflare.com/turnstile/v0/siteverify",
				{
					method: "POST",
					body: formData,
					signal: controller.signal,
				},
			);

			const result = await response.json();

			// Additional validation
			if (result.success) {
				if (
					options.expectedAction &&
					result.action !== options.expectedAction
				) {
					return {
						success: false,
						error: "Action mismatch",
						expected: options.expectedAction,
						received: result.action,
					};
				}

				if (
					options.expectedHostname &&
					result.hostname !== options.expectedHostname
				) {
					return {
						success: false,
						error: "Hostname mismatch",
						expected: options.expectedHostname,
						received: result.hostname,
					};
				}
			}

			return result;
		} catch (error) {
			if (error.name === "AbortError") {
				return { success: false, error: "Validation timeout" };
			}

			console.error("Turnstile validation error:", error);
			return { success: false, error: "Internal error" };
		} finally {
			clearTimeout(timeoutId);
		}
	}
}

// Usage
const validator = new TurnstileValidator(process.env.TURNSTILE_SECRET_KEY);

const result = await validator.validate(token, remoteip, {
	expectedAction: "login",
	expectedHostname: "example.com",
});

if (result.success) {
	// Process the request
} else {
	// Handle failure
	console.log("Validation failed:", result.error);
}

Testování

Dummy token vygenerovaný pomocí testovacího sitekey si můžete ověřit přes Siteverify API s testovacím secret key. Produkční secret key dummy tokeny vždy odmítne.

Viz Testování s dalšími informacemi.


Doporučené postupy

Zabezpečení

Výkon

Zpracování chyb