reCAPTCHA v3 に対して Autify でテスト自動化してみた

この記事は [Autify アドベントカレンダー2021](https://qiita.com/advent-calendar/2021/autify) の17日目のエントリです。

こんにちは! Autify カスタマーサポートエンジニアの hiroxyy です。
Qiita 初投稿となる本エントリでは、 reCAPTCHA v3 が導入されているサイトに対して Autify でテストを実行したらどうなるのか、その結果をまとめました。

# reCAPTCHA v3 とは
Google が提供している、人間と bot を識別するための API です。
以下のような特徴があります。

- サイトに訪問したユーザーにチェックボックスのチェックや文字識別のアクションを求めないため、ユーザビリティを低下させない
- API が返却するスコア(0.0 ~ 1.0)に応じてどう処理するかを、 **サイト側で実装する必要がある**
- サイトへのアクセスが増えるほど、API は学習し返却するスコアもそれに基づき変わる

また以下の公式の FAQ には、過去のバージョンである reCAPTCHA v2 とどちらを使うべきかについても触れられています。
[Should I use reCAPTCHA v2 or v3? | Google Developers](https://developers.google.com/recaptcha/docs/faq#should-i-use-recaptcha-v2-or-v3)

> reCAPTCHA v3 is for site owners who want more data about their traffic. For more information, see the reCAPTCHA v3 developer guide.
> We support security and usability for v2.

こっちを使うべし！とは記載されていないので、 Google としては必要に応じて v2, v3 を使い分けてくれ、といった感じになるんでしょうか。
古いバージョンである v2 に対してもサポートすることが明示されており、 v2 が廃止予定ということもなさそうですね！

v2 と v3 の違いをより詳しく知りたい方は、個人的に以下記事がわかりやすくてオススメです！
[reCAPTCHA v2 と v3 の違い - Qiita](https://qiita.com/koseki/items/a2436b831afcdf73b946)

# reCAPTCHA v3 の導入
今回は [Ruby on Rails チュートリアルのサンプルアプリケーション](https://github.com/yasslab/sample_apps/tree/main/6_1/ch14)のログインページに導入してみました。
M1 Mac 上で初めて Rails アプリケーションを動かしたので、reCAPTCHA v3 の実装以外にも、環境構築になかなか時間を取られました。
環境構築ツライ:innocent:

実装に関しては、 [Google reCAPTCHA v3をRuby On Railsに入れてみました〜 - Qiita](https://qiita.com/HavenSpring/items/98fa8960746c944b6e8c) を参考にしました。
具体的なコードは↓↓です。

## ヘッダーに reCAPTCHA のスクリプト追加
```erb:application.html.erb
<script src="https://www.google.com/recaptcha/api.js?render=reCAPCHAサイトキー"></script>
```

## ログインフォームに Hidden と、ボタンクリック時の処理を追加
`grecaptcha.execute` によって生成されるトークンの有効期限は2分です。
[Placement on your website | Google Developers](https://developers.google.com/recaptcha/docs/v3#placement_on_your_website)に記載があるように、ページロードではなくユーザーのアクションが実行されたタイミングでトークンを生成するようにしましょう。
仮にページロード時にトークンを生成すると、ユーザーのアクション前にトークンが切れてしまい reCAPTCHA v3からスコアが正常に返却されない問題が起き得ます:warning:

```erb:sessions/new.html.erb

<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with(url: login_path, scope: :session, local: true, id: "form") do |f| %>
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= link_to "(forgot password)", new_password_reset_path %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>
      <%= f.submit "Log in", class: "btn btn-primary", id: "login" %>
      <input type="hidden" name="recaptcha_response" id="recaptchaResponse">
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

<script>
$("#login").on("click", function(e) {
  // データ送信をSTOP
  e.preventDefault();
  // reCAPTCHA tokenを生成
  grecaptcha.execute('reCAPCHAサイトキー', { action: 'login' }).then(function (token) {
    var recaptchaResponse = document.getElementById('recaptchaResponse');
    recaptchaResponse.value = token;
    // フォームを送信
    document.getElementById("form").submit()
  });
});
</script>

```

## reCAPTCHA v3 からスコアを受け取り bot の判定処理を追加
今回は[Interpreting the score | Google Developers](https://developers.google.com/recaptcha/docs/v3#interpreting_the_score) の記載に従い、 `0.5` を 人間と bot のしきい値として利用しました。

```ruby:
class SessionsController < ApplicationController

  RECAPTCHA_SECRET_KEY = 'reCAPTCHAシークレットキー'
  RECAPTCHA_SITEVERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'

  def new
  end

  def create

    # siteverifyAPIを呼び出してボットかチェック
    siteverify_uri = URI.parse("#{RECAPTCHA_SITEVERIFY_URL}?response=#{params[:recaptcha_response]}&secret=#{RECAPTCHA_SECRET_KEY}")
    response = Net::HTTP.get_response(siteverify_uri)
    json_response = JSON.parse(response.body)

    # scoreの保存
    session[:score] = json_response['score']

    # reCAPTCHAのレスポンスデータが成功かスコアが0.5以上なら人間のアクションだと判断
    # スコアについては運用中変更する必要がある
    if json_response['success'] && json_response['score'] > 0.5
      user = User.find_by(email: params[:session][:email].downcase)
      if user && user.authenticate(params[:session][:password])
        if user.activated?
          log_in user
          params[:session][:remember_me] == '1' ? remember(user) : forget(user)
          redirect_back_or user
        else
          message  = "Account not activated. "
          message += "Check your email for the activation link."
          flash[:warning] = message
          redirect_to root_url
        end
      else
        flash.now[:danger] = 'Invalid email/password combination'
        render 'new'
      end
    else
      flash.now[:danger] = "不正アクセスです。score: #{json_response['score']}"
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end
```

# Autify を利用し、ログイン操作のテスト自動化
そもそも、reCAPTCHA v3 が組み込まれているページにテストすることは想定されているのでしょうか。
その答えは[公式のFAQ](https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do)にありました！

> For reCAPTCHA v3, create a separate key for testing environments. Scores may not be accurate as reCAPTCHA v3 relies on seeing real traffic.

テスト環境用のキーを作成し、それを利用してくださいとありますね。
今回は個人のお試しアプリケーションなのでテスト環境用のキーはないですが、実際はテスト環境用にキーを分けて、しきい値を `0.0` にして bot としてはじかれないようにした方がテスト自動化の観点だと楽そうですね😋

今回は Autify で以下のようなログイン操作と、その際の reCAPTCHA v3 から返却されるスコアがどうなっているかを確認するテストを作成してみました。
![ログイン_with_reCAPTCHA_v3_-_Autify.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146244/396b4ec4-2818-b789-7f41-91181a5fae30.png)

bot と判定されたらステップ6でログインページに戻されるので、テストが失敗するような作りになっています。
また、bot と判定されなくてもその時のスコアの値がどうなっているのかも確認したいので、ステップ7でスコアを確認するステップを追加しています。

## 実行結果
複数の実行環境で繰返しテストを実行したところ、テストを実行する毎にスコアが下がっていく実行環境があることがわかりました！
実際の、各環境における実行回数毎のスコアの変動を表にまとめています。
赤文字はしきい値を下回り bot 判定されたテスト実行です。

| テストを実行したブラウザ / OS         | 1回目 | 2回目 | 3回目 | 4回目 |
| ------------------------------- | --- | --- | --- | --- |
| Safari 14.0 / macOS Big Sur     | 0.9 | 0.9 | 0.9 | 0.7 |
| IE 11.0 / Windows 10            | 0.9 | 0.9 | 0.9 | 0.9 |
| IE 11.0 / Windows Server 2019   | 0.9 | 0.9 | 0.9 | 0.7 |
| Firefox 93.0 / macOS Big Sur    | 0.9 | 0.9 | 0.9 | 0.9 |
| Firefox 93.0 / Windows 10       | 0.9 | 0.9 | 0.9 | 0.9 |
| Edge 95.0 / Windows 10          | 0.9 | 0.9 | 0.9 | 0.7 |
| Edge 95.0 / Windows Server 2019 | 0.9 | 0.9 | 0.7 | 0.7 |
| Chrome 95.0 / macOS Big Sur     | 0.9 | 0.9 | 0.9 | 0.7 |
| Chrome 95.0 / Windows 10        | 0.9 | 0.9 | 0.9 | <font color="red">0.3</font> |
| Chrome 95.0 / Linux             | 0.9 | 0.9 | 0.7 | <font color="red">0.3</font> |

Firefox のスコアの高さがすごいｗ
サイトへのアクセスが増えるにつれて reCAPTCHA v3 が学習し、返却されるスコアが変わっているのがわかりますね。
実行回数がまだまだ少ないので、今後もどんどん変わりそうな予感がします。

# まとめ

- reCAPTCHA v3 は人間か bot かを判定するためのスコア(0.0 ~ 1.0)を返すのみ。スコアに基づいてどう処理するかはサイト側で実装が必要になる
- テスト環境用に reCAPTCHA v3のキーを分けて運用したほうが良い
- reCAPTCHA v3 が組み込まれたサイトに対して Autify でテストを実行した場合、reCAPTCHA v3 から返却されるスコアはテストの実行環境や、サイトへのアクセス数によってばらつきが出る
- テスト環境の場合は、 bot かどうか判別するしきい値に `0.0` を設定しておくことでテストの失敗を回避可能
- 本番環境の場合は、[固定IPアドレス](https://autify.zendesk.com/hc/ja/articles/900001161983)機能を利用し Autify からのアクセス時は reCAPTCHA v3 による処理を実行しないようにすることでテストの失敗を回避可能

# おまけ

Autify によるログインではなく、手動で色んな方法でログインした時にスコアがどうなるか試してみました。
サイトのアクセス数によってスコアは変わるので、あくまで参考です。
調べてみると[ reCAPTCHA v2・v3 API のレスポンスがわかるデモサイト](https://recaptcha-demo.appspot.com/)もあったので、遊んでみるとけっこう楽しいです👍

- Chrome のゲストモードでログイン: 0.9
- Chrome のシークレットモードでログイン: 0.9
- Chrome のモバイルエミュレートを利用し、iPhone Xでログイン: 0.9
- Chrome のモバイルエミュレートを利用し、カスタムデバイスでログイン: 0.9
- Chrome のユーザーエージェントを　`Goolebot` にしてログイン: 0.9
- Chrome のユーザーエージェントを　`Chrome / Windows` にしてログイン: 0.9
- Chrome のユーザーエージェントを　`hogehoge` にしてログイン: 0.9
- Chrome の Network throttling を　`Slow 3G` にしてログイン: 0.9

自分が試した範囲では見事に全て 0.9 となり、人間判定されています🎊

個人的にはどのようにスコアが計算されているのかは私達にはわかりようがないので、 reCAPTCHA v3 だけに頼るのは怖いなぁという感じがします。
かといって強固にしすぎてユーザビリティを損ねたくもないので、 bot をはじくための実装はまだまだ奥が深そうだと知ることができ、非常に勉強になりました🖌