SPAのアプリケーションにおけるCookieの共有について

個人のアプリケーションを開発において、SPAにする機会が多いのですが、開発のたびにどのような設定をするんだっけ?と忘れてしまうことが多いです。
SPAの場合、バックエンドとフロントエンドをそれぞれデプロイし、オリジンが異なるケースがほとんどかと思いますが、この場合、知っておくべきこと、設定すべきことが多く忘れてしまいやすいので、バックエンドとフロントエンドに分けてまとめます。

## バックエンドで対応すべきこと

### リクエストを受け付ける別オリジンを指定する

これによって、レスポンスの `Access-Control-Allow-Origin` ヘッダーには許可するオリジンが入り返却される。
**`*` で全オリジンを指定すると資格情報を共有できないので注意**する。資格情報を伴わないオリジン間のリソース共有であれば、`*`指定でも(セキュリティ的には問題があるものの)可能である。

以下、`https://example.com`からのリクエストを受け付けるように指定した場合のレスポンスヘッダー例

```
Access-Control-Allow-Origin: https://example.com
```

### リクエストを受け付けるHTTPメソッドを指定する

これによって、レスポンスの `Access-Control-Allow-Methods` ヘッダーには許可するHTTPメソッドが入り返却される。

以下、`GET, POST, PATCH, PUT, DELETE` のメソッドのリクエストを受け付けるように指定した場合のレスポンスヘッダー例

```
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE
```

### 資格情報を共有する設定をする

これによって、レスポンスの`Access-Control-Allow-Credentials` ヘッダーに true が入り返却され、フロントエンド側でクッキーを取り扱えるようになる。

以下、資格情報を共有できる場合のレスポンスヘッダー例

```
Access-Control-Allow-Credentials: true
```

### クッキーのSameSite属性をNoneにする
クッキーのSameSite属性には Lax, Strict, None の3つのモードがあるが、クロスオリジン間での共有では None にする必要がある。

以下、クッキーを設定している場合のレスポンスヘッダー例

```
Set-Cookie: _example_session=XXXXXXX; path=/; secure; HttpOnly; SameSite=None
```

- [SameSite cookies - HTTP | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie/SameSite)

## バックエンド: Railsでの対応例

- CORSの設定に、`rack-cors`, Cookieの設定に`rails_same_site_cookie`を利用する
- APIモードを利用する場合は、ミドルウェアを追加する。(APIモードではない場合は、`require 'rails/all'`によって、すべてのミドルウェアがデフォルトで有効になっているため不要。)

```
# Gemfile
gem 'rack-cors' # CORSの設定に必要
gem 'rails_same_site_cookie', # SetCookieヘッダーにSameSite属性を付与するために必要
```

```config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins ENV['FRONT_DOMAIN']
    resource '*', headers: :any, methods: %i[get post patch put delete], credentials: true
  end
end
```

```config/application.rb
module MyApp
  class Application < Rails::Application
    config.load_defaults 6.1

    config.api_only = true
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore
  end
end
```

- [GitHub - cyu/rack-cors: Rack Middleware for handling Cross-Origin Resource Sharing (CORS), which makes cross-origin AJAX possible.](https://github.com/cyu/rack-cors)
- [GitHub - pschinis/rails_same_site_cookie: Manages the new SameSite=None behavior for Rails apps that use cookie-based authentication for cross-domain requests](https://github.com/pschinis/rails_same_site_cookie)
- [Chrome 80が密かに呼び寄せる地獄 ～ SameSite属性のデフォルト変更を調べてみた - Qiita](https://qiita.com/ahera/items/0c8276da6b0bed2b580c)
- [Rails と Rack - Railsガイド](https://railsguides.jp/rails_on_rack.html)

## バックエンド: Go(Echo)での対応例

```main.go
func main() {
	e := echo.New()

	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins:     []string{os.Getenv("FRONT_DOMAIN")},
		AllowMethods:     []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
		AllowCredentials: true,
	}))
  e.POST("/api/login", Login)
	...
}

func Login(c echo.Context) error {
	...
	cookie := &http.Cookie{
		Name:     "jwt",
		Value:    token,
		Expires:  time.Now().Add(time.Hour * 72),
		SameSite: http.SameSiteNoneMode, // SameSite属性をNoneにする
		Path:     "/", // ブラウザからクッキーを送信できるようにする
		Secure:   true, // SSL,HTTPSプロトコルによる送信のみを許可する
		HttpOnly: true, // ブラウザのJSによるクッキーの書き換えを禁止する
	}
	c.SetCookie(cookie)
	...
}
```

- [CORS Middleware | Echo - High performance, minimalist Go web framework](https://echo.labstack.com/middleware/cors/)
- [CORS Recipe | Echo - High performance, minimalist Go web framework](https://echo.labstack.com/cookbook/cors/)
- [add SameSite support by srenatus · Pull Request #28 · alexedwards/scs · GitHub](https://github.com/alexedwards/scs/pull/28)

## フロントエンドで対応すべきこと

クロスオリジン間で資格情報(クッキー等)を送信するために、リクエスト時に特定のフラグを設定する必要がある。
XMLHTTPRequest や axios では、 withCredentials というフラグを true に設定する。


資格情報を異なるオリジンへ送信している場合のリクエストヘッダー例

```
Cookie: _sample_key=XXXXXXX
```

- [XMLHttpRequest.withCredentials - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials)
- [GitHub - axios/axios: Promise based HTTP client for the browser and node.js](https://github.com/axios/axios#request-config)

## フロントエンド: Nuxt.jsによる対応例

```nuxt.config.js
export default {
  ...
  axios: {
    baseURL: process.env.API_URL, // APIのエンドポイントのURLは環境変数で指定しておくと開発環境、本番環境で分けられるので便利
    credentials: true // withCredentialsがtrueにする
  }
}
```

- [Options - Axios Module](https://axios.nuxtjs.org/options/#credentials)


## 参考

- [同一オリジンポリシー - ウェブセキュリティ | MDN](https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy)
- [オリジン間リソース共有 (CORS) - HTTP | MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)
- [Origin (オリジン) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN](https://developer.mozilla.org/ja/docs/Glossary/Origin)