[rails]session_storeでdomainとtld_lengthを設定した際、ChromeでlocalhostのCookieが保存されない

（使用Railsバージョン : Rails 5.0.2）

RailsでCMSのWebアプリの開発にアサインされている。

CMSで作成したサイトをプレビューできるようにプレビューモードが必要で、サブドメイン名が「preview」になっていたらプレビューモードと判定するようにする。
セッションをサブドメインの有無に関わらず共有したいため、session_storeの設定にdomain: :all と tld_length とを追加した。

参考  
[Railsでsubdomainを使う際にtld_lengthを設定する](https://techracho.bpsinc.jp/baba/2012_11_19/6393)

```ruby
# config/initializers/session_store.rb
Rails.application.config.session_store
  :cookie_store,
  key: '_hogehuga_session',
  domain: :all,
  tld_length: ActionDispatch::Http::URL.tld_length + 1 # サブドメイン対応のためのTLDレングス指定
```

この設定をした上で、ローカルでサブドメインをテストするために開発では「lvh.me」のループバックドメインを利用している。

| 項目                         | 値                  |
|------------------------------|--------------------:|
| 管理画面のドメイン           | lvh.me:3000         |
| プレビューモードのドメイン   | preview.lvh.me:3000 |
| Cookieの保存されるドメイン名 | .lvh.me             |


## 生じた問題
開発では「lvh.me:3000」のURLを使うようになったので「localhost:3000」のURLは使わなくなったのだけど、「localhost:3000」でアクセスして管理画面にログインしようとすると正しいID/パスワードの組み合わせでもActionController::InvalidAuthenticityTokenエラーとなる。

### 問題の現象の詳細を調べてわかったこと
セッションの設定とリクエストヘッダーのCookieの内容とが対応する。

```ruby
# config/initializers/session_store.rb
Rails.application.config.session_store
  :cookie_store,
  key: '_hogehuga_session',
  domain: :all,
  tld_length: ActionDispatch::Http::URL.tld_length + 1 # サブドメイン対応のためのTLDレングス指定
```

このとき、リクエストヘッダーは以下の通り。  
> Set-Cookie:  
> _hogehuga_session=Q3h2cTIveXVaekp2aDMraU53ekhrN1JxbXd2VDcyYjZhU1ZTSm0wZFZ1MTJWTnNBbGVmdENLbUVVL1pjaStFcDEwcUo1VWRTeUc5QjFBOEx2YWMxUlFzV1U0anZ4T2d4NCtGQ1ZTZk9mZFhKaGpkeXFqVG13TmVhMFJBa1c2UHpUZFE4emUyT3gvUmIzZ2RqQ2dzV2lBPT0tLWhDUlZnZ3d5dFVJYThLaXRWR2F1YWc9PQ%3D%3D--121931b337b821e3bd0136133e010e5e8a90631b;  
> ***domain=.localhost;***  
> path=/;  
> HttpOnly

→このときブラウザごとで挙動が違う。

| ブラウザ      | Cookie保存 | サブドメイン間でのCookie共有 |
|:-------------:|-----------:|------------------------------|
| FireFox       | される     | できる                       |
| Google Chrome | されない   | （Cookie保存されず）         |

---

ドメインとTLDレングス指定を削除してみる。

```ruby
# config/initializers/session_store.rb
Rails.application.config.session_store
  :cookie_store,
  key: '_hogehuga_session',
  domain: :all
```

このとき、リクエストヘッダーは以下の通り。domain指定句がなくなる。  
> Set-Cookie:  
> _hogehuga_session=Q3h2cTIveXVaekp2aDMraU53ekhrN1JxbXd2VDcyYjZhU1ZTSm0wZFZ1MTJWTnNBbGVmdENLbUVVL1pjaStFcDEwcUo1VWRTeUc5QjFBOEx2YWMxUlFzV1U0anZ4T2d4NCtGQ1ZTZk9mZFhKaGpkeXFqVG13TmVhMFJBa1c2UHpUZFE4emUyT3gvUmIzZ2RqQ2dzV2lBPT0tLWhDUlZnZ3d5dFVJYThLaXRWR2F1YWc9PQ%3D%3D--121931b337b821e3bd0136133e010e5e8a90631b;  
> path=/;  
> HttpOnly  

→このときはCookieが保存される。

| ブラウザ      | Cookie保存 | サブドメイン間でのCookie共有 |
|:-------------:|:----------:|:----------------------------:|
| FireFox       | される     | できない                     |
| Google Chrome | される     | できない                     |

リクエストヘッダーのSet-Cookie:句におけるdomain:指定の処理の仕方がふたつのブラウザで異なっている。  
アプリではサブドメイン間でのセッション共有が必要なので実際にはTLDレングス指定は必須。

## そもそものCookieの設計

[1994年にネットスケープコミュニケーションズ社によって提案・実装されたクッキーの仕様](https://curl.haxx.se/rfc/cookie_spec.html)

抜粋引用
> domain=DOMAIN_NAME
>
> （略）
>
> Only hosts within the specified domain can set a cookie for a domain and ***domains must have at least two (2) or three (3) periods*** in them to prevent domains of the form: ".com", ".edu", and "va.us". Any domain that fails within one of the seven special top level domains listed below only require two periods. Any other domain requires at least three. The seven special top level domains are: "COM", "EDU", "NET", "ORG", "GOV", "MIL", and "INT".

Cookieが登場した当初はドメイン名にはドットが少なくとも2つか3つ含まれていないと正当なドメイン名だと認められずに保存されなかった。  
これは「.com」や「.jp」のような巨大な水準のドメイン名でCookieが保存されてしまうのを防ぐため。

* ○ www.example.com (2)
* ○ www.example.co.jp(3)
* ☓ example.com(1)
* ○ example.co.jp(2)
* ☓ localhost(0)

その後、当初の仕様が標準化される。
1. RFC 2109で初めて標準化(1997)
2. RFC 2965で更新(2000)
3. RFC 6265で更新(2011)

[W3C (RFC 2109)HTTP State Management Mechanism](https://www.w3.org/Protocols/rfc2109/rfc2109)

抜粋引用

> 4.3.2  Rejecting Cookies
>
> To prevent possible security or privacy violations, a user agent rejects a cookie (shall not store its information) if any of the following is true:
> * The value for the Domain attribute ***contains no embedded dots or does not start with a dot***.
>
> Examples:
> * A Set-Cookie from request-host y.x.foo.com for Domain=.foo.com would be rejected, because H is y.x and contains a dot.
> * A Set-Cookie from request-host x.foo.com for Domain=.foo.com would be accepted.
> * A Set-Cookie with ***Domain=.com or Domain=.com., will always be rejected, because there is no embedded dot.***
> * A Set-Cookie with Domain=ajax.com will be rejected because the value for Domain does not begin with a dot.


ドメイン指定の書式は以下であることが必要であると定められている。
* 「.」から始まること
* 「.」が途中に埋め込まれていること。

| ドメイン名     | OK/NG | 備考                                                   |
|---------------:|:-----:|--------------------------------------------------------|
| .example.com   |    OK |                                                        |
| example.com    |    NG | ドットで始まってない。                                 |
| .example.co.jp |    OK |                                                        |
| localhost      |    NG | ドットで始まっていないし途中にドットを含んでもいない。 |
| jp             |    NG | ドットで始まっていないし途中にドットを含んでもいない。 |
| .localhost     |    NG | ドットで始まっているが途中にドットを含んでいない。     |
| .com           |    NG | ドットで始まっているが途中にドットを含んでいない。     |
| .lvh.me        |    OK |                                                        |
| .co.jp         |    OK | 書式上はOKだがセキュリティ上問題がある。これがブラウザの抱える「クッキーモンスター問題」ということらしい。 |

現在、Cookieの保存の際のドメイン名指定の捌き方は、ブラウザによってまちまちである。  
先頭にドットを付けねばならないというのは厳密には守られていない。  
仕様に厳密に従えば「localhost」「.localhost」のドメイン名のCookieはそもそも保存されてはならない＝Cookieを使うWebサイトの開発はlocalhostのURLを利用しては不可能である。  
しかし、実際は「localhost」「.localhost」のドメインでブラウザがCookieを保存してくれているので開発できている。

## 回避策の提案
http://localhost:3000/ のURLでRailsのsessionを利用したログイン処理（実際にはCookieを使っている）で例外エラーになるのは、Google Chromeの仕様。

回避策として考えられるのは
* 特に何もせず、開発時はlocalhostのURLを使用しないようにする。
* ~~セッション管理の方法をCookieではないものに変更する。~~
   （どうも、管理方法をCookieからたとえばactiverecord-session_store に変更しても結局Cookieは使われているっぽい）
* localhost にアクセスされたらlvh.meにリダイレクトする。
* etc/hosts を書き換え、ローカルホストを参照するためのURLを変更する。  

hostsを編集するならこんな感じ。

```
# /private/etc/hosts
127.0.0.1 localhost.dev
127.0.0.1 preview.localhost.dev
```

| 項目                         | 値                         |
|------------------------------|---------------------------:|
| 管理画面のドメイン           | localhost.dev:3000         |
| プレビューモードのドメイン   | preview.localhost.dev:3000 |
| Cookieの保存されるドメイン名 | .localhost.dev             |

現状localhostのURLではサブドメインを利用した開発ができないので使用していない。

## 補足01
InvalidAuthenticityTokenであるのは、FormにHiddenで埋め込まれているauthenticity_tokenが一致しないために起こっているエラーであるため。

このエラーを起こさないためには以下のプロセスが通ることが必要。
1. ViewがFormを描画したときにCookieにauthenticity_tokenの値が保存される。
2. POST送信値のauthenticity_tokenと、Cookieに保存されているauthenticity_tokenとが一致している。  
    [該当箇所 def any_authenticity_token_valid?](https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L278)

しかし、Cookieが保存されない場合、以下のようになる。

1. ViewがFormを描画したときにCookieにauthenticity_tokenの値を保存しようとするが保存されない。
2. POST送信値のauthenticity_tokenと照合するためのCookieのauthenticity_tokenの値を参照しようとするが見つけられないので再度ランダムなハッシュを生成してそれと照らし合わせようとする。  
[該当箇所 def real_csrf_token(session)](https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L373)
3. 両者が一致しない。

→InvalidAuthenticityToken（不適切な認証トークンエラー）

## 補足02
セッション管理設定をCookieではないものに入れ替えても、結局はCookieを使うらしい。  

セッションのデータはキーと値とのセットで管理されている。
* cookie_store 設定の場合
  * キーはCookieに入る
  * 値もCookieに入る
* cookie_store 以外の設定の場合
  * キーはCookieに入る
  * 値が別のシステム（DBやRedisに入る）
  * 値を取り出す際の検索時にCookieに入っているキーの値を使う

参考 
[How Rails Sessions Work ](http://www.justinweiss.com/articles/how-rails-sessions-work/)  
[Railsのセッション管理方法について](http://shindolog.hatenablog.com/entry/2014/11/02/164118)