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\. [ユーザー委任シナリオ](https://qiita.com/kenakamu/items/81c3dff2bbdc5562418e)で作成したアプリケーションを利用、または新規に登録。プラットフォームの項目で「プラットフォームの追加」をクリック。Web アプリケーションを選択。

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

3\. アプリケーションシークレットの項目で「新しいパスワードを生成」をクリック。パスワードが表示されるのでコピーして保存。
※このパスワードは画面を消すと二度と取得できない。

<img width="870" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/6e5068bd-10f8-5870-ed0b-05d84548ab82.png">

4\. Microsoft Graph のアクセス許可の「委任されたアクセス許可」項目で、「追加」をクリック。

<img width="851" alt="Sketch.png" src="https://qiita-image-store.s3.amazonaws.com/0/214116/71270a50-75f6-7b19-70ee-a7537616b19f.png">

5\. 必要な権限を追加。ここでは User.ReadWrite.All を選択。

<img width="872" alt="Capture.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/214116/84247208-f639-f3f6-eedf-1ee6151712f6.png">

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

## 管理者の同意を取得

ユーザーなしでアクセスを行う場合、および権限に管理者の同意が必要な場合は、エンドポイントにリクエストを投げて同意を行います。

1\. ブラウザに以下のアドレスを入力。

- テナント名は自分のものに変更
- client_id : 登録したアプリケーションのアプリケーション ID
- redirect_uri : 送信したリクエストの戻り先 (今回は受け取って処理しないので仮のアドレス)
- state : リクエストの応答に含まれる値で戻り先で検証が可能

https://login.microsoftonline.com/graphdemo01.onmicrosoft.com/adminconsent
?client_id=550dcc01-129d-4b51-b80c-96995a6848e3
&state=12345
&redirect_uri=http://localhost

2\. サインイン画面が出るので、管理者権限でサインイン。この場合、アプリケーション権限だけでなく、ユーザーの権限も含めたすべての権限が表示される。よってカレンダーの権限も求められる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/c75cc33d-2d78-5fef-d292-4f95eadd4771.png)

3\. 結果はリダイレクトされるので URL で admin_consent の値が True であることを確認。先ほど指定した redirect_uri にリダイレクトされ、また state も指定した元同じ値が戻る。

http://localhost/?admin_consent=True&tenant=00a88ff8-2bc8-4b65-bd69-c5979cd2b9b0&state=12345

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

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/9998e38b-d77e-2ec0-59a6-6650eeb335dd.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/d0b74df5-dcbd-97ce-1e99-786ae3e601cc.png)

3\. 参照より System.Security を追加。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/4dcf07e9-b9d6-3cf7-215d-8bb6c778d45c.png)

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

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

namespace GraphAppDemo
{
    public partial class Program
    {
        static class TokenCacheHelper
        {
            /// <summary>
            /// Get the user token cache
            /// </summary>
            /// <returns></returns>
            public static TokenCache GetAppCache()
            {
                if (appTokenCache == null)
                {
                    appTokenCache = new TokenCache();
                    appTokenCache.SetBeforeAccess(BeforeAccessNotification);
                    appTokenCache.SetAfterAccess(AfterAccessNotification);
                }
                return appTokenCache;
            }
            
            static TokenCache appTokenCache;
         
            /// <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 を以下のコードと差し替え。テナント名など適宜書き換え。

```csharp:C#
using Microsoft.Identity.Client;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace GraphAppDemo
{
    public partial class Program
    {
        private static string clientId = "550dcc01-129d-4b51-b80c-96995a6848e3";
        private static string authority = "https://login.microsoftonline.com/graphdemo01.onmicrosoft.com/v2.0";
        private static string redirectUri = "http://localhost";
        private static Uri graphUrl = new Uri("https://graph.microsoft.com/");
        private static string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
        private static string secret = "hSL3....";
        private static ConfidentialClientApplication cca;
   
        static void Main(string[] args)
        {
            cca = new ConfidentialClientApplication(clientId, authority,
                redirectUri, new ClientCredential(secret), null, TokenCacheHelper.GetAppCache());
            
            Program p = new Program();
            p.Run().Wait();
            Console.ReadLine();
        }

        private async Task Run()
        {
            using (HttpClient client = new HttpClient())
            {
                string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
                client.DefaultRequestHeaders.Authorization =
                    new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));
                client.BaseAddress = graphUrl;

                var result = await client.GetAsync("/v1.0/users");
                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;

            authResult = await cca.AcquireTokenForClientAsync(scopes);

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

6\. F5 キーを押下してプログラムを実行。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/bb05fec5-622e-cda9-f302-e5ddb44578fd.png)

## コードの詳細確認

ではコードの詳細を確認していきます。


**スコープ**
アプリケーション認証の場合のスコープは、ユーザー認証と異なり、https://graph.microsoft.com/.default 1 つだけとなります。これで登録したすべての権限を要求します。

**認証**
まず MSAL クライアントを作成。アプリケーション認証をする場合は、ConfidentialClientApplication を利用します。

引数として　

- clientId : 登録したアプリケーション ID
- authority : ログイン先テナントを指定した login.microsoftonline.com アドレス
- redirectUri : Web プラットフォームに登録したリダイレクト URL 
- ClientCredential : 取得したパスワードを指定して作成
- UserTokenCache/AppTokenCache: ユーザートークンおよびアプリケーショントークンのキャッシュ 

```csharp:C#
authClient = new ConfidentialClientApplication(clientId, authority, redirectUri, new ClientCredential(secret), null, TokenCacheHelper.GetAppCache());
```

次にアクセストークン取得です。アプリケーション認証のトークンは、AcquireTokenForClientAsync メソッドにスコープのみ指定して実行。

```csharp:C#
authClient.AcquireTokenForClientAsync(scopes)
```

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

## タイトルを更新

権限も付与済ですので、ユーザーのタイトルを更新してみます。

1\. Run メソッドを以下のコードと差し替え。

```csharp:C#
private async Task Run()
{
    using (HttpClient client = new HttpClient())
    {
        string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync(scopes));
        client.BaseAddress = graphUrl;

        var result = await client.GetAsync("/v1.0/users?$select=id,displayName,jobTitle");
        var users = JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync());


        foreach (var user in (users as JToken)["value"] as JArray)
        {
            var id = user.Value<string>("id");
            user["jobTitle"] = "アーキテクト";
            var request = new HttpRequestMessage(new HttpMethod("PATCH"), $"/v1.0/users/{id}");
            request.Content = new StringContent(JsonConvert.SerializeObject(user), System.Text.Encoding.UTF8, "application/json");
            result = await client.SendAsync(request);
        }

        result = await client.GetAsync("/v1.0/users?$select=id,displayName,jobTitle");
        Console.WriteLine(JsonConvert.SerializeObject(
                JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()),
                Formatting.Indented));
    }
}
```

2\. F5 キーを押下して実行。結果を確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/ba01f600-b556-1bad-0bf4-26689e417846.png)

# まとめ
アプリケーション認証は管理者同意が必要な強力な権限を必要としますが、バックグランドサービスとして動かしておくシナリオでは必須です。

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

# 参照
[ユーザーなしでアクセスを取得](https://developer.microsoft.com/ja-jp/graph/docs/concepts/auth_v2_service)
[Azure Active Directory v2.0 と OAuth 2.0 クライアント資格情報フロー](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/active-directory-v2-protocols-oauth-client-creds)
[Microsoft Graph Client Library for .NET](https://github.com/microsoftgraph/msgraph-sdk-dotnet)