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

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

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

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

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

# ユーザー委任と Microsoft Authentication Library (MSAL)

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

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

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

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

1\. [Microsoft アプリ登録ポータル](https://apps.dev.microsoft.com/) にアクセスして、ログイン。ここでは組織アカウントを使用。

<img width="924" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/a7d0dde0-654f-ff1d-7925-c72aaf83ffd8.png">

2\. 画面右上の「アプリの追加」をクリックし、アプリケーション名を入力後、Create をクリック。

<img width="763" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/922c2ca3-bb65-1c92-0217-20709c99e6c6.png">

3\. 画面に表示されているアプリケーション ID をコピー。この ID がアプリケーションの固有識別子となる。
<img width="567" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/d09a2455-5e6e-bed6-8c6b-4682e3bb97c8.png">

4\. プラットフォームの項目で「プラットフォームの追加」をクリック。選択肢が出るので必要なものを選択。ここではネイティブアプリケーションを選択。

<img width="606" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/89cfe6c2-7f02-855c-d1c0-8963bb7ec514.png">

5\. Microsoft Graph のアクセス許可の「委任されたアクセス許可」項目で、「追加」をクリック。アプリケーションのアクセス許可はアプリケーション認証用のため、次回見ていきます。

<img width="619" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/6f181201-75a0-11b5-5e64-d504ad3be1a3.png">

6\. 必要な権限を追加。ここでは Calendars.ReadWrite を選択。

<img width="621" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/b488e75f-d2da-dd31-38c8-cb5970cdebca.png">

7\. 最後に「保存」をクリック。

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

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/7a7b5dba-aaab-c77b-1a7b-cc9ca6469dc7.png)


2\. NuGet の管理で 「Microsoft.Identity.Client (MSAL)」および「JSON.NET」を追加。尚 MSAL はプレビューのため、プレビューを含める。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a1274c5a-1326-83f3-e84c-5a8d608aa726.png)

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

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

4\. TokenCacheHelper.cs ファイルを追加し、以下のコードと差し替え。

```csharp:TokenCacheHelper.cs
using Microsoft.Identity.Client;
using System.IO;
using System.Security.Cryptography;

namespace GraphUserDemo
{
    public partial class Program
    {
        static class TokenCacheHelper
        {
            /// <summary>
            /// Get the user token cache
            /// </summary>
            /// <returns></returns>
            public static TokenCache GetUserCache()
            {
                if (usertokenCache == null)
                {
                    usertokenCache = new TokenCache();
                    usertokenCache.SetBeforeAccess(BeforeAccessNotification);
                    usertokenCache.SetAfterAccess(AfterAccessNotification);
                }
                return usertokenCache;
            }

            static TokenCache usertokenCache;

            /// <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 Microsoft.Identity.Client;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.Net.Http.Headers;
using Newtonsoft.Json;

namespace GraphUserDemo
{
    public partial class Program
    {
        private static string clientId = "550dcc01-129d-4b51-b80c-96995a6848e3";
        private static string redirectUri = "http://localhost";
        private static PublicClientApplication pca;
        private static Uri graphUrl = new Uri("https://graph.microsoft.com");
        static void Main(string[] args)
        {
            #region GraphClient and Auth

            pca = new PublicClientApplication(
                clientId,
                "https://login.microsoftonline.com/common",
                TokenCacheHelper.GetUserCache());

            #endregion

            Program p = new Program();
            p.Run().Wait();
            Console.ReadLine();
        }

        private async Task Run()
        {
            using (HttpClient client = new HttpClient())
            {
                string[] scopes = new string[] { "user.read" };
                client.DefaultRequestHeaders.Authorization =
                    new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));
                client.BaseAddress = graphUrl;

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

        private async Task<string> GetAccessTokenAsync(string[] scopes)
        {
            AuthenticationResult authResult = null;

            try
            {
                authResult = await pca.AcquireTokenSilentAsync(scopes,
                    pca.Users.FirstOrDefault());
            }
            catch (MsalUiRequiredException ex)
            {
                authResult = await pca.AcquireTokenAsync(scopes);
            }

            if (authResult != null)
                return authResult.AccessToken;
            else
                return null;
        }
    }
}
```

6\. F5 キーを押下してプログラムを実行。Read.User に該当する "Sign you in and read your profile" を聞かれることを確認。"Access your data anytime" は既定で入っている。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/8d02dd45-be2f-c531-b168-e03ffc418a22.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/adef674a-343c-f311-2a27-3769dc0c0e03.png)


## コードの詳細確認

**PublicClientApplication**
MSAL は 2 つのメインクラスがあります。PublicClientApplication はユーザー委任で利用するクラスです。

引数 

　- ClientId : 登録したアプリケーションの ID
　- ログインテナント : 特定のテナントか、今回のように common にすればマルチテナント対応
　- TokenCache : 認証済ユーザーをキャッシュするオブジェクト

**委任のスコープ**
アプリケーションにはカレンダーの権限も入れましたが、サインイン時に求められなかったのは、アプリケーション内でその権限を要求しなかったためです。尚、本来スコープは [リソース名]/[アクセス許可] のフォーマットで記述しますが、Microsoft Graph だけは例外で、リソース名を省略することが出来るため、アクセス許可だけを記述できます。

```csharp:Program.cs
string[] scopes = new string[] { "user.read" };
```

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

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

AcquireTokenAsync はユーザーにサインインを求めてトークンを取得します。引数に UIBehavior.Consent を指定すると、同意画面も毎回表示できます。

```csharp:Program.cs
await pca.AcquireTokenAsync(scopes, pca.Users.FirstOrDefault(), UIBehavior.Consent, null);
```

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/a6ed25c3-2215-568c-9d3d-c2c245d7fc7b.png)

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

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

ではこちらのサンプルを少し改修して、カレンダーの予定をとってみましょう。追加で Calender.Read を要求しています。Run メソッドを以下のコードを差し替えてください。

```csharp:Program.cs
private async Task Run()
{
    using (HttpClient client = new HttpClient())
    {
        string[] scopes = new string[] { "user.read" };
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));
        client.BaseAddress = graphUrl;

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

        scopes = new string[] { "calendars.read" };
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));

        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/db7af312-69cc-0919-371c-fb40879d9cd4.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/3e90a67e-41ca-dcb3-b44b-93bc2a56a5fa.png)

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

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

# 参照
[Microsoft アプリ登録ポータル](https://apps.dev.microsoft.com/)
[C# WPF MSAL および Graph サンプル](https://github.com/Azure-Samples/active-directory-dotnet-desktop-msgraph-v2)
[Microsoft Authentication Library for .NET](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet)