Microsoft Graph を使ってみよう : Azure AD v1 エンドポイントでデバイスのプロファイル認証を ADAL で行う

#デバイスのプロファイル認証

.NET Core コンソールアプリケーションやキーボードもタッチスクリーンもないデバイスなど、ユーザーがサインインする手段が提供できない環境があった場合、デバイスプロファイル認証が利用できます。

このフローでは認証が必要な場合、アプリケーションは認証先のアドレスとデバイス ID を提示し、ユーザーは指示に従って別のコンピューターなどで認証を完了すると、その情報が元のアプリケーション側に通達されます。
今回は以下のアプリケーションを作ります。

- .NET Core 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/8f11057b-a41f-bbd8-2212-7c38e9148b04.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/40ffc2eb-3d23-ed6d-fe80-5f5645509bd3.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 で .NET Core C# のコンソールアプリケーションプロジェクトを作成。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f631d5f8-5fa4-2ac1-a8db-9eef7247da10.png)


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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/e68395bc-bdd0-7944-6a74-ead5d68bc926.png)

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

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

namespace GraphDeviceDemo
{
    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;
                    }
                }
            }
        }
    }
}
```

4\. 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 GraphDeviceDemo
{
    public partial class Program
    {
        private static AuthenticationContext authContext = new AuthenticationContext("https://login.microsoftonline.com/common/", TokenCacheHelper.GetTokenCache());
        private static string clientId = "dd2a658e-8127-4956-ab57-bc203ad7b77b";
        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
            {
                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)
                {
                    DeviceCodeResult codeResult = await authContext.AcquireDeviceCodeAsync(graphUrl, clientId);
                    Console.WriteLine("サインインが必要です。");
                    Console.WriteLine($"{codeResult.VerificationUrl} にアクセスして、デバイスコード {codeResult.UserCode} を指定してログインしてください。");
                    authResult = await authContext.AcquireTokenByDeviceCodeAsync(codeResult);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Something went wrong.");
                Console.WriteLine("Message: " + ex.Message + "\n");
            }

            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));
            }
        }
    }
}
```

5\. F5 キーを押下してプログラムを実行。画面にメッセージが出る。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/42858134-9f3b-ef0d-7699-19a205eac905.png)

6\. 指定されたアドレスにアクセスしてデバイスコードを入力。「続行」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/4236d2bc-4cb7-b9b7-83ac-a0a4a599a3c9.png)

7\. アクセス許可で「承諾」をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/f9a5aecc-86b3-d6ca-f5b1-b0a389fd51a7.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/8ef926e4-6ba5-4f68-9d85-85ae41c513aa.png)

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

## コードの詳細確認

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

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

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

AcquireDeviceCodeAsync はデバイスプロファイル認証フローで必要な情報を取得します。結果を画面に出して、ユーザーがログインできるようにします。

```csharp:Program.cs
DeviceCodeResult codeResult = await authContext.AcquireDeviceCodeAsync(graphUrl, clientId);
Console.WriteLine("サインインが必要です。");
Console.WriteLine($"{codeResult.VerificationUrl} にアクセスして、デバイスコード {codeResult.UserCode} を指定してログインしてください。");
```
AcquireTokenByDeviceCodeAsync を実行して、ユーザーがログインするのを待ちます。ログインが完了したら結果が返ります。

```csharp
authResult = await authContext.AcquireTokenByDeviceCodeAsync(codeResult);
```

**取得できるトークン**
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)

# まとめ
デバイスプロファイル認証はクロスプラットフォームで動作するコマンドラインアプリケーションや IoT デバイス、またキオスクシナリオなどで活用できる機能です。Azure CLI もこの方式で認証をしています。是非試してください。

[目次へ戻る](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-deviceprofile](https://github.com/Azure-Samples/active-directory-dotnet-deviceprofile)
[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)