本記事では、APIモードのRailsに対してCrossOriginなSPAからSession(Cookie)認証する方法を解説します。
モダンなフロントエンド開発だと、Auth0やFirebaseを使った認証例が多く見られますが、バックエンドにRailsを使った認証例はまだまだ少ないと感じています。
JWT認証ではなくCookie認証となると、その数はさらに少ないようです。
実際にやってみるといろいろな障壁があることがわかったので、やるべきことをまとめることにしました。
背景
Next.jsアプリをVercelに、データ永続化とユーザ認証を目的としたAPIモードのRailsアプリをHerokuにデプロイしています。
この状況でNext.jsアプリからRailsに対して認証を試みると、CrossOriginなリクエストであるが故に様々な障壁があります。
- 本記事に記載する内容はバックエンド側がメインです。(フロントエンド側の実装は需要があれば追々記載予定)
- Railsの認証はdeviseを使用しています
- シンプルなCookieを使ったSession認証なので、本記事に記載している内容はdeviseを使っていなくても同様に実践できる想定です。
- Next.jsではCSRのみを使用しています(SSR, SSG, ISRは使用していない)
- Next.jsに特化した記載は無いので、本記事に記載している内容はNext.jsを使っていなくても同様に実践できる想定です。
やるべきこと
クライアントからのリクエスト時にCookieを送信する
RailsのSessionストレージにCookieを使う前提なので、フロントエンドからのリクエスト時にCookieを送信するよう設定します。
axiosを使っている場合は、以下のように withCredentials: true
を指定することで、Cookieを送信できます。
// 一部のリクエストでCookieを送信する
import axios from 'axios'
const res = await axios.get(
currentUserPath,
{ withCredentials: true } // このオプションを追加する
)
// 全てのリクエストでCookieを送信する
import axiosBase from 'axios'
const axios = axiosBase.create({
baseURL: process.env.NEXT_PUBLIC_API_SERVER,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
withCredentials: true, // このオプションを追加する
})
const res = await axios.get(currentUserPath)
RailsにてCORS設定する
Gemfileに rack-cors
を追加します。
rails new
したタイミングでコメントアウトされていることが多いので、コメントを外してください。
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors' # コメントを外す
Gemfileを更新したら bundle install
しておきましょう。
$ bundle install
開発環境用のCORS設定を記述します。
require 'active_support/core_ext/integer/time'
Rails.application.configure do
# ・・・省略・・・
# cors
config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3001' # リクエスト元となるOriginsを記載する
resource '*',
headers: :any,
methods: %i[get post put patch delete options head],
credentials: true # trueとすること
end
end
end
origins
にはCORSを許可するオリジン(≒URL)を記載してください。
今回はNext.jsの開発環境サーバが http://localhost:3001
だったため、そのオリジンのみ許可しています。
ワイルドカード *
とすることもできますが、セキュリティリスクを最小限とするためにも、許可するオリジンは最小限としたほうが良いです。
headers
や methods
は必要に応じて見直してください。
credentials
は レスポンスに Access-Control-Allow-Credentials
ヘッダを付与するため、 true
とする必要があります。
RailsにてCookie, CookieStoreを使えるようにする
APIモードのRailsでは、RackにてCookieやCookieStoreが有効化されていません。
次のように記述することで、これらを有効化します。
module SampleApplication
class Application < Rails::Application
# ・・・省略・・・
# Cookies
config.middleware.use ActionDispatch::Cookies # 追加する
config.middleware.use ActionDispatch::Session::CookieStore # 追加する
end
end
参考: Rails の API モードでセッションやクッキーを使えるようにする
CookieのSameSite属性をNone, Secure属性をtrueにする
RailsからのレスポンスヘッダーのSameSite属性はデフォルト Lax
となっています。
CrossOriginでCookieをやり取りするためにはこれを None
とする必要があります。
rails_same_site_cookie
というgemをインストールすることで、SameSite属性を None
かつ Secure属性を true
にします。
gem "rails_same_site_cookie", "~> 0.1.8" # 追加する
Gemfileを更新したら bundle install
しておきましょう。
$ bundle install
参考: 【Rails】SameSiteとSecure属性の付与〜Railsのセキュリティ対策〜
しかし、私の環境ではこのGemをインストールしただけではSameSite属性が None
となりませんでした。
これはRails 6.1で追加された下記オプションが影響しているようです。
rails6.1で加わった新しいアプリケーション設定であるnew_framework_defaults_6_1.rbの各項目をさらっと解説
このため、application.rb
に設定を追記することで、SameSite属性を None
とします。
module SampleApplication
class Application < Rails::Application
# ・・・省略・・・
# Cookies
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
config.action_dispatch.cookies_same_site_protection = :none # 追加する
end
end
先の手順で追加した rails_same_site_cookie
はSecure属性を true
とする役割を担っているので、アンインストールせずに残しておきます。
まとめ
以上の方法でCrossOriginなSPAからAPIモードのRailsに対してSession認証ができるようになりました。
プロトコルもフレームワークもブラウザも、セキュリティ強化の一環で制約をかける方向に進化しているが故、一昔前に比べると随分と考慮すべきことが増えたなあという印象です。