ReactのテストツールであるJestの使い方を調べようとこちらの記事のソースに非同期処理を入れました。
その過程で(isomorphic)fetchを使用してのRailsのCSRF検証のパス方法を調べたのを記事にしておきます。
#前提条件
reduxのexampleであるTodosのサーバAPIバージョンを元にしています。
てかこれでのAPI作成の記事ってGET処理はあるんですけど、POSTといった登録更新系の記事はほとんどなくないですか?
なのでReduxのTodosのAPIバージョンとしてソースを見てもいいと思います。
#使用環境
Ruby:2.3.1
Rails:5.0.0.1
Reactjs:15.3.2
isomorphic-fetch:2.2.1
#本記事の全体ソース
https://github.com/chimame/react_on_rails_template
#Rails側のAPIおよび解説
##ソース
詳しくはgithubのソースを見てもらえればいいと思いますが、特に難しいことはしていません。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
class TodosController < ApplicationController
・・・
# POST /todos
# POST /todos.json
def create
@todo = Todo.new(todo_params)
if @todo.save
render json: @todo, status: :created
else
render json: @todo.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /todos/1/toggle
# PATCH/PUT /todos/1/toggle.json
def toggle
if @todo.update({completed: !@todo.completed})
render json: @todo, status: :ok
else
render json: @todo.errors, status: :unprocessable_entity
end
end
・・・
end
ここでのポイントはAPI(Ajax)処理をするにも関わらず、CSRFチェックを行うことです。jQuery(ujs)使うならあまり気にしない人もいるのではないしょうか。
##解説
注目してほしい点はprotect_from_forgery with: :exception
です。この指定をすることによってCSRFトークンのチェックが行われるようになります。(デフォルト)
こちらの記事にも書かれていますが、RailsのCSRFトークンのチェックは以下の判定がすべてfalseの場合にエラーとなります。
設定ファイルで config.application_controller.allow_forgery_protection を false に設定しているか
リクエストのHTTPメソッドがGETであるか
リクエストのHTTPメソッドがHEADであるか
セッション変数 _csrf_token の値とリクエストボディの authenticity_token の値を比較した結果、正しいと判断されるか
セッション変数 _csrf_token の値とリクエストヘッダーの X-CSRF-Token の値を比較した結果、正しいと判断されるか
要は私のソースではCSRFのチェックが行われるということです。これは意図的にこのようにしております。
ちなみにRails5.0.0.1でのソースはこれです。
# 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
#Reactjs側のソースと解析
##ソース
import {addTodo} from '../../utils/WebApi'
class Todos extends Component {
・・・
render() {
const { data, actions } = this.props
return (
<div>
<TodoList todos={data} onTodoClick={actions.toggle} />
<AddTodo onRegist={(text) => {actions.add(addTodo(text))}} />
</div>
)
}
}
import {toggle} from '../../utils/WebApi'
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map(todo =>
<Todo
key={todo.id}
todo={todo}
onClick={() => onTodoClick(toggle(todo.id))}
/>
)}
</ul>
)
import fetch from 'isomorphic-fetch'
const csrfToken = document.getElementsByName('csrf-token').item(0).content
const params = {
method: 'POST',
credentials: 'same-origin',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
}
}
・・・
export const addTodo = (text) => {
return fetch(`/todos`, {...params, ...{body: JSON.stringify({todo: {text: text}})}})
.then(res => {
if (res.status >= 400) {
throw new Error("Bad response from server")
}
return res.json()
})
.then(payload => { return payload })
.catch(error => { return undefined })
}
export const toggle = (id) => {
return fetch(`/todos/${id}/toggle`, {...params, ...{method: 'PATCH'}})
.then(res => {
if (res.status >= 400) {
throw new Error("Bad response from server")
}
return res.json()
})
.then(payload => { return payload })
.catch(error => { return undefined })
}
##解説
大事なのはWebAPI.jsのcredentials: 'same-origin'
と'X-CSRF-Token': csrfToken
部分です。
まず'X-CSRF-Token': csrfToken
はRailsから出力されたcsrf-tokenをリクエストヘッダに入れております。もしjQueryを使っている人ならば同時にjQuery-ujsをほぼ使用しているでしょう。そのjQuery-ujsがこのリクエストヘッダにトークン入れる処理を行っているので普段は気にすることはないと思います。
しかし今回はjQueryのAjaxで通信するのではisomorphic-fetchで通信するため、自身で設定してやる必要があります。そこで上記のようにHTMLのmetaのcsrf_tokenを取得し、設定しています。
ただ、csrf-tokenを送るだけではRails側のチェックが通りません。
それは
セッション変数 _csrf_token の値とリクエストボディの authenticity_token の値を比較した結果、正しいと判断されるか
と書いているようにサーバ側にてセッションを参照できる状態にしないといけません。Railsのセッションストアの設定によりますが、デフォルトではcookie_store
となっています。もしRedis等を使う場合でもcookieにはセッションIDを持っています。
サーバ側ではその受け取ったcookieでセッション変数を参照します。なので、クライアントからはcookieを送るのが必須なのです。なのでcredentials: 'same-origin'
を記載して、同一ホストならばcookieを送るようにしています。
これでサーバ側のCSRFトークン検証をパスすることができるのです。
仮にReactjs側の通信をjQueryのAjaxを使用して通信すると検証をパスできることも確認できます。なぜならばRails側にてjQuery-ujsを出力しているからです。必要ならば確認してみてください。
次の記事はJestの予定でしたが少し足踏みをしてしまった感じです。しかもRailsがAPIしか存在しないとなった場合はこの記事の内容は無用の長物となります。
次はJestを…