Google超え(?)の安全・高速認証ができるCloudflare TurnstileをRailsに導入


## TurnstileとCHAPTCHA
今回はTurnstileとCHAPTCHAについて説明しTurnstileをRailsのアプリに導入していきたいと思います。
先日CloudflareからTurnstileという認証テストのβ版が発表され、ユーザーのプライバシーを担保し高速なUXを提供し無料で利用できるということで従来のCAPTCHAの代替となりうるのではないかとされています。TurnstileはCAPTCHAとは違いTurnstileは高性能で不視性を保つ認証テストです。認証はユーザーの目の見えないところで起こり、人間が操作していることを判断の基準にします。


> Turnstile は、CAPTCHA の「ユーザーフレンドリーでプライバシーを保護する代替手段」として提示されています。プレス リリースによると、Cloudflare によると、通常、通過に平均 32 秒かかる人物の確認に使用されるインタラクティブなチャレンジが不要になり、プロセス全体が 1 秒に短縮されます

https://www.cloudflare.com/ja-jp/press-releases/2022/privacy-preserving-captcha-alternative/
### CAPTCHAの背景

CAPTHCAというのはサービスを操作しているのが人間かbotかを判別するための認証方法です。


CAPTCHAを活用すれば、botが行う自動的で行われる大量アクセスを防ぐことができスパムによるサーバーダウンや不正アカウントの作成を防ぐために開発されました。

よくあるのがサービスを使用する際によく歪んでいる文字を入力したり画像の〇〇を選択してくださいのような認証方法だと思います。
### CHAPTCHAの問題点~認証~
以下のような認証方法だと一見よくわからない視覚的なパズルや歪んでいる文字を与えられた時に、一分以上もの時間が取られてしまうことがありませんか。

私時々CAPTHCA認証に失敗してしまい、AWSのログイン時に人間であるにも関わらず一定期間ログインできなくなった経験があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880777/c3cec138-059f-936d-c4ed-b46ed5166b42.png)
### CHAPTCHAの問題点~プライバシー~
またCAPTCHAはユーザーデータを取り込みプライバシーを犠牲にするという悪評も散見されます。
一般的に使用されているGoogleのreCAPTCHAはGoogleアカウントとの連携を求められユーザー情報をGoogle側に提供してしまうことにつながります。(Googleは広告会社なので）


## Turnstileの導入

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880777/5da2f2d8-2b87-d084-6d03-90c4cfc27f42.png)



### サイト登録 
まず初めにサイトを登録していきます。
今回はQiitaに投稿するということなので`localhost`でCloudflareのTurnstileに登録をしました。
[※本番環境のサイトキーでlocalhostを登録するのは推奨されていません](https://developers.cloudflare.com/turnstile/frequently-asked-questions/)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880777/76dad1e4-8bc8-8358-642d-b68747217a1d.png)

次にTurnstileのWidgetの種類を選択しSite KeyとSecret Keyを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880777/50f0d00a-522c-501f-052c-9dde33d24823.png)



## フロントエンド
取得したサイトキーを以下のコードの引数に入れます。


```app/views/users/registrations/new.html.haml
     .box-login
        .actions
        = f.submit "アカウント登録", class: "btn-text"
      .cf-turnstile.mb-2{"data-callback" => "javascriptCallback", "data-controller" => "cloudflare-turnstile", "data-sitekey" => "サイトキー"}
      .box-newusr
        = link_to "ログイン画面へ", user_session_path
```

## バックエンド
バックエンドではprepend_before_actionを使用してユーザー登録前にTurnstileで認証を行わせます。
Turnstileはbefore_actionの前に行うアクションを行う必要があるためprepend_before_actionを使用します。

```app/controllers/users/registrations_controller.rb
  prepend_before_action :validate_cloudflare_turnstile, only: [:create]

private
  def validate_cloudflare_turnstile
    validation = CloudflareTurnstile.validate(params[:"cf-turnstile-response"], request.remote_ip)
    return if validation

    self.resource = resource_class.new sign_up_params
    resource.validate
    set_minimum_password_length
    respond_with_navigational(resource) { render :new }
  end

```
Turnstileをdeviceで使用するためにはroutingを編集してあげる必要があります。

```config/routes.rb | 
# 変更前
devise_for :users

# 変更後
devise_for :users, controllers: {
  registrations: 'users/registrations'
}
```

validateの論理を作っていきます。

```models/cloudflare_turnstile.rb

require "net/http"

class CloudflareTurnstile
  SECRET_KEY = "シークレットキー".freeze
  ALL_HTTP_ERRORS = [Errno::ETIMEDOUT]
  def self.validate(payload, remote_ip)
    new(payload, remote_ip).validate
  end

 def initialize(payload, remote_ip)
    @payload = payload
    @remote_ip = remote_ip
  end

  def validate
    return false unless @payload
    response = validate_from_cloudflare
    response["success"]
  end

  private

  def validate_from_cloudflare
    retries ||= 0
    url = URI("https://challenges.cloudflare.com/turnstile/v0/siteverify")
    response = Net::HTTP.post_form(url, secret: SECRET_KEY, response: @payload, remoteip: @remote_ip)
    JSON.parse(response.body)
  rescue *ALL_HTTP_ERRORS
    retry if (retries += 1) < 3
  end
end
```


### 参考サイト
https://www.theverge.com/2022/9/28/23367035/cloudflare-turnstile-captcha-bot-blocker-beta

https://dev.classmethod.jp/articles/try-cloudflare-turnstile-with-python/

https://pikawaka.com/rails/recaptcha