Cloudflare Turnstile をさわってみる

# 目的

Cloudflare Turnstile　を触って理解します。

成果物 @ GitHub => [kyouheicf/turnstile-demo](https://github.com/kyouheicf/turnstile-demo)

# Cloudflare Turnstile の特徴
- Javascript タグを挿入するだけで `challenges.cloudflare.com` と通信してチャレンジをおこなってくれる
- Cloudflare にゾーン登録して CDN を採用しなくても使える
- Managed Challenge と同じ仕組みで、なるべくキャプチャを解く必要がないよう、機械学習も組み込み、高速なユーザ体験を提供する

> - [Cloudflare Turnstile · Cloudflare Turnstile docs](https://developers.cloudflare.com/turnstile/)
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/d68c5909-46f5-3c5b-fd20-5eecc4319291.png)

> - [The end of the road for Cloudflare CAPTCHAs](https://blog.cloudflare.com/end-cloudflare-captcha/)
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/c55ab771-e566-3f8d-5e03-70ad0179b25a.png)

# ウィジェットタイプ

- [Widget types · Cloudflare Turnstile docs](https://developers.cloudflare.com/turnstile/reference/widget-types/)

ページ内に配置するウィジェットのタイプを以下の中から選択できる。ほとんどの場合、Managed で問題ないでしょう。

- Managed (recommended)
    - いくつかの計算要求等の結果によって、インタラクティブな操作をマネージドで要求することがある
- Non-Interactive
    - インタラクティブな操作要求はない
- ​​Invisible
    - ページ内のウィジェットは不可視の状態でチャレンジを実行

# サイト作成

1ウィジェットを配置するにあたり、サイト作成時に取得できる sitekey (public) と secret key (private) が必要です。

- [Testing locally · Cloudflare Turnstile docs](https://developers.cloudflare.com/turnstile/reference/testing/#testing-locally)

ローカルでテストできるように `localhost` や `127.0.0.1` をドメインに追加しておくと良いです。

>https://developers.cloudflare.com/turnstile/frequently-asked-questions/#can-i-use-turnstile-when-developing-locally
> 以下に提供するダミーのサイトキーは、localhostを含むどのドメインからでも使用できます。
>Cloudflareは、本番で使用するサイトキーはローカルドメイン（localhost、127.0.0.1）を許可しないことを推奨していますが、ユーザは許可するドメインのリストにローカルドメインを追加することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/dac7129f-e646-1e2f-b2e1-f5d21a3bb089.png)

# Implicit / Explicit レンダリング
以下のコードを参考に、Visual Studio Code でプレビューが確認できます。

- [cloudflare/turnstile-demo-workers: A simple demo with a Turnstile-protected form, using Cloudflare Workers.](https://github.com/cloudflare/turnstile-demo-workers)

disabled属性を使えば、ウィジェットの動きに連動してボタンを有効化・無効化することも可能です。

- [【HTML】disabled属性の使い方が初級者に分かりにくい](https://forties-freelance-engineer.net/how-to-use-attribute-disabled/)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/72a0594f-ae7b-4a20-0060-d5924858b45c.png)

## Implicit レンダリング
`div class="cf-turnstile"` が決めうちで、コード量が少なくシンプルに記述できます。

```html:implicit.html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Turnstile Demo</title>
    <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
</head>

<body>

    <form action="posttest.php" method="post">
        Username: <input type="text" name="username" /> <br>
        Blood Group: <input type="text" name="bloodgroup" /> <br>
        <!-- The Turnstile widget will be injected in the following div. -->
        <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-callback="javascriptCallback" data-language="ja"></div>
        <!-- end. -->
        <input type="submit" id="submit" disabled />
    </form>

    <script>
        window.javascriptCallback = function () {
            document.getElementById('submit').disabled = false;
            console.log(`Challenge Success.`);
        }
    </script>
</body>

</html>
```

## Explicit レンダリング
`render()` を使ってウィジェットのレンダリングすることができますが、ある時点までチャレンジの実行を延期したい場合には、 `execute` モードを使用して、チャレンジが実行され、トークンが生成されるタイミングを制御できたり、と柔軟性があります。

```html:explicit.html
<!DOCTYPE html>
<html lang="en">

<head>
    <title>Turnstile Demo</title>
    <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback" defer></script>
</head>

<body>

    <form action="posttest.php" method="post">
        Username: <input type="text" name="username" /> <br>
        Blood Group: <input type="text" name="bloodgroup" /> <br>
        <!-- The Turnstile widget will be injected in the following div. -->
        <div id="turnstileWidget"></div>
        <!-- end. -->
        <input type="submit" id="submit" disabled />
    </form>

    <script>
        // This function is called when the Turnstile script is loaded and ready to be used.
        // The function name matches the "onload=..." parameter.
        // if using synchronous loading, will be called once the DOM is ready
        window.onloadTurnstileCallback = function () {
            turnstile.render('#turnstileWidget', {
                sitekey: 'YOUR_SITE_KEY',
                theme: 'auto',
                language: 'ja',
                callback: function (token) {
                    document.getElementById('submit').disabled = false;
                    console.log(`Challenge Success. Token === ${token}`);
                },
            });
        }
    </script>
</body>

</html>
```

# Server-side validation

ウィジェットのレスポンスを `cf-turnstile-response` として受け取れることができ、その内容を使って有効性を検証 `siteverify` することができます。

> https://developers.cloudflare.com/turnstile/frequently-asked-questions/#why-does-a-turnstile-token-need-to-be-verified-using-siteverify
> Turnstileは、暗号的に保護されたトークンを作成するフロントエンド・ウィジェットである。しかし、顧客は自分のエンドでトークンの有効性をチェックすることはできません。
トークンが攻撃者によって偽造されたものでないこと、またはまだ消費されていないことを確認するために、顧客はCloudflareのsiteverify APIを使用してトークンの有効性をチェックする必要があります。
> https://developers.cloudflare.com/turnstile/frequently-asked-questions/#can-the-front-end-use-siteverify
> siteverifyAPIは、認証に使われたsecretkeyを明らかにするかもしれないので、フロントエンドから呼び出してはならない。攻撃者は単純にフロントエンドを変更してsiteverifyチェックを全く行わないようにし、Turnstileを無効にしてしまうかもしれない。

以下のページを用意し、Submit します。

```posttest.php
<html>  
   <body>  
   
      Welcome <?php echo $_POST["username"]; ?> </br>  
      Your blood group is: <?php echo $_POST["bloodgroup"]; ?> </br></br> 
      cf-turnstile-response (Token) is: <?php echo $_POST["cf-turnstile-response"]; ?>  </br></br>

      siteverify result is: <?php
      $data = [];
      $data['secret'] = 'YOUR_SECRET_KEY';
      $data['response'] = $_POST["cf-turnstile-response"];
      // 送信データをURLエンコード
      $data = http_build_query($data, "", "&");
      $curl = curl_init();
      curl_setopt_array($curl, [
      CURLOPT_URL => "https://challenges.cloudflare.com/turnstile/v0/siteverify",
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_POSTFIELDS => $data]);
      $response = curl_exec($curl);
      $err = curl_error($curl);
      curl_close($curl);
      if ($err) {
         echo "cURL Error #:" . $err;
      } else {
         echo $response;
      }?>

   </body>  
</html> 
```

以下のような結果が得られ、サーバーサイドでシークレットキーを使った検証ができることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/d3e8e16c-447e-e426-8f9e-e84bbc0fba9c.png)

```json:siteverify_result.json
{
  "success": true,
  "error-codes": [],
  "challenge_ts": "2023-09-13T16:03:09.099Z",
  "hostname": "grey.example.com",
  "action": "",
  "cdata": "",
  "metadata": {
    "interactive": false
  }
}
```

# Solve Rate
Analytics 画面で Solve Rate を確認することができます。

Solve Rate は時間の経過とともに比較的安定すると予想されますが、解決率の変化は、ボットからの攻撃を受けているなど、ウェブサイト上で何かが起こっていることを示します。

- [Analytics · Cloudflare Turnstile docs](https://developers.cloudflare.com/turnstile/reference/turnstile-analytics/)

- [Tooltips · Cloudflare Turnstile docs](https://developers.cloudflare.com/turnstile/reference/tooltips/)

>https://developers.cloudflare.com/turnstile/frequently-asked-questions/#what-is-visitor-solve-rate
>Visitor Solve Rate は、発行されたチャレンジと比較して、発行されたが必ずしもサイト検証されていないトークンの割合です。
> https://developers.cloudflare.com/turnstile/frequently-asked-questions/#what-is-api-solve-rate
> API Solve Rate（API解決率）は、発行されたトークンと比較してサイト検証されたトークンの割合である。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101361/9caae4ca-772a-1606-fb13-bbdfee07b7ed.png)


# まとめ

導入も容易なため、非常に便利に使えそうなことがわかりました。

以下にもある通り、いろいろなツールに組み込んで使えそうです。

- [Community resources · Cloudflare Turnstile docs](https://developers.cloudflare.com/turnstile/community-resources/)

また、`compat=recaptcha` のオプションも用意されていたり、移行もシームレスにおこなえそうです。

- [Migrating from reCAPTCHA · Cloudflare Turnstile docs](https://developers.cloudflare.com/turnstile/migration/migrating-from-recaptcha/)