Microsoft Graph を使ってみよう : Azure AD v1 エンドポイントでの認証を Web API と ADAL で行う

WebAPI はバックエンドとして利用されるため、ユーザーのサインイン画面を Web API として出す機会がありません。そのため以下のフローを取ります。

- 独自 WebAPI 自体を Azure AD でセキュアにする
- クライアントが WebAPI を利用する際に Azure AD にて認証し、トークンを Web API に渡す
- WebAPI が受け取ったユーザー情報を元に、代理でアクセストークンを取得する

今回は以下のアプリケーションを作ります。

- C# ASP.NET WebAPI 
- C# コンソールアプリケーションクライアント
- 代理認証シナリオ
- ADAL を利用

# アプリケーションの開発

## Web API の開発
OAuth 2.0 を使うには、まずアプリケーションの登録が必要ですが、Visual Studio の機能を使うと登録も自動で行えます。今回はこの機能を使ってアプリケーションを開発しながら、登録も行います。

1\. Visual Studio より新しい ASP.NET Web アプリケーションを作成。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f8ff3706-aa05-a252-d80a-163f04a104d8.png)

2\. 「Web API」を選択し、「認証の変更」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/2a106f74-13bf-6e3b-c155-6b7b07d44584.png)

3\. 「職場または学校アカウント」を選択し、アプリケーションを登録するドメインを指定し、「OK」をクリック。サインインを求められたら必要に応じて管理者権限でサインイン。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a95aba42-d319-a6ae-d717-8aec051afdf2.png)

4\. [Azure ポータル](https://portal.azure.com) に接続し、「Azure Active Directory」 より「アプリの登録」を選択。プロジェクト名と同じアプリケーションが作成されていることを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/8393078d-5d6e-5920-559b-b3ea48481fe7.png)

5\. アプリを選択して「設定」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/ed363937-bdfe-31da-6b1b-085f0bc19cff.png)

6\.「必要なアクセス許可」をクリックし、「追加」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/4c755e3a-67b6-0232-4c21-9447d1cd6dae.png)

7\. 「API を選択します」をクリックして、「Microsoft Graph」をクリック。「選択」ボタンをクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/e6e1af37-9f4b-dd32-37bd-77e1eba3dc0e.png)

8\. 「アクセス許可を選択します」をクリックし、必要な権限を追加。ここでは Read and write user and shared calendars を選択し、最後に「完了」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/00437263-207a-ebdf-6997-22af80b62bc7.png)

9\. 今回はユーザー委任の権限を追加しているが、WebAPI で画面がないため、管理者として許可が必要。「アクセス許可の付与」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a2fbb2f1-3031-4b66-f78d-d333252957b3.png)

10\. 次に、応答 URL をクリックし、アドレスを https から http に変更して保存。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/05ea0690-1aa0-f190-ea52-31f4e89a6ee9.png)

11\. Visual Studio に戻り、プロジェクトのプロパティより Web を確認。既定で http:// が指定されているため、http に変更。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/6e6a6bd5-89a3-a013-720b-0ed198e1f1f9.png)

12\. NuGet の管理より「Microsoft.IdentityModel.Clients.ActiveDirectory (ADAL)」を追加。また 「System.IdentityModel」を参照より追加。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/bbba68f7-74ca-bc23-1aa4-08dc2d9273fb.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/1acc7795-9911-9829-ca0a-12ea4227a0be.png)

13\. ユーザーが WebAPI にログイン時、その情報を保存するよう Startup.Auth.cs を書き換え。

```csharp:Startup.Auth.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IdentityModel.Tokens;
using System.Linq;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.ActiveDirectory;
using Owin;

namespace GraphWebAPIDemo
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            app.UseWindowsAzureActiveDirectoryBearerAuthentication(
                new WindowsAzureActiveDirectoryBearerAuthenticationOptions
                {
                    Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
                    TokenValidationParameters = new TokenValidationParameters
                    {
                         ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
                         SaveSigninToken = true
                    }
                });
        }
    }
}
```

14\. ValuesController.cs で認証およびアクセストークンを取得できるようコードを差し替え。

```csharp:ValuesController.cs
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web.Http;

namespace GraphWebAPIDemo.Controllers
{
    [Authorize]
    public class ValuesController : ApiController
    {
        private static string aadInstance = "https://login.windows.net/{0}";
        private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
        private static string clientId = ConfigurationManager.AppSettings["ida:ClientID"];
        private static string appKey = ConfigurationManager.AppSettings["ida:Password"];
        private static string graphUri = "https://graph.microsoft.com";

        // GET api/values
        public async Task<IEnumerable<string>> Get()
        {
            string[] addinScopes = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value.Split(' ');
            if (addinScopes.Contains("user_impersonation"))
            {
                ClientCredential clientCred = new ClientCredential(clientId, appKey);
                var bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext
                    as System.IdentityModel.Tokens.BootstrapContext;
                string userAccessToken = bootstrapContext.Token;
                UserAssertion userAssertion = new UserAssertion(userAccessToken);

                string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
                AuthenticationContext authContext = new AuthenticationContext(authority);
                var authResult = await authContext.AcquireTokenAsync(graphUri, clientCred, userAssertion);

                using (HttpClient client = new HttpClient())
                {
                    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
                    var responseMessage = await client.GetAsync("https://graph.microsoft.com/v1.0/me");
                    JToken me = JToken.Parse(await responseMessage.Content.ReadAsStringAsync());

                    List<string> results = new List<string>();

                    foreach (JProperty property in me.Children())
                    {
                        results.Add(property.Value.ToString());
                    }

                    return results;
                }
            }
            return new string[] { "失敗", "認証情報取得失敗" };
        }


        // GET api/values/5
        public string Get(int id)
        {
            return "value";
        }

        // POST api/values
        public void Post([FromBody]string value)
        {
        }

        // PUT api/values/5
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        public void Delete(int id)
        {
        }
    }
}
```
## Web API クライアントの開発

1\. まずクライアント用のアプリケーションを [Azure ポータル](https://portal.azure.com) より追加。アプリの登録より「新しいアプリケーションの登録」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/c7640c39-f479-6c0d-8b92-fa213b780039.png)

2\. 「アプリケーションの種類」より「ネイティブ」を選択し、任意の「リダイレクト URI」を指定して作成。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/592226bb-5c00-83ae-739e-a5d3fbd9d99b.png)

3\. 作成したアプリケーションの ID を確認して、「設定」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/24d3fd53-afb0-48ce-1dda-224b55188d59.png)

4\. 「必要なアクセス許可」を選択し「追加」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/be333e1a-d09f-0098-4af6-97fa1dee0642.png)

5\. 「API を選択します」をクリックし、検索バーで 「GraphWebAPIDemo」を検索して、選択。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/80f148a5-377f-402c-c725-f4e8e2ce93e4.png)

6\. 「アクセス許可を選択します」より、既存のアクセス許可を選択して「選択」し、最後に「完了」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/999d51eb-bc67-87d3-1817-13acd90a3411.png)

7\. また既に作成されている WebAPI 用のアプリケーションを開き、「設定」より「プロパティ」を選択。「アプリケーション ID/URI」を確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f87578ce-b82b-c493-f424-b856b0d2abc4.png)

8\. 次に、ソリューションに新しくコンソールアプリケーションを追加。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/8b1a2966-9d56-2d63-3473-a0e14a1b0dd3.png)

9\. NuGet の管理で 「Microsoft.IdentityModel.Clients.ActiveDirectory (ADAL)」および「JSON.NET」を追加。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/e815f027-c691-f6ff-64f3-98ec0f8bce1e.png)

10\. 参照の追加より System.Security　アセンブリを追加。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/14692a30-1e2a-0c13-a59c-c81d0a5fb731.png)

11\. TokenCacheHelper.cs ファイルを追加し、以下のコードと差し替え。これは ADAL で取得したトークンキャッシュをローカルディスクに保存するためのクラス。

```csharp:TokenCacheHelper.cs
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.IO;
using System.Security.Cryptography;

namespace GraphWebAPIClientDemo
{
    public partial class Program
    {
        class TokenCacheHelper
        {
            /// <summary>
            /// Get the user token cache
            /// </summary>
            /// <returns></returns>
            public static TokenCache GetTokenCache()
            {
                if (myTokenCache == null)
                {
                    myTokenCache = new TokenCache();
                    myTokenCache.BeforeAccess = BeforeAccessNotification;
                    myTokenCache.AfterAccess = AfterAccessNotification;
                }
                return myTokenCache;
            }

            static TokenCache myTokenCache;

            /// <summary>
            /// Path to the token cache
            /// </summary>
            public static readonly string CacheFilePath = System.Reflection.Assembly.GetExecutingAssembly().Location + ".msalcache.bin";

            private static readonly object FileLock = new object();

            public static void BeforeAccessNotification(TokenCacheNotificationArgs args)
            {
                lock (FileLock)
                {
                    args.TokenCache.Deserialize(File.Exists(CacheFilePath)
                        ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath),
                                                    null,
                                                    DataProtectionScope.CurrentUser)
                        : null);
                }
            }

            public static void AfterAccessNotification(TokenCacheNotificationArgs args)
            {
                // if the access operation resulted in a cache update
                if (args.TokenCache.HasStateChanged)
                {
                    lock (FileLock)
                    {
                        // reflect changesgs in the persistent store
                        File.WriteAllBytes(CacheFilePath,
                                            ProtectedData.Protect(args.TokenCache.Serialize(),
                                                                    null,
                                                                    DataProtectionScope.CurrentUser)
                                            );
                        // once the write operationtakes place restore the HasStateChanged bit to filse
                        args.TokenCache.HasStateChanged = false;
                    }
                }
            }
        }
    }
}
```
12\. Program.cs を以下のコードと差し替え。resourceUri は上記で確認した Web API の「アプリケーションID/URI」を指定。合わせてClientID も書き換え。

```csharp:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json;

namespace GraphUserDemo
{
    public partial class Program
    {
        private static AuthenticationContext authContext = new AuthenticationContext("https://login.microsoftonline.com/common/", TokenCacheHelper.GetTokenCache());
        private static string clientId = "f0bf59c9-50d9-4af2-97e6-8d6c401a9c4d";
        private static Uri redirectUri = new Uri("http://localhost/myapp");
        private string graphUrl = "https://graph.microsoft.com";

        static void Main(string[] args)
        {
            Program p = new Program();
            p.Run();
            Console.Read();
        }

        private async Task Run()
        {
            AuthenticationResult authResult = null;
            try
            {
                if (authContext.TokenCache.ReadItems().Count() > 0)
                    authContext = new AuthenticationContext(authContext.TokenCache.ReadItems().First().Authority, TokenCacheHelper.GetTokenCache());
                authResult = await authContext.AcquireTokenSilentAsync(graphUrl, clientId);
            }
            catch (AdalSilentTokenAcquisitionException ex)
            {
                authResult = await authContext.AcquireTokenAsync(graphUrl, clientId, redirectUri, new PlatformParameters(PromptBehavior.Always));
            }
            catch (Exception ex)
            {               
            }

            using (HttpClient client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization =
                    new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
                client.BaseAddress = new Uri(graphUrl);

                var result = await client.GetAsync("/v1.0/me");
                if (result.IsSuccessStatusCode)
                    Console.WriteLine(JsonConvert.SerializeObject(
                        JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()),
                        Formatting.Indented));
            }
        }
    }
}
```

## アプリケーションのテスト

1\. まず WebAPI プロジェクトを起動してから、クライアントアプリケーションを起動。サインインしたら権限の委任を承諾。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/aaa76fb2-a69b-6ae4-a443-624919ff70d5.png)

2\. WebAPI から Microsoft Graph の結果が返ることを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/656860ba-4efe-9109-2169-3866970f5efd.png)

## コードの詳細を確認

まず WebAPI のコードについて見ていきます。

**WebAPI としてのアクセストークン確認**
user_impersonation スコープで取得しているはずのトークンを再度確認しています。

```csharp
string[] addinScopes = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value.Split(' ');
if (addinScopes.Contains("user_impersonation"))
{
...
```

**代理でアクセストークンを取得**
代理取得のために、まず UserAssertion というユーザー情報を作ります。

```csharp
var bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext
    as System.IdentityModel.Tokens.BootstrapContext;
string userAccessToken = bootstrapContext.Token;
UserAssertion userAssertion = new UserAssertion(userAccessToken);
```
また、この BootstrapContext を取得するために、Startup.Auth.cs で SaveSigninToken = true パラメーターを指定しています。

```csharp
TokenValidationParameters = new TokenValidationParameters
{
    ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
    SaveSigninToken = true
}
```
作成した UserAssertion を使ってアクセストークンを取得。これでユーザーレベルのトークンが取得できます。

```csharp
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
AuthenticationContext authContext = new AuthenticationContext(authority);
var authResult = await authContext.AcquireTokenAsync(graphUri, clientCred, userAssertion);
```

**Microsoft Graph の呼び出し**
HttpClient を使って Graph を呼び出しています。Graph の C# SDK はまた別の機会に紹介します。

クライアント側はいつもと同じ感じですが、キーとなるのは、WebAPI に対するアクセス権の設定およびリソースが「アプリケーション ID/URI」になる点です。

```cshar[
private string resourceUri = "https://graphdemo01.onmicrosoft.com/GraphWebAPIDemo";
```

# まとめ

WebAPI の場合は認証する画面をサーバー側では出さないため、クライアント側での認証、管理者の同意、シークレットの利用など、多くの知識が必要となります。最近は WebAPI を活用したアーキテクチャーが増えているため、是非一度お試しください。

[目次へ戻る](https://qiita.com/kenakamu/items/6a1ca508dc093779eefa)

# 参照
[Azure ポータル](https://portal.azure.com/)
[Azure AD の認証シナリオ](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/active-directory-authentication-scenarios#native-application-to-web-api)
[GitHub:active-directory-dotnet-webapi-onbehalfof](https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof)
[Azure Active Directory のコード サンプル (V1 エンドポイント)](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/active-directory-code-samples)