前回はOWIN Security MiddlewareのOpenID Connectを使ってAzure Active Directory(AAD)でのログオンを行いました。
今回も基本的にやることは同じなのですが、OWINのOpenID Connectのresponse_modeの初期値がform_postなので、一般的なOpenID ConnectをサポートしたIdentity Providerへの流用の可能性を考えるために、Implicitフローで使われるfragmentを使ってid_tokenを受け渡す場合の工夫について試してみます。
※尚、現状AADではImplicitフローをサポートしているという表明があるわけではないので、今回紹介するのはあくまでImplicitもどきです。
■処理の流れとAADの場合
まず、OpenID ConnectにおけるImplicit Clientではどのような流れでid_tokenが発行されるのか?、AADではどうなるのか?についてみておきたいと思います。
参考)OpenID Connect Implicit Client Implementer's Guide 1.0 - draft 15の日本語訳
http://openid-foundation-japan.github.io/openid-connect-implicit-1_0.ja.html
大まかな流れは、以下のようになっています。
①Client は必要なリクエストパラメータを含んだ Authentication Request を構築する.
②Client は Authorization Server にリクエストを送信する.
③Authorization Server は End-User を認証する.
④Authorization Server は End-User の Consent/Authorization を取得する.
⑤Authorization Server は End-User を ID Token, およびもし要求されていれば Access Token とともに, Client に戻す.
⑥Client はそれらのトークンを検証し, End-User の Subject Identifier を取得する.
具体的には以下のような通信が行われます。
①~②クライアントからのAuthorizationリクエスト
https://server.example.com/authorize? response_type=id_token%20token &client_id=s6BhdRkqt3 &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb &scope=openid%20profile &state=af0ifjsldkj &nonce=n-0S6_WzA2Mj
response_typeにid_tokenおよびtokenを付けて要求を投げる必要がある(REQUIRED)、というのが仕様です。
しかし、現状でAADはresponse_typeにtokenをサポートしていないので、id_tokenだけを要求することにします。また、前述の通り、AADではresponse_modeの初期値がform_postなので、Implicit(もどき)の場合はresponse_mode=fragmentをつける必要があります。
結果、以下のようなリクエストになります。
https://login.windows.net/{tenantid}/oauth2/authorize? response_type=id_token &response_mode=fragment &client_id=xxxxx &redirect_uri=https%3a%2f%2flocalhost%3a44307%2fAccount%2fFragment%2f &scope=openid+profile &state=yyyyy &nonce=zzzzz
③~④End-Userの認証、同意取得
この部分は仕様のスコープ外なので手段は問いません。
AADではws-federationを使ってhttps://login.microsoftonline.comへ認証要求を投げ、ユーザ認証を行います。
https://login.microsoftonline.com/login.srf? wa=wsignin1.0 &wtrealm=https%3a%2f%2flogin.windows.net%2f &wreply=https%3a%2f%2flogin.windows.net%2f{tenantid}%2fwsfederation &wctx=xxxx &wp=MBI_FED_SSL &id=
認証が成功するとhttps://login.windows.net/{tenantid}/wsfederationに対してSAMLトークンがPOSTされ、認証結果およびユーザ情報が渡されます。
⑤id_tokenをクライアントへ返す
HTTP302でredirect_uriへ戻します。その際、フラグメント(#)にid_tokenなど要求されたトークンを付加します。
HTTP/1.1 302 Found Location: https://client.example.org/cb# access_token=SlAV32hkKG &token_type=bearer &id_token=eyJ0 ... NiJ9.eyJ1c ... I6IjIifX0.DeWt4Qu ... ZXso &expires_in=3600 &state=af0ifjsldkj
AADの場合は、先ほどのリクエストでtokenを要求できませんでしたので、id_tokenのみが返ってきます。
HTTP/1.1 302 Found Location: https://localhost:44307/Account/Fragment/# id_token=eyJ0.... &state=yyyyy &session_state=zzzzz
⑥返ってきたid_tokenを検証する
id_tokenがフラグメントで返ってきますので、UserAgent(ブラウザ等)上でパラメータを解析してクライアントへ送ってあげる必要があります。通常はJavaScriptでlocation.hashを解析してクライアントへid_tokenなどをPOSTしてあげます。
今回もredirect_uriに指定したページ(https://localhost:44307/Account/Fragment/)へのGETリクエストすると以下のようなJavaScriptを含むHTMLを返すようにしています。(テストなのでベタ書きしています)
<html> <head> <meta name="viewport" content="width=device-width" /> <title></title> </head> <body> <form method="post" name="autopost" action="https://localhost:44307/"> <script type="text/javascript"> var params = location.hash.substring(1).split('&'); var id_token = params[0].substring(9); var state = params[1].substring(6); var session_state = params[2].substring(14); document.write("<input type=hidden name=id_token value="); document.write(id_token); document.write(" />"); document.write("<input type=hidden name=state value="); document.write(state); document.write(" />"); document.write("<input type=hidden name=session_state value="); document.write(session_state); document.write(" />"); </script> <input type="submit" value="Submit" /> </form> <script language="javascript">window.setTimeout('document.forms[0].submit()', 0);</script> </body> </html>
結果、フラグメントでわたってきたid_tokenがOWINのOpenID Connect Middlewareが動いているASP.NETアプリケーション(https://localhost:44307)へPOSTされ、自動的にトークン検証~ユーザ情報の取り出しを行ってくれます。
ここまでのフローをざっとシーケンスにしたものが以下の図です。
■ASP.NET/OWINでの実装
デフォルトのASP.NET/OWIN Middlewareではフラグメントでわたってきたid_tokenをUserAgent側で解析してPOSTする部分が用意されていないので、その部分だけは作ってあげる必要があります。
具体的にはViewを一つ用意して、そのエンドポイントに対するGETがあった場合に先のJavaScriptを含むHTMLを返すようにします。
以下の手順で実装しました。
ソリューション・エクスプローラからAccount Controllerへ新規MVC5 Viewを追加します。先のシーケンスの中にも出てきたように、今回はエンドポイントの名前を「Fragment」としています。
新規作成されたfragment.chtmlに先のJavaScriptを含むHTMLを記載します。
次に、Account Controllerにアクションを定義します。AccountController.csに以下のコードを追記します。
[AllowAnonymous] public ActionResult Fragment() { return View(); }
また、OWINの認証設定部分(Startup.Auth.cs)に以下の設定を入れます。
・response_type:id_token
・response_mode:fragmentを指定
・redirect_uri:先に追加したView(Fragment)のエンドポイントを指定
app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions() { Client_Id = "xxxxx", Authority = "https://login.windows.net/{tenant}.onmicrosoft.com", Response_Type = "id_token", Response_Mode = "fragment", Redirect_Uri = "https://localhost:44307/Account/Fragment/", Description = new Microsoft.Owin.Security.AuthenticationDescription() { Caption = "Azure Active Directory", AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType } });
これで実行すると前回と同じような動きに見えますが、裏側ではImplicit(もどき)でid_tokenが受け渡されます。