Microsoft Graph を使ってみよう : Azure AD v1 エンドポイントのユーザー委任を ADAL で行う

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

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

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

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

# ユーザー委任と Active Directory Authentication Library (ADAL)

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

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

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

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\. 名前を指定し、「アプリケーションの種類」から「ネイティブ」を選択。任意の「リダイレクト URI」を指定し「作成」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/63aa3766-6761-9c80-079a-86651fe52f3b.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/7d16a885-b430-7103-f9ee-228e98f27185.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\. 「アクセス許可を選択します」をクリックし、必要な権限を追加。ここでは Have full access to user calendars を選択し、最後に「完了」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/897e3536-7bf8-1928-390b-d83c28721762.png)

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

1\. Visual Studio で C# のコンソールアプリケーションプロジェクトを作成。尚、.NET Core のコンソールアプリケーションは UI を出せないため、今回のコードでは動作しません。UI 出せない場合の対応は別の記事で紹介します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/dd6eec19-d44a-ea3b-ab32-0600a7df7631.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/e97381b4-3e9c-47b7-30ac-74f550b18d84.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 GraphUserDemo
{
    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 を以下のコードと差し替え。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));
            }
        }
    }
}
```

6\. F5 キーを押下してプログラムを実行。任意のユーザーでサインインして、権限に同意。登録した権限を全て要求されていることを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/bedc16b6-c5ad-3c1e-1058-25f823da44d5.png)

7\. 実行結果が取得できていることを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/193d4dd3-3e27-7ca9-bfaa-a9b7d3304873.png)

8\. アプリケーションを再起動しても、トークンがキャッシュされているため認証は聞かれないことを確認。

## コードの詳細確認

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

**認証/認可**
AcquireTokenSilentAsync メソッドはユーザーサインインを求めずトークンの取得を試みます。キャッシュがある場合はこれだけでトークンが取得できます。失敗した場合、AdalSilentTokenAcquisitionException 例外が発生します。

```csharp:Program.cs
await pca.AcquireTokenSilentAsync(scopes, pca.Users.FirstOrDefault());
```

AcquireTokenAsync はユーザーにサインインを求めてトークンを取得します。引数に new PlatformParameters(PromptBehavior.Always) を指定すると、常にサインイン画面を出せますが、他の引数も利用可能です。

```csharp:Program.cs
authResult = await authContext.AcquireTokenAsync(graphUrl, clientId, redirectUri, new PlatformParameters(PromptBehavior.Always));
```

**取得できるトークン**
AuthenticationResult はアクセストークン以外に、ID トークンを含みます。これは認証情報です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/996a94d6-7ec5-8c01-a554-8e80f5df9544.png)

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

## コードを改修して予定を取得

ではこちらのサンプルを少し改修して、カレンダーの予定をとってみましょう。Run メソッド内の一部を書き換えます。

```csharp:Program.cs
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));

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

予定が取得できています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/547919b7-dd35-ac88-c6af-22c770513666.png)

# まとめ
ユーザー委任は OAuth 2.0 の基本シナリオです。Microsoft Graph だけではなく、Azure AD v1 認証を使う全てのアプリケーションで使えるため、是非一度試してください。

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