Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Multi-Tab Session Sync in a PKCE SPA with `okta-auth-js`

2026年10月08日に「Dev.to」が公開したITニュース「Multi-Tab Session Sync in a PKCE SPA with `okta-auth-js`」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

`okta-auth-js`を使ったPKCE SPAで複数タブを開くと、認証情報の同期が課題となる。SDKだけではトークン更新の競合やログイン処理の問題が発生し、誤動作の原因に。Web Locks APIでタブ間のリーダーを決め、ストレージ操作やトークン更新を慎重に行うことで、安全で安定した多タブ環境を構築できる。

ITニュース解説

シングルページアプリケーション(SPA)において、PKCE(Proof Key for Code Exchange)という安全な方法で認証を行う際、Oktaが提供する認証SDKであるokta-auth-jsを利用すると、複数タブで認証状態を同期させることが難しくなるという問題がある。PKCEは、認証コードの盗難を防ぎ、セキュリティを向上させるための技術で、特にモバイルアプリやSPAのような公開クライアントで広く利用される。ブラウザの各タブはそれぞれ独立したOktaAuthインスタンスを持ち、独自のタイマーやメモリ上の認証状態を管理している。しかし、すべてのタブはブラウザのlocalStorageという共有ストレージを利用するため、アクセストークンやリフレッシュトークンといった認証情報が複数のタブから同時に読み書きされる「共有される可変状態」となり、競合やデータの不整合を引き起こす可能性がある。

okta-auth-jsのSDKは、この問題の一部を解決するための機能を提供している。例えば、tokenManager: { storage: localStorage, syncStorage: true, autoRenew: true }という設定を行うことで、トークンをlocalStorageに保存し、syncStorage: trueによって他のタブがlocalStorageにトークンを書き込んだ際に発生するストレージイベントを検知し、自身の認証状態を更新できる。また、autoRenew: trueを設定すれば、トークンの有効期限が近づくと自動的に更新処理が実行されるため、アプリケーションのセッションを継続的に維持できる。

しかし、SDKが提供する機能だけでは解決できない問題も存在する。特に大きな問題は、トークンの更新処理における相互排他制御が欠けていることだ。これは、複数のタブが同時にトークンの更新(リフレッシュ)を試みる可能性があることを意味する。開発環境ではexpireEarlySecondsという設定で更新タイミングをずらすことができるが、これは本番環境では無視されるため、複数のタブがほぼ同時に「リフレッシュトークンを使って新しいアクセストークンとリフレッシュトークンを取得する」という処理を実行してしまう。リフレッシュトークンは通常一度しか使えないため、一つのタブが成功すると、他のタブは失敗することになる。また、ログイン時のリダイレクト処理も調整されないため、各タブが独立して認証プロバイダーとの間で認証フローを開始し、それぞれが独自のワンタイムコードを持って戻ってくる可能性がある。PKCEトランザクションの保護も不十分であり、認証フローの途中で一時的にlocalStorageに保存されるcode_verifierが、他のタブによって意図せずクリアされてしまうと、進行中のログイン処理が破壊される原因となる。さらに、どのタブが認証処理の「リーダー」となるべきかという概念がSDKにはなく、開発者自身がその選出ロジックを実装する必要がある。

その他の特性として、認証プロバイダーからのリダイレクトコールバックページでは、SDKは意図的に認証状態の更新をスキップするため、authStateが一時的にnullになる。また、同じドキュメント内で複数のSDKインスタンスが存在しても、localStorageへの書き込みがストレージイベントを発火しないため、互いの存在を認識できないという点も理解しておくべきだ。

これらの問題により、いくつかの典型的な失敗モードが発生する。例えば、「リフレッシュトークンでのinvalid_grantエラー」は、複数のタブが同じ使い捨てリフレッシュトークンを猶予期間外で同時に使おうとした場合に起こる。「OAuth redirect params from storageの取得失敗」は、あるタブがログイン処理中に別のタブがlocalStorageをクリアしてしまった際に発生する。「認可コードでのinvalid_grantエラー」は、コードが再利用されたか、60秒の有効期限を過ぎてから交換された場合に起こる。古いトークンが下流のシステムに渡されるのは、レンダリング時にキャッシュされたトークンがlocalStorageの最新値と異なっていた場合だ。一つのタブのエラーが原因で他の全てのタブがログアウトしてしまうのは、localStorage.clear()がsyncStorageを通じて伝播したためである。サイレントリニューアル(バックグラウンドでのトークン更新)が失敗し、隠しiframeを使ったフローにフォールバックしても応答がない場合、OAuthフローがタイムアウトする可能性もある。

これらの失敗を防ぎ、安定した複数タブでの認証を実現するためには、いくつかの設計ルールを適用することが推奨される。まず、一つのドキュメント(ブラウザのタブ)につきOktaAuthインスタンスは一つだけ作成し、アプリケーション全体で共有すること。そして、トークンの更新処理における競合を防ぐためには、Web Locks APIを利用して「リーダー」を選出し、排他ロックをかけることが非常に効果的だ。Web Locks APIは、ブラウザの異なるタブ間でリソースへのアクセスを協調させるための仕組みであり、リーダーに選出されたタブだけが更新処理を実行するように制御できる。ロックを取得した後も、別のタブがすでに更新を完了していないかを再確認するべきだ。ログイン時のリダイレクト処理も同様にシリアル化し、リーダータブが認証フローを完了するまで、他のタブ(フォロワー)はストレージイベントを通じてトークンが現れるのを待つようにする。

ストレージ操作に関しては、アプリケーション全体で共有されるlocalStorageを安易にlocalStorage.clear()で全てクリアすることは避けるべきだ。PKCEトランザクションが進行中の場合は特に危険であり、自分のアプリケーションが管理する名前空間のキーのみを削除し、他のアプリケーションや他のタブの処理に影響を与えないようにする。トークンは、レンダリング時に一度読み出してキャッシュするのではなく、getIdToken()やgetAccessToken()といったメソッドを使って、必要とされる直前にlocalStorageから最新のものを読み出すようにする。これにより、トークンの有効期限切れや更新による不整合のリスクを減らせる。トークンをlocalStorageに書き込む際は、tokenManager.setTokens()メソッドを経由して書き込み、他のタブがストレージイベントを介してその変更を認識できるようにすることが重要である。

認証プロバイダー側でも、リフレッシュトークンの回転猶予期間(grace period)を、最悪の場合のタブ間のハンドオフ時間よりも長く設定するべきだ。invalid_grantエラーが発生した場合は、まず「他のタブがすでにトークンを更新した」という可能性を考慮し、もう一度ストレージから最新のトークンを読み直してから、それでも解決しない場合に再認証を試みるようにする。リフレッシュトークンを安易に削除するべきではない。また、トークン更新はタブがユーザーに見えている「可視状態」の時のみ実行するようにする。バックグラウンドでフリーズしているタブはリクエストが滞り、再開時に古いタイマーが一斉に発火して余計な処理を引き起こす可能性があるからだ。認証コールバックページは、アプリケーションの状態やフラグ、データに依存しないように独立させるべきである。認可コードは60秒という短い有効期限しかないため、ページがロードされたらすぐに交換処理を実行する必要がある。

Identity-provider(Oktaなどの認証プロバイダー)の設定も重要となる。リフレッシュトークンの回転猶予期間は、複数タブ間での最悪のハンドオフ時間よりも長く設定する。リフレッシュトークンの非アクティブウィンドウは、アクセストークンの有効期限の3倍以上にするべきだ。リフレッシュトークンの絶対的な寿命は、IDプロバイダーのセッション寿命よりも長く設定しないと、リフレッシュトークンを使うメリットが薄れてしまう。これらの設定を行うと、リフレッシュトークンが使われている間は、アプリのセッションがシングルサインオン(SSO)セッションよりも長くなる可能性があることを理解しておく必要がある。また、リフレッシュグラント(refresh grant)を有効にすると、新しいログインにのみ影響し、既存のセッションはすでに持っているスコープでトークンを更新することになる点も認識しておくべきだ。

複数タブ環境での認証の安定性を確保するには、包括的なテストが不可欠である。「2つのタブがすでにログインしている」という基本的なテストだけでは不十分であり、より複雑なシナリオをテストする必要がある。例えば、あるタブでフルログインリダイレクトを実行中に別のタブが開いている場合や、タブがフリーズしてから再開したときに更新処理が適切に行われるか、一つのタブでログアウトしたときに他のタブがクリーンに終了するかなどを確認する。両方のタブが同時に有効期限切れになった場合に、認証プロバイダーへのリクエストが一度だけ行われるかも重要だ。ディープリンクやreturnUrlとログインリダイレクトの組み合わせ、サードパーティCookieがブロックされた場合の挙動(iframeでの更新がリダイレクトにフォールバックするか)などもテストすべきである。特に、複数のタブを開いた際に、トークン更新サイクルあたりの/tokenエンドポイントへのPOSTリクエストが、タブの数に関わらず常に1回になっているかを確認することは、リーダー選出が正しく機能しているかを判断する上で非常に重要だ。

最後に、可観測性の確保も忘れてはならない。リーダー/フォロワーの状態、ロックの待機時間、使用時点でのトークンのiat(発行時刻)とlocalStorage内のiatの比較、更新サイクルあたりの/tokenへのPOSTリクエスト数、失敗時のdocument.visibilityState(タブの表示状態)などをログとして記録することが重要だ。特に、「1時間あたりのセッションごとの更新回数」という指標は、非常に価値がある。この数値は、開いているタブの数に関わらず一定であるべきであり、もしタブの数に比例して増加するようであれば、リーダー選出のメカニズムが適切に機能していないことを示唆する。これらの監視を行うことで、問題の早期発見と原因特定が可能となる。

関連コンテンツ

関連IT用語

関連ITニュース