こんにちは、サイオステクノロジーの佐藤 陽です。
今回は、EasyAuthで利用されるセッションについて検証した記事になります。
AppServiceとAzureActiveDirectoryB2Cを題材に、
非常に便利だけどブラックボックスなEasyAuthの中身に迫りたいと思います。
※本記事は独自の検証に基づいた考察であるため、正当性を保証するものではありません、ご了承ください。
なお、本記事は以下の記事を参考にさせていただき、検証の方を行いました。
非常に参考になりました、ありがとうございます。
Abstract
先にざっくりとした結果を書いてしまうと
以下のような全体像になっていると考察できました。
それでは細かい部分を見ていきたいと思います。
はじめに
AzureのPaaSであるApp ServiceやAzure Functionsなどには、EasyAuthとよばれる認証機能が存在します。
こちらはソースコードに修正を加える必要なく、認証機能を追加できる非常に便利な機能です。
この機能が内部的にどのように機能しているか気になったため、自分なりに検証しようと思います。
今回利用するリソースは以下2つです。
- Azure App Service
- Azure Active Directory B2C (AADB2C)
EasyAuthについて
先ほども述べましたが、EasyAuthはアプリに認証機能を加えられるAzureの機能です。
実態としてはアプリケーションと同じVMで実行されるミドルウェアです。
参照:Azure App Service および Azure Functions での認証と承認
EasyAuthの設定方法に関しては、公式のドキュメントでも細かく解説されており、今回の記事の対象外とします。
なお、本記事はAppServiceおよびAADB2Cに対し、認証の設定が完了している前提で進めていきます。
特に目立った設定などは必要ありませんが、AppServiceの認証設定においてトークンストアを有効にしておいてください。
ログイン
認証の設定が完了したら、早速アプリにアクセスします。
するとログイン画面が表示され、サインアップおよびログインが可能となります。
ログインが完了すると、デプロイされたアプリが表示されるかと思います。
認証用のソースコードを書いていないにも関わらず、このログインフローが作成できるのは便利ですね。
トークン取得
ここでブラウザから
GET https://{app-name}.azurewebsites.net/.auth/me
を実行します。
すると、IdTokenを含むjson形式のレスポンスが得られるかと思います。
IdTokenの他にも、AADB2C上のユーザーフローで設定したクレーム情報も取得されています。
この機能には、先ほど紹介したミドルウェアが一役買っています。
ログイン処理が完了すると、発行されたAccessTokenや、IDTokenがミドルウェアの持つトークンストアに保管されます。
このトークントアに対してトークン情報を要求することで、先ほどのjson形式のレスポンスが得られます。
OIDCや、OAuth2.0のフローについて学習すると、IdPのエンドポイントから直接トークンが取得できますが
EasyAuthを使った場合は、このミドルウェアを経由してトークン情報を取得するようになります。
では、このミドルウェアはどのようにしてユーザーの判別や、ログイン状態の有無を判断しているのでしょうか?
ここで登場するのが、今回のテーマでもあるセッションです。
セッション
先ほども述べた通り、トークンストアから情報を取得する際、セッションが使用されます。
セッションには複数種類があり、今回利用するのは以下2つのセッションです
- アプリケーションセッション
- AADB2Cセッション
参照:Azure Active Directory B2C でセッションの動作を構成する
ログインを完了すると、アプリとAADB2Cそれぞれに対してセッションが確立されます。
実際にセッションを確認してみたいと思います。
アプリケーションセッション
まずはアプリセッションを確認します。
ログインを完了させた状態でアプリを開きます。
この時ブラウザのDevToolを開き、アプリドメイン下にあるクッキー情報を見てみます。
するとAppServiceAuthSessionという名称のクッキーが確認できます。
公式ドキュメントに名称の記載は見当たらなかったですが、名前からしておそらくこのクッキーではないかと予想できます。
AADB2Cセッション
次にAADB2Cセッションを確認します。
AADB2Cセッションについては以下のような記載があるため、テナントのドメインにアクセスします。
Cookie は、Azure AD B2C テナントのドメイン名 (https://contoso.b2clogin.com など) の下に格納されます。
https://{tenant-name}.b2clogin.com
ページ自体は正しく表示されないかと思いますが、DevToolでクッキーの表示は出来るかと思います。
すると、以下二つのクッキーが表示されているかと思います。
- x-ms-cpim-sso:{Id}
- x-ms-cpim-csrf
こちらのページにB2Cのクッキーに関する説明があり、
x-ms-cpim-ssoは
SSO セッションを維持するために使われます。
persistent
が有効な場合、この cookie はpersistent
として設定されます。
という記載があります。
内容的にx-ms-cpim-ssoがAADB2Cセッションに対応しそうですね。
また別のドキュメントを見たときに、以下のような記載がありました。
ID トークンが有効期限切れになった場合、またはアプリ セッションが無効になった場合、Azure Web アプリは新しい認証要求を開始し、ユーザーを Azure AD B2C にリダイレクトします。 Azure AD B2C SSO セッションがアクティブである場合、Azure AD B2C は、ユーザーに再度サインインを促さずにアクセス トークンを発行します。 Azure AD B2C セッションが有効期限切れまたは無効になった場合、ユーザーは再度サインインするように促されます。
Azure AD B2C SSO セッションというのは、x-ms-cpim-ssoのクッキーを指していそうです。
また、文脈から読み取るに、このSSOセッションがAADB2Cセッションのことを指しているようです。
セッション動作確認
アプリセッションと、AADB2Cセッションがそれぞれ見つかりました。(恐らく)
セッションが二つ確立されていますが、優劣があるのでしょうか、片方しか確立されていない場合どうなるのでしょうか。
以下の内容を検証していきたいと思います
- ログインして2種類のセッションが存在している状態を基本状態とする
- どちらか、もしくは両方のセッションの破棄を行う
- .auth/meにアクセスし、トークン情報が取得できるか否かを確認する
- それぞれのセッションIDの内容を確認する
アプリケーションセッションを削除
まずはセッションIDを確認します
AppServiceAuthSession | WJ/9/2eLRdKGk1dRV0VY… |
x-ms-cpim-sso | m1.5cuNiesH/bXx3SBZ.QrZYQ5…. |
まずはアプリセッションの削除から試してみます。
ブラウザのDevToolからAppServiceAuthSessionを削除し、.auth/meを実行します。
すると、セッションを破棄したにも関わらずトークンの情報が取得できました。
次に各セッションIDを確認します。
AppServiceAuthSession | +i0yCH1L0JGCxQKx+p3v… |
x-ms-cpim-sso | m1.+7OSZaqqGySRmyYq.KRVqWm… |
アプリセッションもAADB2CセッションもセッションIDが変わっていることが分かりました。
どちらもセッションが再構築されるようです。
この挙動ですが、上でも述べた
ID トークンが有効期限切れになった場合、またはアプリ セッションが無効になった場合、Azure Web アプリは新しい認証要求を開始し、ユーザーを Azure AD B2C にリダイレクトします。 Azure AD B2C SSO セッションがアクティブである場合、Azure AD B2C は、ユーザーに再度サインインを促さずにアクセス トークンを発行します。 Azure AD B2C セッションが有効期限切れまたは無効になった場合、ユーザーは再度サインインするように促されます。
の通りですね。
アプリセッションが存在しなかったため、AADB2Cにリダイレクトされ、AADB2C SSOセッション情報を用いて再度セッションが確立されたと想定されます。
(ただ、AADB2Cセッションも再構築されたのは少し予想外でした。
裏側で再度サインインが行われているのでしょうか…、この辺りは検討課題としたいと思います。)
また、ついでに取得したトークンの中身を見てみると、先ほどとIDTokenの内容が変わっていることも分かります。
セッションが再確立された際に、IDTokenも再取得されたようです。
上の文章ではアクセストークンと書かれていますが、IDTokenも同様に発行されるようです。
AADB2C セッション動作
少し話は逸れますが、AADB2Cのセッション動作についても確認したいと思います。
AzurePortalでAADB2Cの設定画面を開き、ユーザーフローのプロパティの項目を開くと
トークンの有効期限や、セッションの動作に関する設定を行えます。
ここにセッション動作という項目があり、現在セッション時間は15分に設定してあります。
ここでいうセッションは、AADB2Cのセッションの事を指しており、15分経つとセッションが破棄されるようです。
試しに、ログイン完了後に15分経過するまで待ってみます。
その後AppServiceAuthSessionを削除したのちに、.auth/meを実行します。
すると、ログイン画面へリダイレクトされます。
先程はAADB2Cのセッションが生きていたためアプリセッションが再構築されましたが
今回はAADB2Cのセッションが時間切れとなっていたため、ログアウト状態とみなされるようです。
AADB2Cセッションを削除
次にAADB2CセッションCookieを削除した場合の動作を確認します。
ログイン後のセッションIDは以下のようになってます。
AppServiceAuthSession | qQnIoEsTJxq6ELdSR… |
x-ms-cpim-sso | m1.H+dozYS3dt/ |
ここで、x-ms-cpim-ssoのセッションを削除し、/.auth/meを実行します。
トークンが取れることが確認できます。
また、セッションを確認すると
AppServiceAuthSession | qQnIoEsTJxq6ELdSR… |
x-ms-cpim-sso | 無し |
アプリセッションの値は変化なしですね。
AADB2Cセッションは再構築されませんでした。
またIdTokenの値を見ても今回は変化がありませんでした。
やはりトークンは、アプリのミドルウェアによって管理されるトークンストアによって管理されるため
アプリセッションの有無によってトークンの内容に影響があると考えられます。
両方のセッションを削除
最後に両方のセッションを削除した場合を確認します。
AppServiceAuthSession、x-ms-cpim-ssoともに削除し/.auth/meを実行するとログイン画面へリダイレクトされました。
セッションが破棄されているので、これは予想通りとも言えます。
ログアウト
最後にログアウト処理を実施したいと思います。
これまで説明してきた通り、完全にログアウトを行うためにはアプリセッションと、AADB2Cセッション両方を破棄する必要があります。
手動でセッションCookieを削除するわけにもいかないので、アプリから削除する方法を試してみたいと思います。
こちらのAppServiceのドキュメントにも書いてあり、
/.auth/logoutにアクセスすることによってセッションからサインアウトできます。
ということで早速試してみます。
ログイン状態において、/.auth/logoutをブラウザで実行します。
そのあと/.auth/meでトークン取得すると、トークンが取得できてしまういます。
原因追及のため、セッション状態も確認してみたいと思います。
/.auth/logoutを実施しセッションを確認します。
すると、アプリセッションは存在していませんが、AADB2Cセッションが残っていることが確認できます。
これは先ほどアプリセッションを破棄した場合と同じですね。
ですので、完全にログアウトするためにはAADB2Cセッションも破棄する必要があります。
ログアウトのためのエンドポイントがAADB2Cに記載してあり、それに倣いURLを実行します
https://{tenant-name}.b2clogin.com/{tenant-name}.onmicrosoft.com/{userflow-name}/oauth2/v2.0/logout
すると、確かにx-ms-cpim-ssoのセッションは破棄されたことは確認できますが
以下のようなエラー画面が表示されました。
redirect先が見つからないという事なので、post_logout_redirect_uriのクエリを追加して実行します。
参照:Azure Active Directory B2C での OpenID Connect による Web サインイン
リダイレクト先は認証の対象となっているアプリのルートURL(エンコード済み)を指定しました。
https://{tenant-name}.b2clogin.com/{tenant-name}.onmicrosoft.com/{userflow-name}/oauth2/v2.0/logout?post_logout_redirect_uri=https%3A%2F%2Fwebapp-aksato.azurewebsites.net
これで準備は完了です。
/.auth/logoutでログアウトしたのちに
https://{tenant-name}.b2clogin.com/{tenant-name}.onmicrosoft.com/{userflow-name}/oauth2/v2.0/logout?post_logout_redirect_uri=https%3A%2F%2Fwebapp-aksato.azurewebsites.net
でAADB2Cからもログアウトし、アプリのURLへリダイレクトすることで
アプリで認証が必要となり、ログイン画面が表示されることが分かります。
もちろん/.auth/meをしてもトークン情報は取得できません。
ただ、2回URLを実行するのも手間です。
そんな時に、こちらのブログでも紹介されている方法を取ることで、1回のアクセスでアプリからもAADB2Cからもログアウトすることが可能になります。
ただ、記事が書かれた時と仕様が変更されたのか、AppServiceの認証設定画面にallowed-external-redirect-urlsの設定項目が見つかりませんでした。
<追記>
ありました!
この項目に
https://{tenant-name}.b2clogin.com/{tenant-name}.onmicrosoft.com/{userflow-name}/oauth2/v2.0/logout
を追加し、ログインした状態で
https://{app-name}.azurewebsites.net/.auth/logout?post_logout_redirect_uri={エンコード済みのAADB2CログアウトURL}
にアクセスします。
すると、一回のアクセスでログイン画面へと遷移することが確認できました。
また、アプリセッションは破棄されており、AADB2Cセッションも新しいものに更新されたことも確認できました。
これで無事ログアウト完了し、めでたし、めでたしです。
ただ、↓のCloud Shellからの設定は相変わらずBadRequset が返ってきてしまいました。
あまりCloud Shellを使い慣れていないため、もう少し色々触ってみたいと思います。
</追記>
また、この公式ドキュメントを見ると、Azure Azure Cloud Shellから実行できるとのことなので
こちらも試してみましたが、BadRequsetが返ってきてしまいました。
絶賛仕様変更中なのでしょうか…、こちらは改めて調査したいと思います。
まとめ
今回は、EasyAuthで使われるセッションに関して自分なりに調査してみました。
アプリセッションと、AADB2Cセッションの2種類が存在しており
そのセッションが確立されることによりログイン状態が保持されることが分かりました。
また、どちらかのセッションを破棄してもログイン状態自体は保持されますが
それぞれ破棄した場合で、セッション再構築の振る舞いや、取得するトークンへの影響は違うようです。
今回取り上げたセッションは2種類でしたが、FacebookアカウントやTwitterアカウントなど外部のアカウントを利用する場合は
フェデレーションIDプロバイダーセッションなるものも出るようで、こちらの振る舞いも気になるところです。
次に、ログアウトするには両方のセッションを破棄する必要があることが分かりました。
.auth/logoutのリダイレクト先に関してはまた時期を見て検証したいと思います。
なんだかすっきりしない内容のブログになってしまいましたが、引き続き検証の方行いたいと思います。
また、冒頭にも書きましたが
独自の検証に基づいた考察であるため、正当性を保証するものではありません。
誤りや、疑問点があった場合にはコメントなどでご指摘いただけると幸いです。