rails_same_site_cookie gemで、RailsアプリにChrome 80向けのSameSite属性を指定する

## はじめに

以下の記事にあるとおり、Chrome 80では2020年2月17日の週以降にデフォルトのSameSite属性が変更されます。

[Chrome 80が密かに呼び寄せる地獄 ～ SameSite属性のデフォルト変更を調べてみた - Qiita](https://qiita.com/ahera/items/0c8276da6b0bed2b580c)

この変更が入ると、次のように「決済や認証などで外部サービスを利用し、外部サイトからPOSTで戻ってくるサイト」でユーザーが識別できないエラーが発生します。

![https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_117852_84f60298-2a3f-65b0-5ff6-daacf12a1c4b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/c9bcba25-0e88-37d0-ef02-26ed9ba71a2f.png)
（画像の引用元：[Chrome 80が密かに呼び寄せる地獄 ～ SameSite属性のデフォルト変更を調べてみた \- Qiita](https://qiita.com/ahera/items/0c8276da6b0bed2b580c)）

本記事ではRailsアプリケーションでこの問題に対応する方法を紹介します。

### 免責事項 (disclaimer)

この記事のとおりにあなたのRailsアプリケーションを変更して、何らかの不具合やセキュリティ上の問題が発生しても筆者は一切の責任を負いません。

技術的な背景と最新の技術情報を十分理解した上で、本記事の内容を適用してください。
（適用する必要がない場合は何もしないでください）

## 対応方法


rackのバージョンを2.1.0以上に上げます。
（2.1.0以上でないと、`SameSite=None`属性に対応していないため - [参考](https://github.com/rack/rack/blob/master/CHANGELOG.md#210---2020-01-10)）


（訂正：rails_same_site_cookieはrackの機能ではなく、独自にCookieを設定しているため、必ずしもrack 2.1.0以上に上げる必要はないようです）


[rails_same_site_cookie](https://github.com/pschinis/rails_same_site_cookie) gemをインストールします。

```ruby:Gemfile
gem 'rails_same_site_cookie'
```

```console
$ bundle install
```

対応は以上です。

### Rails 6.1以上の場合（2022.5.27追記）

Rails 6.1からは`SameSite=Lax`がデフォルト値になった関係でgemをインストールするだけでは機能しなくなりました（[参考](https://github.com/pschinis/rails_same_site_cookie/issues/31)）。

Rails 6.1以降でも引き続き使う場合は、`config/initializers`などで以下の設定も追加する必要があります。

```ruby 
Rails.application.configure do
  config.action_dispatch.cookies_same_site_protection = nil
end
```

### rails_same_site_cookie gemがやってくれること

`rails_same_site_cookie` gemをインストールすると、自動的に全cookieに`SameSite=None; Secure`属性が追加されます。

ただし、iOS 12とmacOS 10.14のSafariなど、`SameSite=None; Secure`属性を付けると不具合が発生するブラウザ（[参考](https://www.chromium.org/updates/same-site/incompatible-clients)）に対してはこの属性を付与しません。

### rails_same_site_cookie gemがやってくれないこと

rails_same_site_cookie gemはRails側のCookieを変更するだけで、JavaScript側で設定するCookieには何も変更を加えません。
もしJS内でCookieを設定しているコードがある場合は、何らかの対応が必要になります。（詳細未調査）

## 参考1：動作確認の手順（例）

「決済で外部サービスを利用し、外部サイトからPOSTで戻ってくるサイト」を想定した場合の確認手順です。
いきなり本番環境で試すのではなく、テスト環境（ステージング環境）で試すようにしてください。

1. [こちらの手順](https://qiita.com/ahera/items/0c8276da6b0bed2b580c#%E3%83%86%E3%82%B9%E3%83%88%E6%96%B9%E6%B3%95)に従って、Chromeの設定を変更する
2. Railsアプリサイト上で商品を購入し、外部の決済サービスに遷移する
3. 2分以上待つ（2分以上待たないとChromeがCookieを送信してしまうため）
4. 決済を実行して、自サイトに戻る
5. ログイン情報が維持されたまま、正常に決済が完了することを確認する（修正前は何らかの不具合が発生することも事前に確認しておく）

## 参考2：リクエストスペックでテストを書く（例）

`SameSite=None`属性が適切に付与されているかどうかを確認するリクエストスペックの記述例です。
（非SSLで接続する場合、rails_same_site_cookie gemは`SameSite=None`属性だけを付与し、`Secure`属性は付与しません）

ログイン処理はDeviseで実現されていることを前提とします。
テストコード内の`_your_app_session`は、適宜ご自身のアプリケーション名に合わせて変更してください。

```ruby:spec/requests/cookies_spec.rb
require 'rails_helper'

RSpec.describe 'Cookies', type: :request do
  describe 'Cookie の SameSite 属性' do
    before do
      user = create :user
      login_as user
    end
    it 'User-Agent指定無しの場合 SameSite=None がつく' do
      get new_user_session_path
      expect(response.headers['Set-Cookie']).to match /_your_app_session=.*SameSite=None/
    end

    it 'SameSite=Lax がデフォルトになる Chrome 80 では SameSite=None がつく' do
      mac_chrome_80 = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.42 Safari/537.36      '
      win_chrome_80 = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.16 Safari/537.36'

      get new_user_session_path, headers: { 'User-Agent' => mac_chrome_80 }
      expect(response.headers['Set-Cookie']).to match /_your_app_session=.*SameSite=None/

      get new_user_session_path, headers: { 'User-Agent' => win_chrome_80 }
      expect(response.headers['Set-Cookie']).to match /_your_app_session=.*SameSite=None/
    end

    it 'SameSite=Noneの扱いにバグがある iOS12 Safari では SameSite=None がつかない' do
      iphone_ios12_user_agent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1'
      get new_user_session_path, headers: { 'User-Agent' => iphone_ios12_user_agent }
      expect(response.headers['Set-Cookie']).to include '_your_app_session='
      expect(response.headers['Set-Cookie']).not_to include 'SameSite'
    end
  end
end
```

## 参考文献

本記事を書くにあたって、下記記事を参考にさせてもらいました。
どうもありがとうございました。

[Chrome 80が密かに呼び寄せる地獄 ～ SameSite属性のデフォルト変更を調べてみた \- Qiita](https://qiita.com/ahera/items/0c8276da6b0bed2b580c#%E3%83%86%E3%82%B9%E3%83%88%E6%96%B9%E6%B3%95)