サーバーサイドバリデーション
Turnstileは、トークンを生成するフロントエンドウィジェットであり、そのトークンは暗号的に保護されています。攻撃者によってトークンが偽造されていないこと、またはまだ使用されていないことを確認するためには、Cloudflareのsiteverify APIを使用してトークンの有効性を確認する必要があります。
あなたは、ウェブサイトのバックエンドからTurnstileウィジェットの応答を検証するためにsiteverifyエンドポイントを呼び出さなければなりません。ウィジェットの応答は、siteverifyエンドポイントによって検証されるまで有効とは見なされません。応答が存在するだけでは、それを検証するには不十分であり、リプレイや偽造攻撃から保護されません。場合によっては、Turnstileが意図的にsiteverify APIによって拒否される無効な応答を生成することがあります。
成功コールバックを使用してTurnstileに発行されたトークンは、明示的または暗黙的なレンダリングを介して、siteverifyエンドポイントを使用して検証する必要があります。siteverify APIは、トークンを一度だけ検証します。トークンがすでにチェックされている場合、siteverify APIは、トークンがすでに使用されていることを示すエラーを返します。
siteverifyエンドポイントには、secret keyを渡す必要があります。このキーは、sitekeyに関連付けられています。secret keyは、ウィジェットを作成する際にsitekeyとともに提供されます。さらに、応答はsiteverifyエンドポイントに渡す必要があります。
応答は一度だけ検証されることができます。同じ応答が二度提示された場合、二回目以降のリクエストは、応答がすでに使用されていることを示すエラーを生成します。アプリケーションが失敗したリクエストを再試行する必要がある場合、冪等性機能を利用する必要があります。これは、最初に応答を検証する際にPOSTリクエストのidempotency_keyパラメータとしてUUIDを提供し、その応答に対する以降のリクエストでも同じUUIDを提供することで実現できます。
curl 'https://challenges.cloudflare.com/turnstile/v0/siteverify' --data 'secret=verysecret&response=<RESPONSE>'{ "success": true, "error-codes": [], "challenge_ts": "2022-10-06T00:07:23.274Z", "hostname": "example.com"}// これはデモ用のsecret keyです。本番環境では、// secret keyを安全に保管することをお勧めします。const SECRET_KEY = "1x0000000000000000000000000000000AA";
async function handlePost(request) { const body = await request.formData(); // Turnstileは"cf-turnstile-response"にトークンを注入します。 const token = body.get("cf-turnstile-response"); const ip = request.headers.get("CF-Connecting-IP");
// "/siteverify" APIエンドポイントを呼び出してトークンを検証します。 let formData = new FormData(); formData.append("secret", SECRET_KEY); formData.append("response", token); formData.append("remoteip", ip);
const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; const result = await fetch(url, { body: formData, method: "POST", });
const outcome = await result.json(); if (outcome.success) { // ... }}// これはデモ用のsecret keyです。本番環境では、// secret keyを安全に保管することをお勧めします。const SECRET_KEY = '1x0000000000000000000000000000000AA';async function handlePost(request) { const body = await request.formData(); // Turnstileは"cf-turnstile-response"にトークンを注入します。 const token = body.get('cf-turnstile-response'); const ip = request.headers.get('CF-Connecting-IP'); // "/siteverify" APIエンドポイントを呼び出してトークンを検証します。 const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; const result = await fetch(url, { body: JSON.stringify({ secret: SECRET_KEY, response: token, remoteip: ip }), method: 'POST', headers: { 'Content-Type': 'application/json' } }); const outcome = await result.json(); if (outcome.success) { // ... }}// これはデモ用のsecret keyです。本番環境では、// secret keyを安全に保管することをお勧めします。const SECRET_KEY = "1x0000000000000000000000000000000AA";
async function handlePost(request) { const body = await request.formData(); // Turnstileは"cf-turnstile-response"にトークンを注入します。 const token = body.get("cf-turnstile-response"); const ip = request.headers.get("CF-Connecting-IP");
// "/siteverify" APIエンドポイントを呼び出してトークンを検証します。 let formData = new FormData(); formData.append("secret", SECRET_KEY); formData.append("response", token); formData.append("remoteip", ip); const idempotencyKey = crypto.randomUUID(); formData.append("idempotency_key", idempotencyKey);
const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; const firstResult = await fetch(url, { body: formData, method: "POST", }); const firstOutcome = await firstResult.json(); if (firstOutcome.success) { // ... }
// 同じトークンに対する"/siteverify" APIエンドポイントへの // 以降の検証リクエストで、関連する冪等性キーも提供します。 const subsequentResult = await fetch(url, { body: formData, method: "POST", });
const subsequentOutcome = await subsequentResult.json(); if (subsequentOutcome.success) { // ... }}// これはデモ用のsecret keyです。本番環境では、// secret keyを安全に保管することをお勧めします。const SECRET_KEY = '1x0000000000000000000000000000000AA';async function handlePost(request) { const body = await request.formData(); // Turnstileは"cf-turnstile-response"にトークンを注入します。 const token = body.get('cf-turnstile-response'); const ip = request.headers.get('CF-Connecting-IP'); // "/siteverify" APIエンドポイントを呼び出してトークンを検証します。 const idempotencyKey = crypto.randomUUID(); const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; const firstResult = await fetch(url, { body: JSON.stringify({ secret: SECRET_KEY, response: token, remoteip: ip, idempotency_key: idempotencyKey }), method: 'POST', headers: { 'Content-Type': 'application/json' } }); const firstOutcome = await firstResult.json(); if (firstOutcome.success) { // ... } // 同じトークンに対する"/siteverify" APIエンドポイントへの // 以降の検証リクエストで、関連する冪等性キーも提供します。 const subsequentResult = await fetch(url, { body: JSON.stringify({ secret: SECRET_KEY, response: token, remoteip: ip, idempotency_key: idempotencyKey }), method: 'POST', headers: { 'Content-Type': 'application/json' } }); const subsequentOutcome = await subsequentResult.json(); if (subsequentOutcome.success) { // ... }}| POSTパラメータ | 必須/オプション | 説明 |
|---|---|---|
secret | 必須 | ウィジェットのsecret key。secret keyはCloudflareダッシュボードのTurnstileのウィジェット設定で見つけることができます。 |
response | 必須 | あなたのサイトでのTurnstileクライアントサイドレンダリングによって提供された応答。 |
remoteip | オプション | 訪問者のIPアドレス。 |
idempotency_key | オプション | 応答に関連付けられるUUID。 |
siteverifyエンドポイントは、reCAPTCHAやhCaptchaのsiteverifyエンドポイントと似た動作をします。
APIはapplication/x-www-form-urlencodedおよびapplication/jsonリクエストを受け入れますが、応答タイプは常にapplication/jsonです。
応答には常にsuccessプロパティが含まれ、操作が成功したかどうかを示します。
{ "success": true, "challenge_ts": "2022-02-28T15:14:30.096Z", "hostname": "example.com", "error-codes": [], "action": "login", "cdata": "sessionid-123456789"}challenge_tsは、チャレンジが解決された時刻のISOタイムスタンプです。hostnameは、チャレンジが提供されたホスト名です。actionは、クライアント側でウィジェットに渡された顧客ウィジェット識別子です。これは、同じsitekeyを使用するウィジェットを分析で区別するために使用されます。その整合性は、攻撃者からの変更によって保護されています。actionが期待される値と一致することを検証することをお勧めします。cdataは、クライアント側でウィジェットに渡された顧客データです。これは、顧客が状態を伝えるために使用できます。その整合性は、攻撃者からの変更によって保護されています。error-codesは、発生したエラーのリストです。
検証に失敗した場合、応答は次のようになります。
{ "success": false, "error-codes": ["invalid-input-response"]}検証エラーは、successプロパティがfalseに設定されていることで示されます。応答が検証に失敗した理由を示すエラーコードのリストが提供されます。応答には、Turnstile siteverifyが応答を正常に解析できたかどうかに基づいて、追加のフィールドが含まれる場合があります。
エラーコード | 説明 |
|---|---|
missing-input-secret | secretパラメータが渡されていません。 |
invalid-input-secret | secretパラメータが無効または存在しません。 |
missing-input-response | responseパラメータ(トークン)が渡されていません。 |
invalid-input-response | responseパラメータ(トークン)が無効または期限切れです。ほとんどの場合、これは偽のトークンが使用されたことを意味します。エラーが続く場合は、カスタマーサポートに連絡してください。 |
bad-request | リクエストが不正なため拒否されました。 |
timeout-or-duplicate | responseパラメータ(トークン)がすでに検証されています。これは、トークンが5分前に発行され、もはや有効でないか、すでに引き換えられたことを意味します。 |
internal-error | 応答を検証中に内部エラーが発生しました。リクエストを再試行できます。 |