AWS ELBを経由したリクエストでActionController::InvalidAuthenticityTokenが起こったのでデバッグと解決まで

## 背景
AWSで Proxy ELB -> Nginx -> ELB -> Taget Group -> ECS  でリクエストを飛ばしてRailsのサービスを動かしたところ、 CSRFトークン対策でエラーになったのでそのデバッグと解決策までの道のり。

## CSRFトークン対策でエラーになる
### エラー概要
起こっていたエラーは`ActionController::InvalidAuthenticityToken`。

### CSRFトークン対策とは
https://railsguides.jp/security.html#クロスサイトリクエストフォージェリ-csrf
Railsが標準搭載しているセキュリティ対策です。
セッションに保存されてるtokenとPOST時の `authencity_token` が一致しているかを検証し、一致していない場合にエラーを吐く。

### 解決策
nginx.confに`proxy_set_header X-Forwarded-SSL on;`を追加する。

  ```nginx.conf
# もっと本当は書いてあるけど省略
server {
    listen       80;
    server_name  hoge.jp;
    
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    # これを追加する
    proxy_set_header X-Forwarded-SSL on;
}
  ```

## エラー検証
### tokenが異なっている？
> セッションに保存されてるtokenとPOST時の `authencity_token` が一致しているかを検証し、一致していない場合にエラーを吐く

ならセッションに保存されているtokenと`authencity_token`が異なっているのか、通常の操作でそうなることがあるのだろうか、ということで実際に検証しているRailsのコードをみてみた。

```rails/actionpack/lib/action_controller/metal/request_forgery_protection.rb
def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? ||
  (valid_request_origin? && any_authenticity_token_valid?)
end
```
  <small>https://github.com/rails/rails/blob/98a4c0c76938e46009cca668da9c3b584a9e9e74/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L289-L292</small>
   
この `verified_request?` がfalseの時に `InvalidAuthenticityToken` のエラーが投げられる。
tokenが異なるということは `any_authenticity_token_valid?` がfalseということになるので、その予想でデバッグをしてみた。が、`any_authenticity_token_valid?` はtrueだった。

### `valid_request_origin?` がfalseになっている？
上のコードを見ると、`valid_request_origin?` がfalseの時にも`verified_request?`がfalseになる可能性があるので、確認してみた。
すると確かに、`valid_request_origin?` がfalseだった。

`valid_request_origin?`の中身を見てみる。

```rails/actionpack/lib/action_controller/metal/request_forgery_protection.rb
def valid_request_origin? # :doc:
    if forgery_protection_origin_check
      # We accept blank origin headers because some user agents don't send it.
      raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
      request.origin.nil? || request.origin == request.base_url
    else
      true
    end
end
```

<small>https://github.com/rails/rails/blob/98a4c0c76938e46009cca668da9c3b584a9e9e74/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L455-L463</small>

`valid_request_origin?` がfalseになるには `request.origin` と `request.base_url` の中身がわかれば理由が分りそうなので出力してみた。
すると`request.origin` は  `https://〜` なのに対し `request.base_url` が `http://〜` となっていた。

つまり上のコードの `request.origin == request.base_url` の検証部分でfalseになっていることがわかった。

### NginxのconfでX-Forwarded-Protoを設定する？
この時点でいろいろ調べると、「NginxからRailsにリクエストが渡される時にHTTPSでNginxにアクセスしてもHTTPとしてRailsに渡されてしまうらしく、これを防ぐために Nginxのconfで `X-Forwarded-Proto`を使ってRailsにHTTPSであることを知らせる」、という方法がすぐ出てくる。やってみた。

```nginx.conf
# もっと本当は書いてあるけど省略
server {
    listen       80;
    server_name  hoge.jp;
    
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # これを追加
    proxy_set_header X-Forwarded-Proto https;
}
```

けどダメだった。
試しにRailsでリクエストヘッダを出力してみると `"X-Forwarded-Proto": "http"`となっていた。

### どこかでhttpsからhttpに上書きされている？
**その通りで、これはELBの性質上でした。**
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533070/e6dc8c38-d663-8177-2025-b61e1d5f881e.png)


今回、ProxyとなるELBから動かしているRailsのサービスに紐づくELBに対してリクエストが送られてくるが、ここはHTTPで送られてくる。
> <b>Application Load Balancer および クラシックロードバランサー は、クライアントに返信する応答のプロキシの後のクライアントの入力リクエストからの接続ヘッダーを優先します</b>

とのことで、NginxからELB間のHTTP通信が優先されてリクエストヘッダの `X-Forwarded-For`、 `X-Forwarded-Proto`、 `X-Forwarded-Port`が書き換えられてしまっていた。

### じゃあどうする
requestオブジェクトはRackで作られているらしいのでそこのコードを見てみた。

```rack/lib/rack/request.rb
def scheme
if get_header(HTTPS) == 'on'
  'https'
elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
  'https'
elsif forwarded_scheme
  forwarded_scheme
    else
      get_header(RACK_URL_SCHEME)
    end
end
      
# 省略

def base_url
    "#{scheme}://#{host_with_port}"
end
```

<small>https://github.com/rack/rack/blob/649c72bab9e7b50d657b5b432d0c205c95c2be07/lib/rack/request.rb</small>

`base_url` の作られ方から、 `scheme` が `https` になれば良い。
`schema`が `https` になるにはいくつか条件があるけれど 、今回は`get_header(HTTP_X_FORWARDED_SSL) == 'on'` になるようにすればいけそう！
(`HTTP_X_FORWARDED_SSL` はELBに書き換えられる心配もない)
ということで、Nginxのリクエストヘッダに `X_Forwarded_SSL` を追加してみた。

```nginx.conf
# もっと本当は書いてあるけど省略
server {
    listen       80;
    server_name  hoge.jp;
    
    proxy_set_header Host $host;
    # この下2つはELBに書き換えられちゃう
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    # これを追加する
    proxy_set_header X-Forwarded-SSL on;
}
```

### 結果
エラーが出なくなった！
デバッグしてみたらちゃんとリクエストヘッダに`X_Forwarded_SSL`が追加されて `scheme`は`https`になっていた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533070/0d997d37-5549-4056-f791-4a276b1f0a70.png)