しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

App Service Authentication と Azure AD B2C の組み合わせでログアウトを正しく実装する

少し前に App Service Authentication が OpenID Connect に対応したので、様々な IdP を利用することが出来るようになりました。Azure AD B2C も OIDC 対応によって正式利用が可能になった IdP の一つです。

ログアウト後のリダイレクト先を指定する

App Service Authentication から使う時にはログインは当然ながら確認すると思いますが、案外確認が漏れがちなのがログアウトです。ここで単純に /.auth/logout にリダイレクトするだけでは、以下のような懐かしさすらある画面が表示されてしまいます。

普通の Web アプリケーションであれば、ログアウトした後は独自のログアウトしたことを知らせる画面や、ログイン画面にリダイレクトするのが一般的なので遷移先を post_logout_redirect_uri クエリパラメータで /.auth/logout に渡して対応します。

この時 post_logout_redirect_uri には相対パスか "Allowed external redirect URLs" に追加した外部 URL を URL エンコードして渡してあげます。この辺りのカスタマイズ方法は公式ドキュメントに記載があります。

ログアウトを行った後にログイン画面を表示するためには、認証が必須になっているパスにリダイレクトすればよいのでルート / にでも飛ばせば解決しそうです。単純に以下のような URL で実現できます。

https://***.azurewebsites.net/.auth/logout?post_logout_redirect_uri=%2f

実際に上の URL を使ってログアウトを行ってみると、見た目上はログインしたままで正しく動作しません。

これは Azure AD B2C 側のセッションが生きている場合には、ルートにリダイレクトした後にログインが行われてしまうため発生します。リダイレクトを確認すると動作がよく分かります。

ルートにリダイレクトした後に Azure AD B2C へ更にリダイレクトされて、そのままログインが完了してしまっています。よって正しくログアウトを行うには Azure AD B2C からもログアウトする必要があります。

同時に Azure AD B2C からもログアウトする

目指す動作のためには App Service Authentication からログアウトした際に Azure AD B2C からもログアウトする必要がありますが、ドキュメントにひっそりと書いてあるように Azure AD と Google アカウントの場合は、サーバーサイドでログアウトを行ってくれるようです。

  • For Azure Active Directory and Google, performs a server-side sign-out on the identity provider.
Customize sign-ins and sign-outs - Azure App Service | Microsoft Docs

残念ながら OpenID Connect を使っている場合は対応していませんし、そもそも OIDC 側のセッション管理に関する仕様がドラフトのようなので、App Service Authentication が対応するには時間がかかりそうです。*1

とはいえ既に Azure AD / Azure AD B2C には必要なエンドポイントは実装されていました。以下のドキュメントによると OIDC のメタデータに含まれている end_session_endpoint が該当するようです。

このエンドポイントは Azure AD B2C のユーザーフロー単位に提供されるメタデータにも含まれています。

Azure AD B2C のドキュメントにも end_session_endpoint に関する詳細が記載されています。パラメータがいくつか必要そうに見えますが、今回はとりあえず post_logout_redirect_uri のみ指定すれば良いです。

ここで重要になるのは App Service Authentication のログアウトを先に行ってから、Azure AD B2C のログアウトを行うという点です。このフローを実現するためには post_logout_redirect_uri でのリダイレクトを組み合わせる必要があります。

リダイレクトが多いので複雑ですが、分解すると以下のようなログアウト開始 URL を作成します。

# 1. App Service Authentication のログアウトを行った後に任意の URL にリダイレクトする URL
https://***.azurewebsites.net/.auth/logout?post_logout_redirect_uri=...

# 2. Azure AD B2C のログアウトを行った後に App Service のルートにリダイレクトする URL
https://***.b2clogin.com/***.onmicrosoft.com/oauth2/v2.0/logout?p=b2c_1_signupsignin&post_logout_redirect_uri=https%3a%2f%2f***.azurewebsites.net

# 3. 1 と 2 を組み合わせて App Service Authentication => Azure AD B2C => App Service を実現
https://***.azurewebsites.net/.auth/logout?post_logout_redirect_uri=https%3a%2f%2f***.b2clogin.com%2f***.onmicrosoft.com%2foauth2%2fv2.0%2flogout%3fp%3db2c_1_signupsignin%26post_logout_redirect_uri%3dhttps%253a%252f%252f***.azurewebsites.net

App Service Authentication のログアウト後にリダイレクトする URL は、オープンリダイレクト防止のために明示的に許可する必要があります。

これは Azure AD B2C の end_session_endpoint を Azure Portal から設定しておけば良いです。クエリパラメータを省いた部分のみで問題ないようです。

設定後に作成したログアウト開始 URL を実行すると、App Service Authentication のログアウトが行われた後に Azure AD B2C のログアウトも実行され、最後に App Service に戻ってくることが確認できます。

App Service 自体に認証がかかっているので、更にログイン画面へリダイレクトされています。

これで App Service Authentication と Azure AD B2C のログアウトを同時に行えるようになりました。必要な URL は少し複雑ですが、コードから作成するのは非常に簡単なはずです。

一応は目標とした動作は実現できるようになりました。最後に Azure AD B2C 側に残ったオープンリダイレクトの問題を修正して終わりにします。

Azure AD B2C ログアウト時のリダイレクト先を制限する

数回試した範囲では Azure AD B2C のログアウトエンドポイントに指定する post_logout_redirect_uri は、デフォルトでは指定されたリダイレクト先の検証を行わず、何でも受け付けてしまうようです。

このままの挙動ではオープンリダイレクトの問題が残ってしまっているので対応します。以下のドキュメントの最後に書いてある部分がそれに該当します。

要するに id_token_hint パラメータにログアウトするユーザーが保持していた id_token を渡すことで、発行したアプリケーションのリダイレクト許可リストを使って検証を行うという仕組みです。

ログアウト時に id_token を必須にするオプションはユーザーフローの設定内にあります。

この設定を有効にして id_token を必須にすると、これまで動作していたログアウト URL は以下のようなエラーが出るため動かなくなります。エラーメッセージにもあるように id_token_hint が必要になります。

パラメータとして id_token_hint を追加すればこのエラーは消えますが、代わりにリダイレクト URL が許可されていないというエラーになるので、最終的にリダイレクトさせたい URL を追加しておきます。

設定場所は見て分かるように、ログイン時のコールバック URL を指定する場所と同じです。

これで一通りの設定が完了したので、最後に一連のログアウトに必要な URL の生成を行う簡単なサンプルコードを紹介しておきます。

App Service Authentication は Token Store を有効化すると id_token/.auth/me から取得できるので、それを使って一連のログアウト処理に必要なデータを用意します。

// App Service Authentication / Azure AD B2C それぞれのログアウトエンドポイント
const endSessionEndpoint = "https://***.b2clogin.com/***.onmicrosoft.com/oauth2/v2.0/logout?p=b2c_1_signupsignin";
const logoutEndpoint = window.location.origin + "/.auth/logout";

// ログイン中ユーザーの id_token を取得する(失敗したらログアウト済み)
const response = await fetch("/.auth/me");
const userInfo = await response.json();

// リダイレクト先の URL を組み立てていく
const postLogoutRedirectUri = window.location.origin
const startLogoutAzureADB2CUri = endSessionEndpoint + "&post_logout_redirect_uri=" + encodeURIComponent(postLogoutRedirectUri) + "&id_token_hint=" + userInfo[0].id_token;
const startLogoutUri = logoutEndpoint + "?post_logout_redirect_uri=" + encodeURIComponent(startLogoutAzureADB2CUri);

window.location = startLogoutUri;

コード自体はリダイレクト先を組み立てては順次 post_logout_redirect_uri に URL エンコードして設定していくだけです。今回は id_token_hint が付いている点だけが異なっています。

スクリーンショットは出しませんが、このコードをログイン中の App Service 内で実行すると App Service Authentication と Azure AD B2C の両方に対して、安全な形でログアウトが行われます。

ちなみに MSAL.js v2 を使って直接 Azure AD B2C を利用している場合でも、ログアウト時に id_token を指定すれば同じ挙動になるようです。是非とも App Service でも公式対応を期待したいです。