2014年5月21日水曜日

[AAD/ASP.NET] (続)OpenID Connectを使ってAADでログオンする~response_mode=fragment編

先日のポストの続きです。

前回は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が受け渡されます。

0 件のコメント: