Microsoft Graphを.NETで使いAzure ADから情報を取得してみた

この記事では、認証基盤であるAzure AD からユーザー一覧を取得する実装についてお話しできればと思います。
具体的には.NETのSDKを使ってMicorsoft Graph API 経由でAzure AD からユーザー一覧を取得するAPIを作成します。
当方も日々、新しい知識と戦っていますので、間違いがあれば教えてください!!!


# 前提条件
- Azure ADを含むAzure環境を自由に操作できる
- Visual Studioがインストールされている（Visual Studio Code などのソースコードエディタでもできるっちゃできる）
- PostmanなどのAPIをたたけるツールがある

# Microsoft Graphとは
詳しくは[公式ドキュメント](https://docs.microsoft.com/ja-jp/graph/overview)から
簡単にまとめると、Microsoft のサービスである Office 365 や Azure AD など、様々なサービスのデータをグラフ形式で扱えるエンドポイントです。グラフ形式でデータを扱えるので、「自分の予定」や「自分の上司」などデータを関連から取得できるものです。

# Azure Portalでの準備
## Azure ADでアプリの登録
Azure AD では「アプリの登録」という機能を使ってあらかじめ登録されているWebアプリケーションからであれば認証要求は受け付けるよ！という機能があります。
こういう機能のことをサービスプリンシパルといいます。

### 新規作成
「Azure Active Directory」⇒「アプリの登録」⇒「新規作成」からアプリケーションの登録を行います。（命名は目を瞑ってください...）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/46dfa2c2-4634-15f9-354b-40a3c8a69fb3.png" width=500 />

### アクセス許可の付与
アプリの概要のページから、左のメニューの[APIのアクセス許可]を選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/4607f632-df02-6398-66fc-ffb182b4a92a.png" width=200 />

- [+アクセス許可の追加]をクリックし、付与したいAPIを選択
    - 今回はGraph APIを使って、ユーザー情報の読み取りを行います
    - [Microsoft Graph API]を選択
        - [委任されたアクセス許可] ... (A) 対話的にユーザーが許可を与える
        - [アプリケーションの許可] ... (B) あらかじめ管理者が許可を与え、ユーザ操作なしで動く
          - 後に出てきますが、今回はAPIで実行するので(B)とします。 
          - ちなみにMicrosoft Graph のアクセス許可の[リファレンス](https://docs.microsoft.com/ja-jp/graph/permissions-reference)
    - その中で [User.Read.All]を選択(おおざっぱにAllでいきます。)
    - [アクセス許可の追加]をクリック


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/e77fa962-da4b-3be6-7d10-448dbd9a1896.png)


アクセス許可一覧に1行増えていると思います。
[User.Read.All」の場合は「管理者の同意が必要」なので同意を与えましょう。

- [規定のディレクトリに管理者の同意を与えます]をクリック
  - 先のアクセス権の追加だけでなく、管理者の同意を与えて、初めてAPIのアクセス権が有効になります
  - ※ポータルにログインしているユーザのロールが「グローバル管理者」になっていないと、同意ができない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/f3b6fc53-a55a-5586-fe4a-c91addb42f81.png)

### クライアントシークレットの作成
- 登録したアプリを表示し、左の[証明書とシークレット]を選択
- [新しいクライアントシークレット]をクリック
  - 名前と期間を指定して生成
- 生成されたシークレットの値を確認(この値は後で使うのでどこかに控えます)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/80ac415c-e142-739a-7c4e-1893769e23fa.png)




# プロジェクトの準備
## 前置き
今回はVisual Studio 2019を使っていこうと思います。
プロジェクトの作成から、「Azure Functions」を選択します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/4c8980b2-771c-4518-8244-59ce4b0f2587.png" width=500 />

使用するフレームワークは .NET Core 3.1 です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/e0fae177-4e22-7fba-4072-b5fa46be35c0.png)

次に必要なパッケージをインストールします。

- Microsoft.Graph
- Microsoft.Graph.Auth (プレリリース)
- Microsoft.Identity.Client

もちろんコマンドでもOKです

```
Install-Package Microsoft.Graph
Install-Package Microsoft.Graph.Auth -IncludePrerelease
```

「プレリリースを含める」をチェックすると「Microsoft.Graph.Auth」も出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/c9190280-bb08-856d-f947-f7f0256bdc5f.png)

今回は下記のバージョンでやっていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/ef3665d2-2168-9b1b-bff7-fdc11adb77f3.png)

次に、認証プロバイダーの選択について考えます。
[公式ドキュメント](https://docs.microsoft.com/ja-jp/graph/sdks/choose-authentication-providers?tabs=CS)
今回は、Azure AD からユーザー一覧を取得するAPIを作成しようと思うので「[クライアント資格情報プロバイダー](https://docs.microsoft.com/ja-jp/graph/sdks/choose-authentication-providers?tabs=CS#ClientCredentialsProvider)」を選択します。
公式ドキュメントにも、下記の通り記されております。
「クライアント資格情報フローを使用すると、ユーザーによる操作なしでサービスアプリケーションを実行できます。」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/cff61d0e-f4c6-0668-272e-71f10c681d44.png)

クライアント資格情報のテンプレートが下記のようにあります。

```公式ドキュメント引用.cs
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithTenantId(tenantID)
    .WithClientSecret(clientSecret)
    .Build();

ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
```

ここで、clientId、tenantId、clientSecretは何だろうと思うので話をそちらに切り替えます。
最初に作成した「アプリの登録」につながります。

- portal上で「アプリの登録」から作成したアプリを表示します。
- 「概要」に「アプリケーション(クライアント)ID」と「ディレクトリ(テナント)ID」とあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/f3a60177-2830-00ae-b206-dd796df11872.png)

つまり、テンプレートではそれぞれ
clientId=「アプリケーション(クライアント)ID」
tenantId=「ディレクトリ(テナント)ID」
clientSecret=「クライアントシークレット」(クライアントシークレットの作成をした際に控えたシークレットの値)

を使えばよいということです。

## 実装
実装といっても数行で収まります。
最初からテンプレートで作成されている「Function1.cs」を使います。

### クライアントの作成
まずは、Microsoft Graphクライアントを作成します。
下記がクライアントを作成するコードになります。

[公式ドキュメント](https://docs.microsoft.com/ja-jp/graph/sdks/create-client?tabs=CS)

``` 公式ドキュメント.cs
// Build a client application.
IPublicClientApplication publicClientApplication = PublicClientApplicationBuilder
            .Create("INSERT-CLIENT-APP-ID")
            .Build();
// Create an authentication provider by passing in a client application and graph scopes.
DeviceCodeProvider authProvider = new DeviceCodeProvider(publicClientApplication, graphScopes);
// Create a new instance of GraphServiceClient with the authentication provider.
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
```

ただし、今回はプロバイダーをクライアント資格情報プロバイダーで使いたいので、そこだけ書き換えます。


``` 書き換えたやつ.cs
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithTenantId(tenantID)
    .WithClientSecret(clientSecret)
    .Build();

ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);

GraphServiceClient graphClient = new GraphServiceClient(authProvider);
```

### Azure AD からユーザー一覧取得
graphClient.hogehogeでいろいろなAPIを呼び出せます。
Azure AD からユーザー一覧をとる場合は下記のコードとなります。

``` .cs
var users = await graphClient.Users.Request().GetAsync();
```

### 今回のソースコード全貌
これらを踏まえて書くとこうなります。

``` Function1.cs
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Identity.Client;
using Microsoft.Graph.Auth;
using Microsoft.Graph;

namespace SampleGraphAPI
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            //string name = req.Query["name"];

            //string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            //dynamic data = JsonConvert.DeserializeObject(requestBody);
            //name = name ?? data?.name;

            //string responseMessage = string.IsNullOrEmpty(name)
            //    ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response."
            //    : $"Hello, {name}. This HTTP triggered function executed successfully.";

            //return new OkObjectResult(responseMessage);


            IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
                .Create(clientId) // 自分の環境に合わせる
                .WithTenantId(tenantID) // 自分の環境に合わせる
                .WithClientSecret(clientSecret) // 自分の環境に合わせる
                .Build();

            ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);

            GraphServiceClient graphClient = new GraphServiceClient(authProvider);


            var users = await graphClient.Users.Request().GetAsync();

            return new OkObjectResult(users);
        }
    }
}
```

# Postmanで動かしてみた
Postmanで動かしてみたところ、こんな感じで無事取得できました。（中身は伏せさせていただきます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889527/120f8f9b-eb6b-ff36-7c9f-742081c53781.png)

# まとめ
今回は自分がAzure AD からデータを取得する際に苦戦したので、その方法についてまとめてみました。
Graph APIを使うことで、他にもユーザーの追加であったりなど様々なこともできそうだなと思いました。