Rails 6.1対応版: APIモードのRailsに対してCrossOriginなSPAからSession認証する方法

本記事では、APIモードのRailsに対してCrossOriginなSPAからSession(Cookie)認証する方法を解説します。

モダンなフロントエンド開発だと、Auth0やFirebaseを使った認証例が多く見られますが、バックエンドにRailsを使った認証例はまだまだ少ないと感じています。
JWT認証ではなくCookie認証となると、その数はさらに少ないようです。

実際にやってみるといろいろな障壁があることがわかったので、やるべきことをまとめることにしました。

## 背景

Next.jsアプリをVercelに、データ永続化とユーザ認証を目的としたAPIモードのRailsアプリをHerokuにデプロイしています。
この状況でNext.jsアプリからRailsに対して認証を試みると、CrossOriginなリクエストであるが故に様々な障壁があります。

![Untitled_Portfolio_Site_-_Cacoo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60996/933c8ae9-051f-5b96-8b8b-dd1696d20939.png)

- 本記事に記載する内容はバックエンド側がメインです。(フロントエンド側の実装は需要があれば追々記載予定)
- Railsの認証はdeviseを使用しています
  - シンプルなCookieを使ったSession認証なので、本記事に記載している内容はdeviseを使っていなくても同様に実践できる想定です。
- Next.jsではCSRのみを使用しています(SSR, SSG, ISRは使用していない)
  - Next.jsに特化した記載は無いので、本記事に記載している内容はNext.jsを使っていなくても同様に実践できる想定です。

## やるべきこと

### クライアントからのリクエスト時にCookieを送信する

RailsのSessionストレージにCookieを使う前提なので、フロントエンドからのリクエスト時にCookieを送信するよう設定します。
axiosを使っている場合は、以下のように `withCredentials: true` を指定することで、Cookieを送信できます。

```javascript
// 一部のリクエストで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` したタイミングでコメントアウトされていることが多いので、コメントを外してください。

```ruby:Gemfile
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'  # コメントを外す
```

Gemfileを更新したら `bundle install` しておきましょう。

```sh
$ bundle install
```

開発環境用のCORS設定を記述します。

```ruby:config/environments/development.rb
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でAPIにCORSを設定する](https://qiita.com/yumikokh/items/b5fd604e12720027b4d5)

### RailsにてCookie, CookieStoreを使えるようにする

APIモードのRailsでは、RackにてCookieやCookieStoreが有効化されていません。
次のように記述することで、これらを有効化します。

```ruby:config/application.rb
module SampleApplication
  class Application < Rails::Application
    # ・・・省略・・・

    # Cookies
    config.middleware.use ActionDispatch::Cookies               # 追加する
    config.middleware.use ActionDispatch::Session::CookieStore  # 追加する
  end
end
```

参考: [Rails の API モードでセッションやクッキーを使えるようにする](https://www.nightswinger.dev/2020/04/rails-api-with-session-and-cookie/#%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2%E3%81%AE%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF)

### CookieのSameSite属性をNone, Secure属性をtrueにする

RailsからのレスポンスヘッダーのSameSite属性はデフォルト `Lax` となっています。
CrossOriginでCookieをやり取りするためにはこれを `None` とする必要があります。

`rails_same_site_cookie` というgemをインストールすることで、SameSite属性を `None` かつ Secure属性を `true` にします。

```ruby:Gemfile
gem "rails_same_site_cookie", "~> 0.1.8"  # 追加する
```

Gemfileを更新したら `bundle install` しておきましょう。

```sh
$ bundle install
```

参考: [【Rails】SameSiteとSecure属性の付与〜Railsのセキュリティ対策〜]()

しかし、私の環境ではこのGemをインストールしただけではSameSite属性が `None` となりませんでした。
これはRails 6.1で追加された下記オプションが影響しているようです。

[rails6.1で加わった新しいアプリケーション設定であるnew_framework_defaults_6_1.rbの各項目をさらっと解説](https://qiita.com/SoarTec-lab/items/10280dbea1195bf21b3c#railsapplicationconfigaction_dispatchcookies_same_site_protection--lax)

このため、`application.rb` に設定を追記することで、SameSite属性を `None` とします。

```ruby:config/application.rb
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認証ができるようになりました。

プロトコルもフレームワークもブラウザも、セキュリティ強化の一環で制約をかける方向に進化しているが故、一昔前に比べると随分と考慮すべきことが増えたなあという印象です。

## 参考

- [CrossOrigin な SPA と Rails API 構成におけるcookie + session認証](https://qiita.com/hxrxchang/items/d51a27f486fcd1612dbf)