約1週間にわたり、ポータルを開いた再訪ユーザーは、ログイン画面が表示されるまで「Loading…」という文字が画面に10秒間表示されるのを目撃しました。たまにではなく、だいたいではなく、毎回ぴったり10秒です。そして、その後はすべてが正常に動作しました。初回訪問者にはこの現象は見られませんでした。以前にサインインしたことがある人だけに発生しました。
ランダムな時間がかかるバグはパフォーマンスの問題です。正確に10秒かかるバグは自白です。健全なWebリクエストの中で、きれいな10の累乗に丸められるようなものはありません。この数字は実際の作業の合計ではなく、タイムアウトの上限値であり、タイムアウトは、何かが到達しないものを待機していることを意味します。
これは、何を待機していたのか、そして待機していたものが、私たちが誇りに思っていたセキュリティヘッダーによってブロックされた理由についての物語です。
ポータルのマウント時の動作
私たちのポータルはシングルページアプリケーションです。ロード時に、何かを表示する前に、1つの質問に答えようとします。すでにサインインしているか?OIDCでそれを行う丁寧な方法は、サイレントチェックです。アプリケーションはアイデンティティプロバイダーに「このブラウザにすでにセッションがある場合、ユーザーに迷惑をかけることなく新しいトークンを渡してください」と尋ねます。当社のクライアントライブラリoidc-client-tsは、これをsigninSilent()として公開しており、renewSession()ヘルパーからマウント時に呼び出していました。
このサイレントチェックを実行する方法は2つあります。アプリケーションがリフレッシュトークンを保持している場合、ライブラリはUIを介さずに静かなバックチャネル交換を行い、サインイン完了です。リフレッシュトークンを保持していない場合、ライブラリは古いメカニズムにフォールバックします。アイデンティティプロバイダーの認可エンドポイントを指す非表示のiframeをprompt=noneで開き、そのiframeが結果をポストバックするのを待ちます。iframeの要点は、それが不可視であることです。表示されるはずがなく、健全な設定ではミリ秒単位で解決されるため、表示されることはありません。
私たちの場合、まったく解決しませんでした。
自ら構築した壁
iframeは認証ホストをロードします。そして、認証ホストは、すべての本格的なホストと同様に、「フレームに入れることを許可しない」という2つのヘッダーを送信します。
X-Frame-Options: DENYContent-Security-Policy: frame-ancestors 'none'
これらはクリックジャッキング防御策であり、正しいものです。攻撃者がログインページをiframeできる場合、デコイの下に浮かべて、ユーザーに無害に見えるものに実際の認証情報を入力させ、収集することができます。frame-ancestors 'none'は、どのオリジンもこのページを埋め込むことを許可しないという現代的な指示です。意図的に有効にしました。セキュリティレビューで探し求め、報われるべきまさにその種のものです。
したがって、oidc-client-tsがそのホストに対して非表示のiframeを開くと、ブラウザはまさに私たちが指示した通りの動作をしました。フレーム内のページのレンダリングを拒否したのです。そして、残酷な部分があります。拒否されたフレームはエラーをスローしません。ライブラリがキャッチできるエラーイベントも、拒否されたPromiseも、コンソール行もありません。iframeはただ空のまま、 indefiniteに座っています。ライブラリの観点からは、何も起こっていないため、唯一可能なことを行います。タイムアウトを待ちます。そのタイムアウト、つまりデフォルトのsilentRequestTimeoutは10秒です。
非表示のiframeが空白の壁を見つめる10秒間、その後ライブラリはあきらめ、Promiseが最終的に拒否され、アプリケーションは肩をすくめて実際のログインページにリダイレクトし、すべてが動作します。ハングは決して失敗ではありませんでした。失敗する運命のiframeを通る景色の良いルートを経た成功でした。
2つの正しいもの、1つの悪い継ぎ目
これが本当に見えにくかった理由は、何も壊れていなかったことです。セキュリティヘッダーは正しかったです。サイレントリニューアルのフォールバックは正しく、合法的で広く使用されているOIDCパターンでした。すべてのコンポーネントが、設計どおりに、レビューアが望むとおりに正確に動作しました。10秒間のハングは、それらのいずれかの中に存在しませんでした。それらの間の空間に存在し、それぞれが他方について行った仮定の中に存在しました。SSOライブラリは、アイデンティティプロバイダーをフレーム化できると仮定しました。アイデンティティプロバイダーは、誰もフレーム化を許可すべきではないと仮定しました。どちらの仮定も擁護可能です。単に互換性がなく、単一のファイルに矛盾が含まれていませんでした。
なぜiframeに到達しようとしたのか
それでもまだ質問が残っていました。高速パスであるリフレッシュトークン交換は、iframeを完全にスキップしたでしょう。なぜ再訪ユーザーが低速パスに到達したのでしょうか?リフレッシュトークンを保持していなかったからです。そして、リフレッシュトークンがなかったのは、ポータルOAuthクライアントがAllowOfflineAccessなしでプロビジョニングされていたためです。これは、クライアントが発行されることを許可するフラグです。オフラインアクセスなし、リフレッシュトークンなし、高速パスなし、そしてすべての再訪ユーザーは、ロードできないiframeに転送されました。
それが本当の欠陥であり、すべてのテナントに広がるデータの問題であり、1行のコード変更で一度に展開できるものではありませんでした。したがって、修復は、起動時にすべてのテナントのポータルクライアントにAllowOfflineAccessを再適用する調整サービスであり、手動でテナントに触れることなく、次のデプロイでフリート全体を修正します。リフレッシュトークンが再び流れ始め、高速パスが自動的に復活しました。
修正と教訓
調整サービスは原因を修正しました。しかし、ログインは低速パスに到達した場合でも10秒間停止すべきではないため、継ぎ目も強化しました。renewSession()は、保存されたユーザーを最初に検査するようになりました。リフレッシュトークンがない場合、即座に何も返さず、失敗するとわかっているiframeをスキップし、ユーザーをインタラクティブログインに直接送信します。リフレッシュトークンの高速パスは変更されません。また、iframeを開く可能性のあるバックグラウンドリニューアルのバックストップとして、タイムアウトを10秒から5秒に短縮し、最悪の場合を半分にしました。
実際に得た教訓は、この1つのインスタンスではなく、バグのカテゴリに関するものです。ハングはバグです。エラーも、例外も、ログの赤い線も出力しません。残す唯一の証拠は経過時間です。そして、その時間がきれいな丸い数字である場合、スローな作業を最適化するために狩りをするのではなく、タイムアウトを探し、そしてその反対側にある、静かに、永久に、決して応答しないものを探してください。私たちの場合はiframeで、意図的に閉ざしたドアを丁寧にノックしていました。
ロックダウンされた認証ホストとサイレントリニューアルiframeが混在しないことを、ログイン フローがすでに知っている方が良い場合は、それが10秒ごとのページロードで互換性がないことを発見する2つの正しい半分としてではなく、一緒にテストされた1つのシステムとしてSSO配管とセキュリティヘッダーを出荷する、Authagonalがすでに遭遇した継ぎ目です。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.