Rails+Reactのユーザ認証のCORS対策ではまった

## 前提

Rails+Reactでサービス開発をはじめ、まずはユーザ認証を実装しようとしていた。
Rails側は[devise](https://github.com/plataformatec/devise)+[devise_token_auth](https://github.com/lynndylanhurley/devise_token_auth)で認証APIを実装、React側はAPIにPOSTでリクエストを送って
トークンを取得し、LocalStorageに保存することでユーザのログイン・未ログインを実装しようとしていた。
APIへのアクセスに[superagent](https://github.com/visionmedia/superagent)を、CORS対策に[rack-cors](https://github.com/cyu/rack-cors)を利用した。


## 開発環境

* Rails (5.1.5)
* React (16.2.0)
* superagent (3.8.2)
* rack-cors (1.0.2)


## 発生した問題

ReactからSuperAgentを利用してAPIにPOSTすると、期待したJSONが帰ってくるのに、ヘッダの`access-token`が取得できなかった。


## 実装

### Rails側 

こちらの記事を参考に、`email`と`password`を含んだJSONをPOSTで送るとトークンを返すように実装をした。
[devise token auth を使って簡単に早くAPIを作る 1 | せんべいって美味しいよね](http://clc.gonna.jp/2017/01/post-1306/)


### React側のリクエスト

指定したURLにユーザから入力された`email`と`password`をPOSTするだけ。
※ フォームの作成にmaterial-uiを使用していますが、全く関係ないので気にしないでください🙇

```jsx:login.jsx
import React, {Component} from 'react'
import request from 'superagent'
import TextField from 'material-ui/TextField'
import FlatButton from 'material-ui/FlatButton'

const baseURL = 'http://localhost:3000' // 開発環境に合わせて設定

export default class Login extends Component {
  constructor (props) {
    super(props)
    this.state = { email: '', password: '' }
  }

  // api呼び出し
  api (command) {
    request
      .post(baseURL + '/api/' + command)
      .set('Content-Type', 'application/json')
      .send({
        email: this.state.email,
        password: this.state.password
      })
      .end((err, res) => {
        if (err) return
        if (res.headers['access-token']) {
          // 認証トークンをlocalStorageに保存
          window.localStorage['access-token'] = res.headers['access-token']
          return
        }
      })
  }

  render() {
    const changed = (name, e) => this.setState({[name]: e.target.value})
    return (
      <div>
        <h1>ログイン</h1>
        <TextField
          value={this.state.email}
          floatingLabelText="email"
          onChange={e => changed('email', e)}
        /><br />
        <TextField
          value={this.state.password}
          floatingLabelText="パスワード"
          type="password"
          onChange={e => changed('password', e)}
        /><br />
        <FlatButton label="ログイン" onClick={e => this.api('auth/sign_in')} />
      </div>
    )
  }
}
```

`res.headers`に`access-token`が含まれていて、取得できると思っていたらできなかった。
ちなみにChromeの開発者ツールのNetworkから見ると、しっかりとHeadersに`access-token`が含まれていた。

原因は、レスポンスヘッダに含まれる独自のHTTPレスポンスヘッダを取得しようとすると、ブラウザがセキュアではないヘッダにアクセスしようとしているとみなしてアクセス許可をしてくれないことだった。
さらに、`Access-Control-Expose-Headers`というレスポンスヘッダを設定すると解決しそうなこともわかった。


## 解決方法

`rack-cors`の設定で許可するレスポンスヘッダを指定することができた。
`rack-cors`の`expose`の指定で許可するヘッダを指定する(Access-Control-Expose-Headersを設定する)。

```ruby:application.rb
config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8080' # 開発環境に合わせて設定
    resource '*', :headers => :any, :methods => [:get, :post, :options], :expose => ['access-token']
  end
end
```

この設定を追加することで無事React側で`access-token`を取得することができました。


## 参考記事

[CORSまとめ#レスポンスに独自のHTTPレスポンスヘッダを追加してブラウザから読み出したい場合 - Qiita](https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%81%AB%E7%8B%AC%E8%87%AA%E3%81%AEhttp%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%83%98%E3%83%83%E3%83%80%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%A6%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%8B%E3%82%89%E8%AA%AD%E3%81%BF%E5%87%BA%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88)
[Missing response headers in response.headers object · Issue #713 · visionmedia/superagent](https://github.com/visionmedia/superagent/issues/713)
[Expose HTTP Headers in CORS | Jake Trent](https://jaketrent.com/post/expose-http-headers-in-cors/)