Next.jsでcross-site間のCookieを共有する
概要
loaclhostでの開発時に、外部APIサーバーにPOSTした際に返却されるset-cookieが効かず、ブラウザにCookieが保存されなかった際の対処録です。
環境
- フロント: Next.js
- APIサーバー: Rails
想定する動作
前述したset-cookieによってセッションIDをブラウザで管理し、ログインチェック時にCookieヘッダに値を乗せリクエストを送信する
SameSite=Laxがもたらす制約
ログイン時のレスポンスヘッダは以下のようになっています。
Set-Cookie: _session_id=value; path=/; HttpOnly; SameSite=Lax
ここで、SameSite=Laxとは、ファーストパーティのコンテキストのみの扱いに制限されることを意味します。つまり、cross-siteから送信されたCookieはブラウザに保存することが出来ません。
Chrome DevToolsからレスポンスヘッダを確認すると、cross-siteレスポンスからのCookieをブロックする警告が表示されます。
This attempt to set a cookie via a Set-Cookie header was blocked because it had the "SameSite=Lax" attribute but came from a cross-site response which was not the response to a top-level navigation.
Cookieを保存するためには、この制約を回避しなければなりません。
SameSite属性について
SameSite
属性は、cross-siteからのCookieを付与する範囲を制限するための属性値です。
属性値は3パターンに分かれ、それぞれの対応は以下の通りです。
属性値 | 送受信コンテキスト |
---|---|
None | 全て(cross-siteを許可する) |
Strict | ファーストパーティ |
Lax | トップレベル ナビゲーション |
Noneは、いかなるサイトからのリクエストであってもCookieが付与されます。
Strictは、送信元と送信先がsame-siteである場合のみ、トップレベルナビゲーション(formやaタグによる遷移)を含む全てのリクエストでCookieの付与が許可されます。
この制約により、悪意ある攻撃者が用意したサイトから非正規のリクエストを発行した際にCookieが付与されることは無くなります。その反面、cross-siteからの遷移を考慮したサイトの場合、ユーザー体験を損ねる恐れがあります。
Laxは、トップレベルナビゲーションで発行されるGETリクエストのみCookieの付与が許容され、それ以外のcross-siteリクエストは付与が制限されます。Strictよりかは多少緩くなった制約です。
SameSite=Noneによるアプローチ
SameSite属性によって制御が定められているため、cross-siteでもCookieを送受信できるようにサーバーサイドで設定すれば、やりたい事を実現できます。
その場合、SameSite=None
とSecure
属性を設定します。これは両方指定しなければなりません。
Set-Cookie: third_party_cookie=value; SameSite=None; Secure
また、SameSite=None
はデフォルト値でないことに注意してください。SameSite属性が省略された場合、SameSite=Lax
がデフォルトとなります。
SameSite=Noneの問題点
SameSiteをNoneにするとcross-site Cookieを自由に扱えるようになるため、CSRF攻撃のリスクをはらむことに注意してください。
今回の問題はフロントのhttp://localhost:3000
と、APIサーバーhttps://example.com/api(例)
間の通信に対して発生するものであり、本番運用ではドメインが割り当てられsame-siteとなるパターンです。そのため、本記事では開発時のみの問題として取り扱います。[1]。
それらを踏まえ、CookieのSameSite属性の変更について考えないものとします。
Proxyを用いたアプローチ
動作環境はNext.js、HTTPクライアントはaxiosを前提とします。
NextにはRewritesというURLプロキシの機能が用意されているため、こちらを活用します。
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
async rewrites() {
return [
{
source: '/:path*',
destination: 'your-api-domain/:path*',
},
]
},
}
module.exports = nextConfig
注意すべきは、こちらの設定はあくまで開発環境上での関心毎であるため、本番運用の際はrewrite設定が適用されないようにすることです。process.envなどを参照して切り替えるようにしましょう。
async rewrites() {
return process.env.NODE_ENV === 'development'
? [
// ローカル開発時のproxy設定
...
] : []
}
まとめ
リソース間のやり取りには適切な制限を設けるべきであり、際限無しに制限したり、全ての操作を自由にすることは現実的ではありません。Cookieのような技術はセキュリティ脅威の元となる可能性を含むため、どこまでの範囲を許容し、それによってどのような攻撃が想定されるのか吟味した上で慎重な判断を下した上で開発を進めましょう。
-
もちろん、本番運用でも同じ状況(フロントとAPIサーバーのドメインが別々)となるプロジェクトはあるかと思います。その場合はCookieをどのように扱うべきか、セキュリティを考慮した議論をする必要があります ↩︎
ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion