優れたログアウト エクスペリエンスの条件

Kenji Baheux
Kenji Baheux

ログアウト

ユーザーがウェブサイトからログアウトすると、パーソナライズされたユーザー エクスペリエンスから完全に離れてほしいという要望が伝えられます。したがって、ユーザーのメンタルモデルにできる限り忠実に従うことが重要です。たとえば、適切にログアウトするには、ユーザーがログアウトする前に開いていたタブも考慮する必要があります。

優れたログアウト エクスペリエンスを実現する鍵は、ユーザー エクスペリエンスの視覚的要素と状態の要素全体で一貫性を保つということです。このガイドでは、特に注意すべき点と、適切にログアウトする方法に関する具体的なアドバイスを提供します。

考慮すべきポイント

ログアウト機能をウェブサイトに実装する際は、スムーズで安全かつ直感的なログアウト プロセスを実現するために、次の点に注意してください。

  • 明確で一貫性のあるログアウト UX: ウェブサイト全体で識別しやすく、いつでもアクセスできるログアウト ボタンまたはリンクを提供します。あいまいなラベルを使用したり、わかりにくいメニューやサブページなど、わかりにくい場所にログアウト機能を非表示にしたりしないでください。
  • 確認プロンプト: ログアウト プロセスを完了する前に確認プロンプトを実装します。これにより、ユーザーが誤ってログアウトするのを防ぎ、ユーザーが本当にログアウトする必要があるかどうかを再検討できるようになります(たとえば、安全なパスワードやその他の認証メカニズムを使用してデバイスを注意深くロックする場合など)。
  • 複数のタブを処理する: ユーザーが同じウェブサイトの複数のページを別々のタブで開いていた場合、1 つのタブからログアウトすると、そのウェブサイトで開いている他のすべてのタブにも反映されます。
  • 安全なランディング ページにリダイレクトする: ログアウトしたら、ログインしなくなったことを明確に示す安全なランディング ページにユーザーをリダイレクトします。個人情報を含むページにユーザーをリダイレクトすることは避けてください。同様に、他のタブにもログイン状態が反映されないようにします。また、攻撃者が悪用できるオープン リダイレクトを作成しないようにしてください。
  • セッションのクリーンアップ: ユーザーがログアウトしたら、そのユーザーのセッションに関連付けられている機密性の高いユーザー セッション データ、Cookie、一時ファイルを完全に削除します。これにより、ユーザー情報やアカウント アクティビティへの不正なアクセスが防止されるほか、ブラウザが機密情報を含むページをさまざまなキャッシュ、特にバックフォワード キャッシュから復元するのを防ぐことができます。
  • エラー処理とフィードバック: ユーザーのログアウト時に問題が発生した場合は、明確なエラー メッセージやフィードバックを提供します。ログアウト プロセスが失敗した場合は、潜在的なセキュリティ リスクやデータ漏洩について通知します。
  • ユーザー補助機能に関する考慮事項: 障がいのあるユーザーがログアウト機能にアクセスできるようにします。これには、スクリーン リーダーやキーボード ナビゲーションなどの支援技術を使用するユーザーも含まれます。
  • クロスブラウザ互換性: さまざまなブラウザやデバイスでログアウト機能をテストして、一貫性があり確実な動作になることを確認します。
  • 継続的な監視と更新: ログアウト プロセスを定期的に監視し、潜在的な脆弱性やセキュリティ ループの有無を確認します。タイムリーなアップデートとパッチを実装して、特定された問題に対処します。
  • ID 連携: ユーザーがフェデレーション ID を使用してログインしている場合は、ID プロバイダからのログアウトもサポートされており、必要かどうかを確認します。また、ID プロバイダが自動ログインをサポートしている場合は、必ず自動ログインを阻止してください。

すること

  • ログアウト フロー(または他のアクセス取り消しフロー)の一環としてサーバーの Cookie を無効にする場合は、ユーザーのデバイスでも Cookie を削除してください。
  • ユーザーのデバイスに保存した可能性のある機密データ(Cookie、localStoragesessionStorageindexedDBCacheStorage、その他のローカル データストア)をクリーンアップします。
  • 機密データを含むリソース(特に HTML ドキュメント)は、ブラウザがこれらのリソースを永続ストレージ(ディスク上など)に保存しないように、Cache-control: no-store HTTP ヘッダーとともに返されるようにします。同様に、機密データを返す XHR/fetch 呼び出しも、キャッシュを防ぐために Cache-Control: no-store HTTP ヘッダーを設定する必要があります。
  • ユーザーのデバイスで開いているすべてのタブが、サーバーサイドでのアクセス権の取り消しによって最新の状態になっていることを確認してください。

ログアウト時の機密データのクリーンアップ

ログアウト時に、エフェメラル データやローカルに保存されている機密データを消去することを検討してください。機密データに重点を置いているのは、すべて消去してしまうと、このユーザーが再度アクセスする可能性が大幅に低下するため、ユーザー エクスペリエンスが大幅に低下するという事実に起因しています。たとえば、ローカルに保存されたデータをすべて消去した場合、ユーザーは Cookie 使用の同意を求めるメッセージに再度同意し、そもそもウェブサイトにアクセスしたことがないものとして他のプロセスを実行しなければならなくなります。

Cookie をクリーンアップする方法

ログアウト ステータスを確認するページのレスポンスに Set-Cookie HTTP ヘッダーを追加して、機密データに関連する Cookie や機密データを含むすべての Cookie を消去します。expires の値は遠い過去の日付に設定し、Cookie の値は空の文字列に設定します。

Set-Cookie: sensitivecookie1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure
Set-Cookie: sensitivecookie2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure
...

オフラインのシナリオ

上記のアプローチは一般的なユースケースには十分ですが、ユーザーがオフラインで作業している場合はうまくいきません。ログイン状態をトラッキングするために、安全な HTTPS 専用の Cookie と、JavaScript でアクセスできる通常の Cookie の 2 つの Cookie を設定することをおすすめします。ユーザーがオフライン中にログアウトしようとしている場合は、JavaScript Cookie を消去し、可能であれば他のクリーンアップ処理を続行します。Service Worker がある場合は、Background Fetch API を利用して、ユーザーが後でオンラインになったときにサーバーの状態をクリアするリクエストを再試行することもできます。

ストレージをクリーンアップする方法

ログアウト状態を確認するページのレスポンスで、さまざまなデータストアから機密データをクリーンアップします。

  • sessionStorage: ユーザーがウェブサイトとのセッションを終了すると消去されますが、ユーザーがログアウトしたときにセンシティブ データを事前にクリーンアップすることを検討します。これは、ユーザーがウェブサイトで開いているタブをすべて閉じ忘れた場合に備えるためです。

    // Remove sensitive data from sessionStorage
    sessionStorage.removeItem('sensitiveSessionData1');
    // ...
    
    // Or if everything in sessionStorage is sensitive, clear it all
    sessionStorage.clear();
    
  • localStorageindexedDBCache/Service Worker API: ユーザーがログアウトしたら、これらの API を使用して保存した機密データをクリーンアップします。機密データはセッションをまたいで保持されるためです。

    // Remove sensitive data from localStorage:
    localStorage.removeItem('sensitiveData1');
    // ...
    
    // Or if everything in localStorage is sensitive, clear it all:
    localStorage.clear();
    
    // Delete sensitive object stores in indexedDB:
    const name = 'exampleDB';
    const version = 1;
    const request = indexedDB.open(name, version);
    
    request.onsuccess = (event) => {
      const db = request.result;
      db.deleteObjectStore('sensitiveStore1');
      db.deleteObjectStore('sensitiveStore2');
    
      // ...
    
      db.close();
    }
    
    // Delete sensitive resources stored via the Cache API:
    caches.open('cacheV1').then((cache) => {
      await cache.delete("/personal/profile.png");
    
      // ...
    }
    
    // Or better yet, clear a cache bucket that contains sensitive resources:
    caches.delete('personalizedV1');
    

キャッシュをクリーンアップする方法

  • HTTP キャッシュ: センシティブ データを含むリソースに対して Cache-control: no-store を設定している限り、HTTP キャッシュはセンシティブ データを保持しません。
  • バックフォワード キャッシュ: 同様に、Cache-control: no-store に関する推奨事項と、ユーザーのログアウト時に機密性の高い Cookie(認証関連の安全な HTTPS のみの Cookie など)を消去することに関する推奨事項を遵守していれば、バックフォワード キャッシュに機密データが保持されることを心配する必要はありません。バックフォワード キャッシュ機能では、以下のシグナルの 1 つ以上が検出されると、Cache-control: no-store HTTP ヘッダーとともに配信される同一オリジン ページが強制排除されます。
    • 1 つ以上の安全な HTTPS 専用 Cookie が変更または削除されました。
    • ページで発行された XHR/fetch 呼び出しのレスポンスに、Cache-control: no-store HTTP ヘッダーが含まれていました。

どのタブでも一貫したユーザー エクスペリエンス

ユーザーがログアウトする前に、ウェブサイトで何度もタブを開いた可能性があります。それまでに、他のタブやブラウザ ウィンドウのことを忘れているかもしれません。関連するすべてのタブやウィンドウをユーザーが閉じようとすることは避けるべきです。代わりに、ユーザーのログイン状態がタブ間で一貫しているようにして、積極的に対応してください。

使用方法

タブ間でログイン状態を統一するには、pageshow/pagehide イベントと Broadcast Channel API を組み合わせて使用することを検討してください。

  • pageshow イベント: pageshow が永続的な場合、ユーザーのログイン ステータスを確認し、ユーザーがログインしなくなった場合は機密データ(またはページ全体)を消去します。pageshow イベントは、戻る/進むナビゲーションからページが初めてレンダリングされる前にトリガーされます。これにより、ログイン状態チェックによって、ページが機密でない状態にリセットされることが保証されます。

    window.addEventListener('pageshow', (event) => {
      if (event.persisted && !document.cookie.match(/my-cookie)) {
        // The user has logged out.
        // Force a reload, or otherwise clear sensitive information right away.
        body.innerHTML = '';
        location.reload();
      }
    });
    
  • Broadcast Channel API: この API を使用して、タブやウィンドウをまたいでログイン状態の変化を伝えます。ユーザーがログアウトしている場合は、機密データをすべて消去するか、機密データを含むすべてのタブとウィンドウでログアウト ページにリダイレクトします。

    // Upon logout, broadcast new login state so that other tabs can clean up too:
    const bc = new BroadcastChannel('login-state');
    bc.postMessage('logged out');
    
    // [...]
    const bc = new BroadcastChannel('login-state');
    bc.onMessage = (msgevt) => {
      if (msgevt.data === 'logged out') {
        // Clean up, reload or navigate to the sign-out page.
        // ...
      }
    }
    

おわりに

このドキュメントのガイダンスに従うことで、意図しないログアウトを防ぎ、ユーザーの個人情報を保護する、優れたログアウトのユーザー エクスペリエンスを設計できます。