LoginSignup
20
19

More than 5 years have passed since last update.

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

Posted at

前提

Rails+Reactでサービス開発をはじめ、まずはユーザ認証を実装しようとしていた。
Rails側はdevise+devise_token_authで認証APIを実装、React側はAPIにPOSTでリクエストを送って
トークンを取得し、LocalStorageに保存することでユーザのログイン・未ログインを実装しようとしていた。
APIへのアクセスにsuperagentを、CORS対策に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側

こちらの記事を参考に、emailpasswordを含んだJSONをPOSTで送るとトークンを返すように実装をした。
devise token auth を使って簡単に早くAPIを作る 1 | せんべいって美味しいよね

React側のリクエスト

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

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.headersaccess-tokenが含まれていて、取得できると思っていたらできなかった。
ちなみにChromeの開発者ツールのNetworkから見ると、しっかりとHeadersにaccess-tokenが含まれていた。

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

解決方法

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

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
Missing response headers in response.headers object · Issue #713 · visionmedia/superagent
Expose HTTP Headers in CORS | Jake Trent

20
19
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
19