🔐

ブラウザのタブ間で認証状態を同期する

2023/12/22に公開

この記事は MICIN Advent Calendar 2023 の 22 日目の記事です。
前回は影山さんの『SEO未経験のエンジニアが1年間SEO対策を通じて学んだこと』でした。

はじめに

ウェブアプリケーションを作っていて認証状態をブラウザのタブ間で同期したいと思ったことはありませんか?
あるいはマルチテナントアプリケーションであれば、あるタブでテナントを切り替えたら他のタブでも選択中のテナントを切り替えて再読み込みしてほしいと思ったことはありませんか?

MICINで扱っている医療機関向けのプロダクトでは1つのデバイスを複数人で扱うことも多いです。
ユーザーが特定のタブでログアウトしても別タブがそのままであれば、他の人に機微な情報を見られてしまう可能性があります。

こういったリスクを避けるために、ブラウザのタブ間で認証状態を同期し、状態に変更があれば再読み込みや別ページに遷移する方法を紹介したいと思います。
まずは扱うドメインが1つだけのケースから、発展形として複数ドメインで動作する方法も扱います。
Reactによるサンプルコードもあるので参考にしてもらえればと思います。

localStorageを活用して認証状態を同期する

やり方の1つとして、ポーリングやServer Sent Events等サーバーを介して行う方法があげられると思います。
今回はよりお手軽にフロントエンドだけで完結するlocalStorageを利用する方法を紹介します。

Windowにはstorageイベントが定義されており、addEventListenerによってイベントリスナーを登録しておくと、localStorageが変更された際に任意の関数を実行できます。

https://developer.mozilla.org/ja/docs/Web/API/Window/storage_event

本来、これは同じ保存領域を使用している同じドメインの他のページが更新を同期するための仕組みです

MDNのドキュメント中にもこのような記載があり、今回のような用途にうってつけなのだと思われます。

単一ドメインへの対応のみでよいケース

まず初めに動作イメージを見てもらえればと思います。

次の図のような流れで同期が行われています。

以下具体的なコードと説明です。

1.認証状態の変更によってlocalStorageを変更する

storageイベントのイベントリスナーを定義します。localStorageが変更されてkeyがtenantNameであれば画面をリロードします。

https://github.com/nekootoko3/auth-sync-sample/blob/98ce8d5a84a451964d9dba9868e988866e94a673/apps/simple/app/dashboard/page.tsx#L35-L39

コンポーネントが描画されると定義したイベントリスナーを登録します。

https://github.com/nekootoko3/auth-sync-sample/blob/98ce8d5a84a451964d9dba9868e988866e94a673/apps/simple/app/dashboard/page.tsx#L14-L20

2.localStorageイベントの発行に応じて画面のリロードやリダイレクトを行う

各ボタンを押下することでlocalStorageを変更します。

https://github.com/nekootoko3/auth-sync-sample/blob/98ce8d5a84a451964d9dba9868e988866e94a673/apps/simple/app/auth/page.tsx#L3-L40

複数ドメインへの対応が必要なケース

こちらも動作イメージから見てもらえればと思います。
http://localhost:3000/authのページで認証状態を変更することによって、http://localhost:3001http://localhost:3002のページがリロードしていることが見てとれると思います。

全体の流れとしては次のようになります。

iframe が登場していたり、単一ドメインのケースからはいくらか複雑になっています。
この背景として、localStorageには異なるオリジンからはアクセスできないという制約があります。そのためにhttp://localhost:3000からhttp://localhost:3001のlocalStorageを直接変更できないのです。
この制約の中で異なるオリジンに対して変更を伝えるためWindow.postMessageを利用しています。

https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage

Window オブジェクト間で安全にオリジン間通信を可能にするためのメソッドです。
例えば、ポップアップとそれを表示したページの間や、iframe とそれが埋め込まれたページの間での通信に使うことができます。

今回紹介する実装でもhttp://localhost:3000http://localhost:3001のiframeを埋め込むことでオリジン間で認証状態の変更を伝えています。
Window.postMessageを介して伝えられるイベントはmessageイベントとして定義されています。

https://developer.mozilla.org/ja/docs/Web/API/Window/message_event

このイベントと単一ドメインのケースでも使ったlocalStorageの変更イベントをlistenする方法を併用することで複数ドメインへの対応を行います。必要なことを列挙すると以下3点になります。

  1. 各サービス画面にWindow.postMessageで発行されたイベントを受け取って、localStorageを変更するページを用意します。
    a. このページは認証画面中のiframeで描画されますが、非表示となる想定なので中身は空でよいです。
  2. 認証を担うページで1で用意したページをiframeで描画し、認証状態の変更に応じてiframeへの参照にpostMessageします。
  3. localStorageイベントの発行に応じて画面のリロードやリダイレクトを行います。

1と2について具体的なコードとともに見ていきます。
3については単一ドメインのケースと同じ内容になるので割合します。

1. postMessageで発行されたイベントを受け取ってlocalStorageを変更する

ページ描画時にmessageイベントのイベントリスナーを登録します。
イベントリスナーは特定のオリジンから特定のメッセージを受け取るとlocalStorageを変更します。

https://github.com/nekootoko3/auth-sync-sample/blob/98ce8d5a84a451964d9dba9868e988866e94a673/apps/docs/app/auth/page.tsx#L7-L28

2. 認証ページでiframeへの参照にpostMessageする

認証を担うページ内で1で用意したページをiframeで描画します。内容に関心はないので非表示にしています。
認証状態を変更するボタンが押下されると、iframeへの参照にpostMessageします。

https://github.com/nekootoko3/auth-sync-sample/blob/98ce8d5a84a451964d9dba9868e988866e94a673/apps/auth/app/auth/page.tsx

各サービスに対してpostMessage、各サービスではmessageを受け取ってlocalStorageを変更、その変更によって画面をリロードするという流れになっています。

複数ドメインへの対応が必要なケース(番外編)

さきほど認証サービスに各サービスのiframeを埋め込んで対応する方法を紹介しました。
各サービス側で認証サービスのiframeを埋め込んで認証状態を同期する方法もあります。
これまで紹介した原理の応用になるので全体の流れのみ載せておきます。

さいごに

認証後のページが残らないようにすることでセキュリティを向上させる実装について紹介しました。
サンプルコードは https://github.com/nekootoko3/auth-sync-sample に置いてあります。
他にもっと良いやり方がある、こういったケースではうまくいかないのでは、といったフィードバックあればぜひ教えてください。


MICINではメンバーを大募集しています。
「とりあえず話を聞いてみたい」でも大歓迎ですので、お気軽にご応募ください!

https://recruit.micin.jp/

株式会社MICIN

Discussion