SSRで認証をどうするべきか?
既存プロジェクトにNext.js等のSSRを導入する場合は既存の認証機能を維持したり活かしたりする必要があります。しかしSSRの認証とCSR(SPA)の認証は同じではなく、簡単に共有できるものではありません。
ここでは複数のテックブログの事例を見ながら、この難しさと本物のウェブサービスで採用された解決策を見ていきます。
CSRとSSRの認証の違い
まず最初に、CSR(SPA)とSSR[1]の認証の違いをざっくり紹介します。技術的制約を理解していただくことが目的です。
クライアント視点では、認証システムは突き詰めると、トークン(鍵)[2]を安全に保管し、必要時にサーバに渡す(鍵穴に差し込む) ことです。この2つに絞って解説します。[3]
https://www.flaticon.com/free-icons/vault (by IYIKON) https://www.flaticon.com/free-icons/key (by monkik)
トークンを送るタイミング
サーバに認証してもらうには、適切なタイミングでサーバにトークンを送る必要があります。このタイミングがSSRとCSRとでは大きく異なります。
- CSRは呑気にトークンを送ります: CSRはまず最初に空っぽのHTMLを読み込み、そこからJavaScriptを読み込みます。JavaScriptを読み込むと、次にlocalStorageに保管していたトークンを読み出します。最後にこのトークンを含めたリクエストをサーバに投げて、プライベート情報を含むJSONを受け取ります。
- SSRは一番最初にトークンを送ります: SSRでは最初に受け取るHTMLにすでにプライベート情報が含まれています。そのためブラウザネイティブの機能を使って、Cookie🍪にあるトークンを最初のリクエストと同時にサーバに送ります。最初の通信より前にやりますので、ブラウザネイティブの機能しか使えません。手法として実質Cookie🍪しかないのはそのためです。
トークンの送り先の指定
-
CSRはトークンの送り先を自由に指定できます: ブラウザを
frontend.example.com
に向けながら、JavaScriptでapi.example.com
とにトークンを渡したりできます。そしてプライベートな通信ができます。JavaScriptなので、原則としてどこのホストでも認証できます[4]。 -
SSRはトークンを同じホストにしか送りません: 例えばアクセスしているドメインが
www.example.com
ならば、以前にそこをアクセスしたときに付与されたCookie🍪しか送信できません[5]。Cookie🍪を使う以上は同じホストにしかトークンが送れませんので、必ずでそこで認証を行う必要があります。
トークンの保管先
- CSRはどこにトークンを保管しても良い: JavaScriptでトークンをサーバに送りますので、トークンの保管先は自由です。localStorageでも良いし、Cookie🍪でも良いし、メモリの中に保管しても構いません[6]。
-
SSRはトークンをCookieに保管するしかない: 最初のリクエストと同時にブラウザネイティブの機能として自動的にトークンを送る必要があります。そうなるとトークンの保管先はCookie🍪しかありません。
- 昔のiModeではCookie🍪が使えなかったため、URLにトークンを含めることもありました(例えば
http://example.com?session_id=1234
)。こうすれば確かにブラウザネイティブの機能として自動的にトークンが送られますが、...流石にこれは今では非推奨です...。
- 昔のiModeではCookie🍪が使えなかったため、URLにトークンを含めることもありました(例えば
CSRとSSRのトークン管理の違いのまとめ
このようにCSRとSSRではトークンの保管の仕方、トークンの送信の仕方の制限が大きく異なります。一言でいうと、SSRはトークン送信タイミングの関係上、必ずCookie🍪を使う必要があり、Cookie🍪を使う関係上、どこで認証できるかが限定されます。以下では実際の現場でその違いをどのように解決・吸収しているかを見ていきます。
実例: テックブログから
下記では公開されているテックブログを参考に技術構成を推測し、議論しています。
あくまでも技術構成を推測しているだけですので、不正確なところはきっとありますので、ご了承ください。
メドレー:CSRの認証だけを行い、SSRの認証を取りやめた例
メドレーの例を最初に見ていきます。
- 認証はRuby on Railsサーバが行います
- HTMLテンプレート(ERB)の認証をCookie🍪で行います
- Next.js CSRページ用のJSON APIの認証も同じCookie🍪で行います
- Next.js SSRページは別ホストにいるため、Cookie🍪情報をNext.jsに渡すことができません。
- 別ホストなので、ブラウザは上記の認証用のCookie🍪は送りません
- 認証できないので、プライベートデータをSSRで配信することを諦めました
今後はNext.jsのSSRでも認証を実現したいとのことですが、既存の認証の追加・修正や新規認証コード作成などが必要になりそうです。下記2つの案のどれかに近づけていくのではないかと思います。
Zozo:複数ドメインのCookieを同期する
ZozoでフロントをNext.jsにした例
事前に行ったセッション管理の変更
- 従来はASPで運用していたHTMLテンプレートに対して、Cookie🍪で認証を行っていました。
- Microservice化およびフロントエンドのBFF化を進めています。
- Next.jsサーバはSSRを生成し、Cookie🍪で認証を行います。
- ASPのページとNext.jsのページではそれぞれ異なる認証処理が行われ、ホストも異なりますので、Cookie🍪も独立です。
- 独立なCookie🍪を同期するために、ASPおよびNext.jsのサーバはセッション情報を一つのRedis DBに保管して共有します。Cookie🍪がそれぞれ独立であっても、内容は同期しているためにセッション状態は共有されます。
記事にも書いてありますが、フロントエンドをNextJS化するのに先立ってASP側(IIS)のセッション管理を刷新してRedisに引越させています。逆に言うと、この仕組みで運用するためには既存のセッション管理を書き換える必要があります。
なお似たような構成でRedisの代わりにAuth0やAWS Cognitoを共通の認証プロバイダーにすることも可能だと思います。しかし私の知る限りだと、Auth0やCognitoのサーバを毎回叩いているとコストもかかる上、ネットワーク遅延も発生しますので、普通はキャッシュを入れるでしょう。そうなると完全に同期しなくなる可能性があります。
GMO ペパボ:Cookie転送
GMO ペパボの例です。
- 認証はRuby on Railsサーバが担います。
- セッションCookie🍪はサブドメイン共通のものを使用しています[5:1]。
- CSR時には、ブラウザは直接 Rails APIサーバにJSON APIを送ります。Cookie🍪認証です。
- ExpressのSSRページにアクセスしたときも同じサブドメインCookie🍪が送られます。
- SSRサーバは認証機能を持っていませんので、Cookie🍪の中身をそのままRuby on Railsサーバに転送します。
- Ruby on Railsサーバで認証が行われ、プライベート情報へのアクセスが可能になります。
既存の認証システムの書き換えが不要で、かつExpressサーバ側で認証コードを書く必要がありません。そのため非常にシンプルな構成になり、自分がやった範囲だとCookie🍪転送用コードを数行分Expressサーバに追加するだけで十分かもしれません。このようにCookie🍪の中身を転送する方法は、少し珍しい気もしますが、手軽であり、私も似た方法を何回か聞いたことがあります。
ただしこのやり方はサブドメインCookie🍪を使う必要があります。
感想
最後に、テックブログを読んだり、勉強したり、あるいは私が経験した現場のプロジェクトを見て感じたことを少し述べます。
- どのようなサーバ構成にするか、あるいはCSRを使うかSSRの選択は認証システムに大きな影響があります。新しい機能で最初に微妙な選択をしてしまったために、あっちこっちも変えなければならなくなるという本末転倒な事態も起こり得そうです。熟慮するべきでしょう。
- 最近はNext.jsがVercelで便利にホスティングできるので、あまり深く考えずにSSRを採用し、別のホスト名のサーバから運用する例が多いと理解しています。ちゃんと熟慮しましょう。
- 小さいチームの場合は、よりシンプルな構成を考えた方が良いかもしれません。自分の記事が多く、我田引水も甚だしいのですが、いくつかの例を紹介します。
- Hotwireを使えばRailsの認証システムをそのまま使うだけですので、悩む必要はありません。HotwireはRails以外でも使えますし、高度なUIも実現できます。似たものとして、HTMXも話題です。
- SEOが不要であれば、React Router v7のSPAモードを使うのも有望な選択肢です。Railsのpublicフォルダに静的ファイルをホストするだけで、やはりRailsの認証システムをそのまま使えます。クライアントサイドのルータはもちろん、自動code-splittingによるバンドルサイズ縮小、SSR同様のUXを実現するデータロードパターンなど、Next.jsやRemixの恩恵の多くがSPAでも受けられます。
念のため、ここではSSRの認証が複雑だと言っているのではありません。SSRの認証はCookie🍪一択ですし、かなり枯れた技術ですし、むしろ簡単です。難しいのはドメイン・ホストを分けつつ、共通の認証方式を使うことです。Vercel等のおかげもあって、フロントとバックのドメインを分けることが簡単になっていますが、注意が必要です。
いずれにしても、とにかくよく考えましょう。そしてHotwire, HTMXのようにHTMLテンプレート方式(ERB, Blade等)を大幅に改善した技術や、React Router v7 SPAモードのようにSPAを改善した技術も発展しています。これらにしっかりにキャッチアップするのも重要です。
従来の簡単だったアプローチをモダンな視点で再考するのも、非常に有効だと思います。
-
CSR, SPA, SSRの定義は難しいので、このポストにおける定義について軽く紹介します。認証だけを取り上げていますので、認証の時にどこのサーバに対してどのような通信を行うかだけを考えています。CSR, SPAは最初にプライベート情報を含まない、空っぽのHTMLを受け取るものです。一方でSSRは最初のHTMLにもプライベート情報が含まれているものを指します。これらは認証が必要なタイミングに大きな差があります。 ↩︎
-
認証でよく出てくる「トークン」という言葉ですが、私は「鍵」と理解するようにしています。例えばホテルでは最初に受付をして、名前と予約を照合して、クレジットカードなどで本人確認をした上て最後に鍵を渡されます。これがID、パスワード入力に相当します。部屋に入るたびにこれをやっていると大変なので、受付は鍵を渡してくれます。この鍵を使えば、いちいち受付に行かなくても部屋に入れるようになります。これが「トークン」の役割です。 ↩︎
-
サーバ側は認証情報確認(idとパスワード)とトークン作成と配信、受信トークン確認(認証)の処理があります。日常生活に例えると、認証情報確認は身分証明書の確認、トークン作成は鍵の作成と渡し、トークン確認は鍵穴の用意に相当します。 ↩︎
-
本記事ではCORSやCookie🍪のSameSite属性の話はしません。これらはどちらかというとCSRの場合の話であるためです。本記事ではSSRを軸に話をしますが、別の機会にCORS, SameSiteについても話していきたいと思います。 ↩︎
-
サブドメインCookie🍪を使うと配信先を拡張できます。例えば
www.example.com
とapi.example.com
でCookie🍪を共有できるようになります ↩︎ ↩︎ -
localStorageにセキュリティ上の懸念があるか否かという話題はよく見かけます。ここでは議論しませんが、ただ一般論として技術選定は自分だけが納得すれば良いものではなく、チームや将来メンテする人、あるいは第三者の監査人等が納得するかどうかも重要です。後で他の人にぐちぐち言われるような技術は、例え自分なら論破できたとしても極力避けるべきだとは思います。 ↩︎
Discussion