メインコンテンツへスキップ
Auth0 は、Enterprise プランを契約しているすべてのテナントで OpenID Connect Back-Channel Logout 1.0 specification をサポートしています。 この仕様では、 に含まれるセッション ID (sid) と ログアウトトークン を使用して、バックチャネル通信によるセッション終了を連携させます。異なるセッション ID は、テナント内のユーザーエージェントまたはデバイスごとの個別のセッションを表します。ログアウトトークン は、ログアウト対象のエンドユーザーとセッションを識別します。

バックチャネル通信

Back-Channel Logout を使用するには、アプリケーションで、テナントサーバーからアクセス可能な Back-Channel Logout URI を公開する必要があります。この URI は、アプリケーションが ログアウトトークン を含むリクエストを受信するエンドポイントです。アプリケーションがこのリクエストを受信した場合は、トークン内のクレームに一致するローカルセッション状態をクリアする必要があります。
バックチャネルログアウトを機能させるには、アプリケーションがバックチャネル通信を受信できる必要があります。

OIDC バックチャネルログアウト通信におけるトークン

バックチャネル経由で通信が行われる場合、アプリケーションはどのセッションを終了すべきかを判断するためにに依存することはできません。代わりに、このサービスは IDトークンとログアウトトークンに含まれる共有セッション識別子 (sid) を使用します。 エンドユーザーがログイン時に Auth0 で正常に認証されると、がアクセストークンとIDトークンを発行します。ログアウトトークンは、ログアウト操作やセッションの失効などでセッションが破棄されたときに生成されます。IDトークンとログアウトトークンの両方には、アプリケーションが Back-Channel Logout ワークフローを実行するために必要なクレームが含まれます。クレームの詳細については、JSON Web Token Claims を参照してください。
バックチャネルログアウトのワークフロー
  1. ログイン - ユーザー認証中に、Auth0 テナントは IDトークンに sid を追加します。
  2. ログイン - アプリケーションは受け取ったセッション識別子を独自のセッションストアに保存し、アプリケーション固有のセッションに関連付けます。
  3. ログアウト - IdP が事前登録されたログアウトコールバック URL を呼び出し、このエンドポイントにログアウトトークンを POST します。このトークンには、他のパラメーターとともに user_id (sub) と sid が含まれます。
  4. ログアウト - アプリケーションのバックエンドは、OIDC 仕様に従ってログアウトトークンを検証し、sid を抽出する必要があります。その後、バックエンドはこのトークンを使用して、その識別子に関連付けられたセッションを特定し、必要に応じて終了できます。
ログアウトが成功した場合、想定されるレスポンスは HTTP 200 です。誤ったリクエストや不正な形式のリクエストを示す HTTP 400 を受け取った場合は、トラブルシューティングのヒントを利用できます。詳細については、Configure Back-Channel Logout を参照してください。

仕組み

このサンプルのユースケースでは、複数のアプリケーションで Back-Channel Logout がどのように機能するかを示します。
複数アプリケーションでの Back-channel logout のユースケース
  1. アプリケーションの設定時に、Application A は Back-Channel Logout URI を Auth0 に登録します。
  2. アプリケーションの設定時に、Application B は Back-Channel Logout URI を Auth0 に登録します。
    OIDC Back-Channel Logout URL は、次の要件を満たす必要があります。
  3. エンドユーザーのログイン時に、ユーザーは Application A にアクセスするために Auth0 で認証されます。
  4. Auth0 は sid を含む IDトークンを Application A に送信します。詳しくは、ID Token Structure を参照してください。
  5. ユーザーは Application B にアクセスするために Auth0 で認証されます。
  6. Auth0 は同じ sid を含む IDトークンを Application B に送信します。アプリケーションはセッション情報を保存する必要があります。
  7. ログアウト時に、Application A または他のエンティティがフロントチャネルでログアウトを開始します。
  8. Auth0 はセッションクッキーを介して Auth0 のセッションレイヤーを終了します。
  9. Auth0 は Application A の Back-Channel Logout URI を呼び出し、ログアウトトークン を POST します。
  10. Application A は ログアウトトークン を検証し、セッションを終了します。
  11. Auth0 は Application B の Back-Channel Logout URI を呼び出し、ログアウトトークン を POST します。
  12. Application B は ログアウトトークン を検証し、セッションを終了します。
Back-channel logout リクエストは非同期キューに格納され、できるだけ速やかに処理されます。高負荷時には、ログアウトリクエストが実行されるまでにわずかな遅延が発生する可能性があるため、この結果整合性を考慮してアプリケーションを設計してください。

サンプルトークン

Auth0 で ログアウトトークン として使用するには、アプリケーションが を解析して検証できる必要があります。詳細については、JSON Web Tokens を検証する を参照してください。 アプリケーションがトークンを検証してデコードすると、その内容は次の例のようになります。
JSON
{
  "iss": "https://artex-dev.eu.auth0.com/",
  "sub": "auth0|602e93db83fa6f00749a23e6",
  "aud": "TuhNLv7ulXD3RfyLlSMbOvszzwJJFPpO",
  "iat": 1698160928,
  "exp": 1698161048,
  "jti": "44a91215-dfb4-4dfe-a1eb-fcafa911deba",
  "events": {
    "http://schemas.openid.net/event/backchannel-logout": {}
  },
  "trace_id": "81b336a94a4a5707",
  "sid": "375UIp_ID5mCTClIeBEHpXfGwq51tF_L"
}

Auth0 SDKs

完全なサンプルと本番環境向けのコードは、express-openid-connect SDKBack-Channel Logout Example セクションにすでに含まれています。

実装例

セッションストレージ

このセッションストレージの例は Node (Express) で構築されており、Express OpenID Connect Web App Sample をベースにしています。 アプリケーションの Sessions タブで、ログアウトトークン を受信するよう設定したルートを公開します。トークンを検証し、ユーザーのセッションを終了します。
この例では、デモ用にインメモリのセッションストアを使用しています。
routes/index.js
const express = require('express');
const router = express.Router();
const { requiresAuth } = require('express-openid-connect');

// ログアウトトークンを検証するミドルウェア
const requiresValidLogoutToken = require('../middlewares/validateLogoutToken');

// ユーザーセッションを削除するヘルパー関数
const deleteUserSessions = require('../utils/sessions');

// バックチャネルログアウトトークンを受信する新しいルート
// Auth0 管理ダッシュボードの Application -> Sessions タブで
// 設定する必要があります
router.post(
  '/backchannel-logout',
  requiresValidLogoutToken,
  function (req, res, next) {
    // この時点でログアウトトークンは有効です(requiresValidLogoutToken ミドルウェアによって検証済み)
    // リクエストオブジェクトからアクセスできます: req.logoutToken

    // ユーザーセッションを削除してログアウトさせる
    deleteUserSessions(
      req.app.locals.sessionStore,
      req.logoutToken.sub,
      req.logoutToken.sid
    );

    res.sendStatus(200);
  }
);

router.get('/', function (req, res, next) {
  res.render('index', {
    title: 'Auth0 Webapp sample Nodejs',
    isAuthenticated: req.oidc.isAuthenticated(),
    headline: process.env.APP_NAME,
    backgroundColor: process.env.BACKGROUND_COLOR,
    baseURL: process.env.BASE_URL,
  });
});

router.get('/profile', requiresAuth(), function (req, res, next) {
  res.render('profile', {
    userProfile: JSON.stringify(req.oidc.user, null, 2),
    title: 'Profile page',
    headline: process.env.APP_NAME,
    backgroundColor: process.env.BACKGROUND_COLOR,
    baseURL: process.env.BASE_URL,
  });
});

module.exports = router;
middlewares/validateLogoutToken.js
// このミドルウェアは、以下で定義されているログアウトトークンを検証します:
// https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation

const jose = require('jose');

async function requiresValidLogoutToken(req, res, next) {

  // トークン検証用のリモートキーセットを取得する
  const JWKS = jose.createRemoteJWKSet(
    new URL(process.env.ISSUER_BASE_URL + '/.well-known/jwks.json')
  );

  const logoutToken = req.body.logout_token;

  if (!logoutToken) {
    res.status(400).send('Need logout token');
  }

  try {
    const { payload, protectedHeader } = await jose.jwtVerify(
      logoutToken,
      JWKS,
      {
        issuer: process.env.ISSUER_BASE_URL + '/',
        audience: process.env.CLIENT_ID,
        typ: 'JWT',
        maxTokenAge: '2 minutes',
      }
    );

    // ログアウトトークンに sub クレーム、sid クレーム、またはその両方が含まれていることを確認する
    if (!payload.sub && !payload.sid) {
      res
        .status(400)
        .send(
          'Error: Logout token must contain either sub claim or sid claim, or both'
        );
    }

    // ログアウトトークンに events クレームが含まれていることを確認する
    // その値は、メンバー名 http://schemas.openid.net/event/backchannel-logout を含む JSON オブジェクトであること
    if (!payload.events['http://schemas.openid.net/event/backchannel-logout']) {
      res
        .status(400)
        .send(
          'Error: Logout token must contain events claim with correct schema'
        );
    }

    // ログアウトトークンに nonce クレームが含まれていないことを確認する
    if (payload.nonce) {
      res
        .status(400)
        .send('Error: Logout token must not contain a nonce claim');
    }

    // 有効なログアウトトークンをリクエストオブジェクトに添付する
    req.logoutToken = payload;

    // トークンが有効な場合、次のミドルウェアを呼び出す
    next();
  } catch (error) {
    res.status(400).send(`Error:  ${error.message}`);
  }
}

module.exports = requiresValidLogoutToken;

ログアウトトークンストア

トークンの保存方法として一般的なのは、セッションストアモデルの代替としてログアウトストアを定義するアプローチです。アプリケーションは、永続化レイヤーに ログアウトトークン のコレクションを保持します。 アプリケーションが認証状態を確認する必要があるたびに、ログアウトトークン ストアを参照して、そのセッションがまだアクティブかどうかを確認します。ログアウトストアは、必要な情報だけを残すために、古くなった情報を定期的に削除します。
Logout Token Store

セキュリティに関する考慮事項

Back-Channel ログアウトトークン はインターネット経由で配信されるため、それらを受信するコールバックエンドポイントは、信頼性と安全性の高い運用を確保するためのベストプラクティスに従う必要があります。以下の推奨事項は網羅的なものではなく、実際のデプロイや運用状況に応じて適切に判断し、対応する必要があります。以下の一覧では、Back-Channel ログアウトトークン を処理するアプリを「アプリ」と呼びます。
  • アプリは、後で Back-Channel ログアウトトークン を受信した際に参照できるよう、ユーザーのログイン時に受信したセッション ID (sid クレーム) を保存できなければなりません。
  • アプリは、受信したすべてのトークンを JWT validation best practices に従って検証しなければなりません。
  • アプリは、信頼できるテナントによって発行されたトークンのみを受け入れなければなりません。悪意のある第三者が、他の Auth0 テナントによって発行されたトークンを送信しようとする可能性があるため、そのような試みは拒否しなければなりません。
  • アプリは、アプリが認識できる sid 値 (セッション ID) を含むトークンのみを受け入れなければなりません。無効なセッション ID (期限切れまたは未認識) を含むトークンは拒否しなければなりません。
  • アプリは、コールバックエンドポイントを TLS 経由でのみ公開しなければなりません。暗号化されていない通信チャネルは許可されません。
  • アプリは、公開されている outbound IP addresses の一覧に含まれる送信元からのリクエストのみを受け入れることが推奨されます。
  • アプリは、監視、ログ記録、レート制限に関する一般的なベストプラクティスに従うことが推奨されます。ただし、これらの詳細はこのドキュメントの対象外です。
  • アプリは、古くなった、または期限切れのセッションを定期的にクリーンアップすることが推奨されます。
  • エンドポイントアドレスに変更があった場合は、ログアウトトークン が常に正しい Back-Channel Logout Callback URL に配信されるよう、テナント設定と同期しなければなりません。