RailsのPOSTでCSRF TokenがVerifyできないときに確認したいこと

## 前提条件
- nginxがSSL Terminationをするリバースプロキシである
- Railsのバージョンが5の状態で`rails new`をしている

## 発生する問題
- formのPOST時に`Can't verify CSRF token authenticity.`というエラーが発生する

## 確認するところ
- CSRF Tokenがページ内に埋まっているか？
- nginxにおけるリパースプロキシの設定は間違っていないか？

### CSRF Tokenがページ内に埋まっているか？
Railsで入力フォームを作成するときは、おそらく`form_for`か`form_tag`を使うことになると思います。その場合、普通は`authenticity_token`というhidden fieldが追加されています。

たとえばこれはQiitaの意見を送信するフォームに`authenticity_token`が埋め込まれている様子です。
<img width="619" alt="qiita authenticity_token" src="https://qiita-image-store.s3.amazonaws.com/0/18796/669bdcbd-89e7-0cac-2707-3e09c6c1330d.png">

またAjaxで送信するフォームなどは、代わりに`csrf_meta_tag`を`<head>`の中で呼んでやることで、`X_CSRF_Token`をリクエストヘッダーに付与することによってCSRF対策を行っています。

このように、Rails側にCSRF対策のためのtokenを送ることができているかを確認するといいでしょう。

たとえばこの記事を下書き保存したときのRequestをみてみると、このように`authenticity_token`を送っているのが確認できます。これはChromeのdeveloper toolsにあるNetworkの欄で確認することができます。
<img width="748" alt="qiita save as draft request" src="https://qiita-image-store.s3.amazonaws.com/0/18796/1dd07940-a621-e6e6-a5da-9f6cf17efe0e.png">

### nginxにおけるリバースプロキシの設定は間違っていないか？
Railsをproduction環境で動作させる場合、大抵はApacheやnginxをリバースプロキシとして前段に噛ませるでしょう。その場合に、リパースプロキシがRailsに送るheaderが間違っている(不足している)と、CSRF Tokenの検証に失敗します。

RailsがCSRF Tokenを検証する部分のコードを見てみます。

```ruby:actionpack/lib/action_controller/metal/request_forgery_protection.rb#L211
      def verify_authenticity_token
        mark_for_same_origin_verification!

        if !verified_request?
          if logger && log_warning_on_csrf_failure
            logger.warn "Can't verify CSRF token authenticity."
          end
          handle_unverified_request
        end
      end

```

[rails/request_forgery_protection.rb at v5.0.0.1 · rails/rails](https://github.com/rails/rails/blob/v5.0.0.1/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L211)


まず`mask_for_same_origin_verification!`ですが、中身はこれです。要はGET Requestであればpassします。

```ruby:actionpack/lib/action_controller/metal/request_forgery_protection.rb#L244
      # GET requests are checked for cross-origin JavaScript after rendering.
      def mark_for_same_origin_verification!
        @marked_for_same_origin_verification = request.get?
      end
```

[rails/request_forgery_protection.rb at v5.0.0.1 · rails/rails](https://github.com/rails/rails/blob/v5.0.0.1/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L244)

次の`verified_request?`はこうなっています。

```ruby:action_controller/metal/request_forgery_protection.rb#L266
      # Returns true or false if a request is verified. Checks:
      #
      # * Is it a GET or HEAD request?  Gets should be safe and idempotent
      # * Does the form_authenticity_token match the given token value from the params?
      # * Does the X-CSRF-Token header match the form_authenticity_token
      def verified_request?
        !protect_against_forgery? || request.get? || request.head? ||
          (valid_request_origin? && any_authenticity_token_valid?)
      end
```

[rails/request_forgery_protection.rb at v5.0.0.1 · rails/rails](https://github.com/rails/rails/blob/v5.0.0.1/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L266)

`protect_against_forgery?`は`config.allow_forgery_protection`の設定値、つまりCSRFの検証をおこなうかどうかを返すメソッドで、デフォルトで`true`です。

その次の２つはhttp requestの種類をみています。GETもしくはHEADなら許可ということですね。

そして次の`valid_request_origin?`が先程述べたリパースプロキシの設定と関係してきます。

```ruby:actionpack/lib/action_controller/metal/request_forgery_protection.rb#L398
      # Checks if the request originated from the same origin by looking at the
      # Origin header.
      def valid_request_origin?
        if forgery_protection_origin_check
          # We accept blank origin headers because some user agents don't send it.
          request.origin.nil? || request.origin == request.base_url
        else
          true
        end
      end
```

[rails/request_forgery_protection.rb at v5.0.0.1 · rails/rails](https://github.com/rails/rails/blob/v5.0.0.1/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L398)

そしてこの`forgery_protection_origin_check`ですが、この設定値を見に行きます。

```ruby:config/initializers/request_forgery_protection.rb
# Be sure to restart your server when you modify this file.

# Enable origin-checking CSRF mitigation.
Rails.application.config.action_controller.forgery_protection_origin_check = true
```

これはRails 5からデフォルトで`true`になった設定です。

ifの中でrequestのoriginとbase_urlが同一かどうかを検証していますが、リパースプロキシの設定に不足があるとこの検証が通らず、`Can't verify CSRF token authenticity.`となってしまいます。

僕の環境では、nginxの設定に`proxy_set_header X-Forwarded-Proto $scheme;`が不足していたためにこのエラーが発生していました。

```nginx
location \ {
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $http_host;
  proxy_set_header X-CSRF-Token $http_x_csrf_token;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-Proto $scheme; #これがなかった
  proxy_redirect off;

}
```

## まとめ
CSRF対策はしっかり行いましょう。

## 参考
- [Rails 5で追加されたinitializerのまとめ | 日々雑記](http://y-yagi.tumblr.com/post/144067860535/rails-5%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%81%9Finitializer%E3%81%AE%E3%81%BE%E3%81%A8%E3%82%81)
- [nginx で SSL リバースプロキシを構築 - WebOS Goodies](http://webos-goodies.jp/archives/ssl_proxy_using_nginx.html)
- [RailsのCSRF対策の仕組みについて - Programming log - Shindo200](http://shindolog.hatenablog.com/entry/2014/09/01/013840)