Azureの小ネタ (改)

~Azureネタを中心に、色々とその他の技術的なことなどを~

Easy Auth からグループのクレームを取得する

なんとなく続きます。

/.auth/me を呼び出すと自身のクレームが取得できることは前回に言及したと思いますが、この中には自身が属するグループの情報(クレーム)は含まれていません。Easy Authに限った話ではないのですが。

グループのクレームを取得する

Easy Authで設定すると、AADにアプリケーションが登録されまるので、このマニュフェストファイルを修正する必要があります。

マニュフェストを編集すると、groupMembershipClaims という項目がありますが、既定ではnullになっています。

f:id:StateMachine:20180712203113p:plain:w400

取り得る値としては、null/All/SecurityGroup なのですが、詳細は Azure AD でサポートされているさまざまなトークンとクレームの種類について | Microsoft Docs を参照してください。

引用すると以下の通りです。

アプリケーション マニフェストの ”groupMembershipClaims” プロパティを介して構成されます。 値が null の場合はすべてのグループが除外され、値が ”SecurityGroup” の場合は Active Directory セキュリティ グループのメンバーシップのみが含まれ、値が ”All” の場合はセキュリティ グループと Office 365 配布リストの両方が含まれます。

再度サインイン後に、/.auth/me をアクセスするとグループのクレームが取得できます。

{
typ: "groups",
val: "912a6e53-2690-4040-881e-1bfc1ccbb5b4"
},

がGUIDですので、グループ名を取得するにはGraph APIをたたく必要があります。Portalを見れば、なんのグループかはわかりますが。

f:id:StateMachine:20180712204752p:plain:w400

Easy Auth の Tokenで Graph APIを呼び出す

前回の記事 で Azure Web Apps の Easy Auth 情報をASP.NET Coreで受け取る - Azureの小ネタ (改) の続き的な記事で、Easy Authで受け取ったTokenでGraph APIをたたきたい場合です。

Easy Authで認証すると、X-MS-TOKEN-AAD-ACCESS-TOKEN にToken情報が入ってきますが、このTokenを使ってGraph APIは叩けません。

f:id:StateMachine:20180703122327p:plain

Graph APIを叩けるTokenを得るには、以下の操作をする必要があります。

準備

1) リソースエクスプローラ https://resources.azure.com/ を開く

2) 以下までTreeを展開する

3) additionalLoginParams を探して、以下のように定義し、POST(保存)します。

{
    "additionalLoginParams": ["response_type=code id_token", "resource=https://graph.windows.net"],
}

再度、アクセスすると、X-MS-TOKEN-AAD-ACCESS-TOKEN の値がJWTぽく変わっていることがわかります。

f:id:StateMachine:20180703124528p:plain

https://jtw.ms/ あたりでデコードしてみると、audience に Graph API のURLが確認できます。

f:id:StateMachine:20180703124659p:plain

Graph API を graph.microsoft.com にすることもできます。ただし、同時に複数のAudienceを指定することはできません。

アクセス例

以下は、↑と違って、graph.microsoft.com にアクセスする例です。Graph API SDKをNugetでインストールしておいてください。

        public async Task<IActionResult> GetMeAsync()
        {
            if (!this.HttpContext.Request.Headers.ContainsKey("X-MS-TOKEN-AAD-ACCESS-TOKEN"))
            {
                return Ok();
            }

            var graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(requestMessage => {
                requestMessage
                    .Headers
                    .Authorization = new AuthenticationHeaderValue("bearer", this.HttpContext.Request.Headers["X-MS-TOKEN-AAD-ACCESS-TOKEN"]);

                return Task.FromResult(0);
            }));

            var user = await graphServiceClient.Me.Request().GetAsync();
            return new JsonResult(user);
        }

ブラウザで直接APIをたたくと、User情報のJSONが取得できると思います。

以上

Azure Web Apps の Easy Auth 情報をASP.NET Coreで受け取る

Azure App Service の Web AppsにはAzure ADで簡単に認証を設定できたりします(Easy Authというらしい)。

Azure App Service での認証と承認 | Microsoft Docs

これを使うとアプリ側に認証コード書かなくて済むのと、ASP.NET (Coreじゃない) の場合は、

すべての言語フレームワークで、App Service はユーザーの要求を要求ヘッダーに挿入することによってコードで要求を使用できるようにします。 ASP.NET 4.6 アプリの場合、App Service は認証されたユーザーの要求で ClaimsPrincipal.Current を設定するので、標準の .NET コード パターン ([Authorize] 属性など) に従うことができます。

フレームワーク側でPrincipalに適宜情報を設定してくれたりしますが、ASP.NET Core 2.xではサポートされていません。どうすればいいのかググってたらいくつか方法があったので、備忘録をブログっておきます。

stackoverflow の以下の記事。

stackoverflow.com

抜粋して拡張メソッド化すると以下のような感じです。app.UseEasyAuth() とかすればOKです。

パイプライン途中でヘッダ情報からGenericPrincipalを生成して、Context.Userに設定する方法です。

   public static class ApplicationBuilderExtension
    {
        public static void UseEasyAuth(this IApplicationBuilder app)
        {
            app.Use(async (context, next) =>
            {
                // Create a user on current thread from provided header
                if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
                {
                    // Read headers from Azure
                    var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
                    var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];

                    // Create claims id
                    var claims = new Claim[]
                    {
                        new System.Security.Claims.Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", azureAppServicePrincipalIdHeader),
                        new System.Security.Claims.Claim("name", azureAppServicePrincipalNameHeader)
                    };

                    // Set user in current context as claims principal
                    var identity = new GenericIdentity(azureAppServicePrincipalNameHeader);
                    identity.AddClaims(claims);

                    // Set current thread user to identity
                    context.User = new GenericPrincipal(identity, null);
                }
                await next.Invoke();
            });
        }
    }

もう1つは、こちらの記事です。

stackoverflow.com

こちらも抜粋すると以下の通り。上との違いは、/.auth/me にリクエストなげて自身の情報をゲットしてクレーム作っているとこでしょうか。

アプリケーションでは、/.auth/me を呼び出して認証されたユーザーの追加の詳細を取得することもできます。

 public static void UseEasyAuth2(this IApplicationBuilder app)
        {
            app.Use(async (context, next) =>
            {
                // Create a user on current thread from provided header
                if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
                {
                    // Read headers from Azure
                    var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
                    var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];

                    #region extract claims via call /.auth/me

                    //invoke /.auth/me
                    var cookieContainer = new CookieContainer();
                    HttpClientHandler handler = new HttpClientHandler()
                    {
                        CookieContainer = cookieContainer
                    };
                    string uriString = $"{context.Request.Scheme}://{context.Request.Host}";
                    foreach (var c in context.Request.Cookies)
                    {
                        cookieContainer.Add(new Uri(uriString), new Cookie(c.Key, c.Value));
                    }

                    string jsonResult;
                    using (HttpClient client = new HttpClient(handler))
                    {
                        var res = await client.GetAsync($"{uriString}/.auth/me");
                        jsonResult = await res.Content.ReadAsStringAsync();
                    }

                    //parse json
                    var obj = JArray.Parse(jsonResult);

                    string user_id = obj[0]["user_id"].Value<string>(); //user_id

                    // Create claims id
                    List<Claim> claims = new List<Claim>();
                    foreach (var claim in obj[0]["user_claims"])
                    {
                        claims.Add(new Claim(claim["typ"].ToString(), claim["val"].ToString()));
                    }

                    // Set user in current context as claims principal
                    var identity = new GenericIdentity(azureAppServicePrincipalIdHeader);
                    identity.AddClaims(claims);

                    #endregion

                    // Set current thread user to identity
                    context.User = new GenericPrincipal(identity, null);
                }

                await next.Invoke();
            });

リクエストごとに呼ばれるよりはキャッシュしたりしたほうが実用的でしょう。どちらの方法を使うかはケースバイケースで。