Microsoft Graph を使ってみよう : Azure AD v1 エンドポイントのアプリケーション認証を ADAL で行う

#ユーザー委任とアプリケーション認証

Azure AD v1 エンドポイントの認証方法は 2 つあります。

**ユーザーに委任されたアクセス許可**
ユーザーが明示的にサインインして、アプリケーションで必要なアクセス許可に同意をする方法。

**アプリケーションのアクセス許可**
バックグランドサービス用で、事前に管理者が必要なアクセス許可に同意をし、プログラム実行時には認証を聞かれない方法。アプリケーション認証といわれることもある。

# アプリケーション認証と Active Directory Authentication Library (ADAL)

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

- C# コンソールアプリケーション
- アプリケーションシナリオ
- ADAL を利用

また Microsoft Graph に対するアクセス許可は管理者の許可が必要なレベルのものを利用してみます。

# アプリケーションの登録

OAuth 2.0 を使うために、まず初めにアプリケーションを登録します。

1\. [Azure ポータル](https://portal.azure.com/) にアクセスして、ログイン。ここでは組織アカウントを使用。ログイン後、Azure Active Directory を選択。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f9e8ccea-9d96-7972-dd0f-780f1882ce4b.png)

2\. 「アプリの登録」を選択し、「新しいアプリケーションの登録」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/3e0990e2-8214-6952-c8d3-d232d9742a43.png)

3\. 名前を指定し、「アプリケーションの種類」から「Web アプリ/API」を選択。任意の「リダイレクト URI」を指定し「作成」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/433c92e3-0c06-2739-6761-eaa5e96fb223.png)

4\. アプリが作成されたら「アプリション ID」を確認。その後「設定」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/615b5aa7-e5f5-590e-0871-730a82bba6d4.png)

5\. 「必要なアクセス許可」をクリックし、「追加」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/4c755e3a-67b6-0232-4c21-9447d1cd6dae.png)

6\. 「API を選択します」をクリックして、「Microsoft Graph」をクリック。「選択」ボタンをクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/e6e1af37-9f4b-dd32-37bd-77e1eba3dc0e.png)

7\. 「アクセス許可を選択します」をクリックし、必要な権限を追加。ここでは Read and write all users' full profiles を選択し、最後に「完了」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/cf582dd9-fde4-3eed-08e6-eaabac17fe32.png)

8\. 管理者のアクセス許可が必要なため、「アクセス許可の付与」をクリックし、「はい」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/ce3b9b2f-367a-c96b-ae4d-f079ca40d8cd.png)

9\. キーメニューをクリックし、説明に名前を、期間を選択して「保存」。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/2df73a17-84be-c127-5b82-19bfe7df06e8.png)

10\. 値が表示されるのでコピー。この値は画面遷移後はもう表示されない。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/0161c252-3950-1e0b-fd28-a9c6ea834936.png)

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

1\. Visual Studio で C# のコンソールアプリケーションプロジェクトを作成。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/5da3619e-2129-3d50-0d47-783cce66d494.png)

2\. NuGet の管理で 「Microsoft.IdentityModel.Clients.ActiveDirectory (ADAL)」および「JSON.NET」を追加。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/83435977-5539-00bb-c533-d1e02539ba88.png)

3\. 参照の追加より System.Security　アセンブリを追加。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/14692a30-1e2a-0c13-a59c-c81d0a5fb731.png)

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

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

namespace GraphAppDemo
{
    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;
                    }
                }
            }
        }
    }
}
```
5\. Program.cs を以下のコードと差し替え。Tenant や 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 GraphAppDemo
{
    public partial class Program
    {
        private static AuthenticationContext authContext;
        private static string tenant = "graphdemo01.onmicrosoft.com";
        private static string clientId = "5170ecc8-1246-4682-8523-99d7df3be696";
        private static string clientSecret = "AKqUvsJtUtxE...";
        private string graphUrl = "https://graph.microsoft.com";

        static void Main(string[] args)
        {
            authContext = new AuthenticationContext($"https://login.microsoftonline.com/{tenant}/",
                TokenCacheHelper.GetTokenCache());
            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.AcquireTokenAsync(graphUrl, new ClientCredential(clientId, clientSecret));
            }
            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/users");
                if (result.IsSuccessStatusCode)
                    Console.WriteLine(JsonConvert.SerializeObject(
                        JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()),
                        Formatting.Indented));
            }
        }
    }
}
```

6\. F5 キーを押下してプログラムを実行。ログインのダイアログは表示されず結果が取得できることを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/17bf2526-8b5b-faa3-53fd-dd8f9b8cb1e1.png)

## コードの詳細確認

**AuthenticationContext**
ADAL の基本クラスである AuthenticationContext に、アプリケーションを登録したテナントである Authority の値と、トークンキャッシュを渡します。今回はキャッシュをローカルに保存できるよう独自クラスを実装してるため、そちらを利用。

**認証/認可**
AcquireTokenAsync に対してリソース名と、クライアント ID とシークレットで作成した ClientCredential を渡してアクセストークンを取得。

```csharp:Program.cs
authResult = await authContext.AcquireTokenAsync(graphUrl, new ClientCredential(clientId, clientSecret));
```

**取得できるトークン**
ユーザー委任ではないため、IdToken は取得されない。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/0f793d51-c766-b275-2bed-21af766a4b3b.png)

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

# まとめ

定期的にデータを取得するなど、プログラムをユーザーの操作なしに実行する場合、アプリケーション認証が有効です。是非試してください。

[目次へ戻る](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-daemon](https://github.com/Azure-Samples/active-directory-dotnet-daemon)
[Azure Active Directory のコード サンプル (V1 エンドポイント)](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/active-directory-code-samples)