訳あってMicrosoft Graph API調べてみた

# はじめに

最近Microsoft Graph APIについて調べる機会がありました。この記事はその中から一般的な範囲についてまとめたものです。
AzureもAD(Aictive Directory)もGraph APIも詳しくないので誤りがあるかもしれません。間違いを発見されたらコメントや編集リクエストをお待ちしています。

# Microsoft Graph APIとは

ざっくり言うと、Officie 365 や Azure ADなどの情報を検索、更新できるWeb API。例えば次のようなリソースにアクセス可能（許可を与えれば）

- ユーザー
- グループ
- ディレクトリ(AD)
- デバイス情報
- カレンダー
- メール
- チャット
- ファイル
- 通知
- チーム
- 場所

今回はユーザー情報にターゲットを絞る。

# Graph API の種別

- (A) ユーザーの代わりにアクセスを取得（ユーザーが対話的に許可を与える）
    - ブラウザなどで、利用者本人がアプリのアクセスを許可する
    - GUI アプリケーションで利用
- (B) ユーザーなしでアクセスを取得（あらかじめ管理者が許可を与えれば、ユーザ操作なしで動く）
    - 管理コンソールで、管理者が許可を与える
    - バッチ的なアプリケーションで利用

(A)の認証画面の例
<img width="458" alt="user_permission.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/390a62d8-0520-042c-91f8-3a4c6eac5abe.png">

# Graph API 利用までの準備

- Azureへのサインアップ
- アプリの登録
- APIアクセス許可の付与
- 管理者の同意
- クライアントシークレットの生成

## Azureへのサインアップ

- Azure にサインアップ ... 今回は12ヶ月の無料体験でスタート
- Azureポータルにログイン https://portal.azure.com
- Azure Active Directory のページに移動
    - ホームからアイコンをクリック
    - または、左上のメニューから選択

<img width="408" alt="azure_ad.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/55cc4d6e-8923-031e-5478-c49cf52906e6.png">

※この時[規定のディレクトリ - 概要]で「ロール」が「グローバル管理者」になっていることを確認。

<img width="320" alt="azure_ad_user_role.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/c4b81635-9a77-7ee4-37d9-6b8a4ef05682.png">

- 「ロール」が「ユーザー」の場合は、この後でアプリにWrite権限を与えることができない（最初それでハマった）
    - ロールが「ユーザー」の場合は、ログアウト→再ログインでロールが変わる（？）


## アプリの登録

<img width="272" alt="azure_add_app.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/d2899d29-6a00-fe34-2418-17f4ee1db648.png">

- Active Directoryのページで、メニューから「アプリの登録」を選択
    - もし「ディレクトリに登録」か「個人アカウントに関連付ける」を聞かれたら前者を選択
    - （そうしないと、この後でアプリにWrite権限を与えることができない）
- [新規登録]からアプリを登録
    - 名前
    - 種類（シングルテナント/マルチテナント/マルチテナント+個人）
        - シングルテナント or マルチテナント を選択
    - リダイレクト先のURI
- アプリの情報を確認
    - アプリケーションID(client_id)
    - ディレクトリID(tenant ID)

アプリ登録中の画面

<img width="723" alt="azure_new_app.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/8463c9fa-15ed-45d4-67a0-41a62162c37e.png">


アプリ登録後の表示

<img width="727" alt="azure_app_info.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/7e98aa15-6a5d-8526-c737-3d861615557a.png">


## APIアクセス許可の付与

- アプリの概要のページから、左のメニューの[APIのアクセス許可]を選択

<img width="243" alt="azure_app_api_permission.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/460e48fb-bddb-af5a-61fe-0d69d7d6bd6c.png">

- [+アクセス許可の追加]をクリックし、付与したいAPIを選択
    - 今回はGraph APIを使って、ユーザー情報の読み書きを行う
    - [Microsoft Grap API]を選択
        - [委任されたアクセス許可] ... (A) 対話的にユーザーが許可を与える
        - [アプリケーションの許可] ... (B) あらかじめ管理者が許可を与え、ユーザ操作なしで動く
        - ※バッチ的に動かすには(B)を選択。
            - この時「個人アカウントに関連付ける」では、Write許可が与えられない様子
    - その中で [User.ReadWrite.All]を選択
    - [アクセス許可の追加]をクリック

<img width="796" alt="azure_api_graph.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/ce9608c4-3c21-e9b6-0c8f-682441dbfac0.png">

<img width="825" alt=" azure_graph_with_or_without_user.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/53ce80ac-bf1a-f1da-ff5e-3323bb0e6332.png">

<img width="774" alt="azure_api_user.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/9a8ebe48-1c38-2b0f-c19a-56829651baff.png">


## 管理者の同意

- [規定のディレクトリに管理者の同意を与えます]をクリック
    - 先のアクセス権の追加だけでなく、管理者の同意を与えて、初めてAPIのアクセス権が有効になる
    - ※ポータルにログインしているユーザのロールが「グローバル管理者」になっていないと、同意ができない

<img width="843" alt="azure_api_admin_ok.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/def9bd09-a04e-1bb2-ef23-fb6339db8bdf.png">


## クライアントシークレットを生成

- 登録したアプリを表示し、左の[証明書とシークレット]を選択
- [新しいクライアントシークレット]をクリック
    - 名前と期間を指定して生成
- 生成されたシークレットの値を確認

<img width="843" alt="azure_api_client_secret.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/97dae0bb-0304-65e2-57e3-5a05ecf6d351.png">


# Graph APIを使う

ここでは[「ユーザーなしでアクセスを取得」](https://docs.microsoft.com/ja-jp/graph/auth-v2-service)のケースを記載

## アクセストークンの取得

- POSTでアクセストークンを取得
    - アプリのクライアントID, テナントID, クライアントシークレットを利用

curlコマンドでPOST実行し、token.jsonに保存する例

```text:curlの実行例
curl -d "client_id={クライアントID}" \
 -d "scope=https%3A%2F%2Fgraph.microsoft.com%2F.default" \
 -d "client_secret={クライアントシークレット}" \
 -d "grant_type=client_credentials" \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -X POST https://login.microsoftonline.com/{テナントID}/oauth2/v2.0/token  > token.json
```

取得結果(token.json)の例

```json:token取得結果
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "ext_expires_in": 3600,
  "access_token": "xxxxxxxxxxx......."
}
```

→ 以後、取得したaccess_tokenの値を利用する


## トークンを使って情報取得

GETでユーザーの情報を取得

- 先ほどのアクセストークンの値と、取得するユーザーのオブジェクトIDを指定する

```text:curlで取得してファイルに保存する例
curl -H "Authorization: Bearer {access_tokenの値}" \
 https://graph.microsoft.com/v1.0/users/{取得するユーザーのオブジェクトID} > user.json
```

```json:ユーザー情報の例
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "businessPhones": [],
  "displayName": "Joe Test",
  "givenName": null,
  "jobTitle": null,
  "mail": null,
  "mobilePhone": null,
  "officeLocation": null,
  "preferredLanguage": null,
  "surname": null,
  "userPrincipalName": "joe@xxxxxxxxx.onmicrosoft.com",
  "id": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```

## トークンを使って情報の更新

PATCHで部分的に属性を更新可能

- アクセストークンの値と、更新するユーザーのオブジェクトIDを指定する
- ボディ部分にjsonで更新する属性、値を指定する

```terminal:curlでPATCHを実行
curl --request PATCH https://graph.microsoft.com/v1.0/users/{更新するユーザーのオブジェクトID} \
 -H "Authorization: Bearer {access_tokenの値}" \
 -H "Content-Type: application/json" \
 -d '{ "jobTitle": "test user"}'
```

APIのアクセス許可が適切に追加できていれば、指定した属性のみ更新される



# Graph API「ユーザーの代わりにアクセスを取得する」場合

## ユーザによる許可

- ブラウザでURLを開く
    - https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id={クライアントID}&response_type=code&redirect_uri={リダイレクトURIのエンコード表記}&response_mode=query&scope=offline_access%20user.read%20mail.read&state={任意の数値}
        - scope=でアクセス範囲を指定（この例では、user情報の取得、mailの取得)
    - アクセスの許可を求める画面が出るので、許可をあたえる
- リダイレクト先に飛ぶ
    - URLにstateが含まれるので、先ほど指定した数値と一致することを確認
    - URLにcodeが含まれるので、それを取り出す
- リダイレクト例)
    - http://localhost/?code=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&state={先ほど指定した数値}

## アクセストークンの取得

POSTでアクセストークンを取得

```text:curlを使った例
curl -d "client_id={クライアントID}}" \
 -d "scope=user.read mail.read" \
 -d "code={ユーザーによる許可で得られたcode}" \
 -d "redirect_uri={リダイレクトURIのエンコード表記}" \
 -d "grant_type=authorization_code" \
 -d "client_secret={クライアントシークレット}" \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -X POST https://login.microsoftonline.com/common/oauth2/v2.0/token
```

```json:トークン取得結果の例
{
  "token_type": "Bearer",
  "scope": "User.Read Mail.Read",
  "expires_in": 3600,
  "ext_expires_in": 3600,
  "access_token": "xxxxxxxxxxxxxx...",
  "refresh_token": "xxxxxxxxxxxxxxx..."
}
```

## 情報の取得

```text:自分の情報の取得
curl -H "Authorization: Bearer {アクセストークンの値}" https://graph.microsoft.com/v1.0/me
```

```text:ユーザーを指定して情報を取得
curl -H "Authorization: Bearer {アクセストークンの値}" \
 https://graph.microsoft.com/v1.0/users/{ユーザーID}
```

## その他

- 有効期間内なら refresh_token を使ってアクセストークンを再取得できる
- 情報の更新は「権限不足」で失敗
    - 試したのが個人向けアプリだったため？ 原因は未追求

# おまけ

## POSTとPUTとPATCH

今回初めてPATCHメソッドの存在を知った。

- POST ... リソースの新規追加
- PUT ... リソースの更新（置換）
- PATCH ... リソースの部分更新（アップデート）

## カスタム属性の利用

ユーザーの情報としてカスタム属性を利用する場合には、「Azure AD B2C ディレクトリ」を利用する必要がある。 （無料体験のライセンスでは利用できない）


# Graph API beta の利用（2019.11.16追記)

ここまでは v1.0 を利用していたが、beta ではより多くの属性が扱えることが判明したので使ってみた。

- アクセストークンの取得まではv1.0と同じ

## betaでのユーザー情報の取得

v1.0と同様に、GETでユーザーの情報を取得。URLの「v1.0」の箇所を「beta」にするのが違い。

- 先ほどのアクセストークンの値と、取得するユーザーのオブジェクトIDを指定する

```text:curlで取得してファイルに保存する例
curl -H "Authorization: Bearer {access_tokenの値}" \
 https://graph.microsoft.com/beta/users/{取得するユーザーのオブジェクトID} > user_beta.json
```

```json:betaのユーザー情報の例
{
  "@odata.context": "https://graph.microsoft.com/beta/$metadata#users/$entity",
  "id": "xxxxxxxxxxxxxxxxxxxxxxxx",
  "deletedDateTime": null,
  "accountEnabled": true,
  "ageGroup": null,
  "businessPhones": [],
  "city": null,
  "createdDateTime": "2019-11-03T09:34:59Z",
  "creationType": null,
  "companyName": "GOOD COMPANY",
  "consentProvidedForMinor": null,
  "country": null,
  "department": null,
  "displayName": "Joe Test",
  "employeeId": null,
  "faxNumber": null,
  "givenName": null,
  "imAddresses": [],
  "isResourceAccount": null,
  "jobTitle": "test user",
  "legalAgeGroupClassification": null,
  "mail": null,
  "mailNickname": "joe",
  "mobilePhone": null,
  "onPremisesDistinguishedName": null,
  "officeLocation": null,
  "onPremisesDomainName": null,
  "onPremisesImmutableId": null,
  "onPremisesLastSyncDateTime": null,
  "onPremisesSecurityIdentifier": null,
  "onPremisesSamAccountName": null,
  "onPremisesSyncEnabled": null,
  "onPremisesUserPrincipalName": null,
  "otherMails": [],
  "passwordPolicies": null,
  "postalCode": null,
  "preferredDataLocation": null,
  "preferredLanguage": null,
  "proxyAddresses": [],
  "refreshTokensValidFromDateTime": "2019-11-03T09:34:58Z",
  "showInAddressList": null,
  "signInSessionsValidFromDateTime": "2019-11-03T09:34:58Z",
  "state": null,
  "streetAddress": null,
  "surname": null,
  "usageLocation": null,
  "userPrincipalName": "joe@xxxxxxx.onmicrosoft.com",
  "externalUserState": null,
  "externalUserStateChangeDateTime": null,
  "userType": "Member",
  "assignedLicenses": [],
  "assignedPlans": [],
  "deviceKeys": [],
  "identities": [
    {
      "signInType": "userPrincipalName",
      "issuer": "xxxxxxxxx.onmicrosoft.com",
      "issuerAssignedId": "joe@mxxxxxx.onmicrosoft.com"
    }
  ],
  "onPremisesExtensionAttributes": {
    "extensionAttribute1": null,
    "extensionAttribute2": null,
    "extensionAttribute3": null,
    "extensionAttribute4": null,
    "extensionAttribute5": null,
    "extensionAttribute6": null,
    "extensionAttribute7": null,
    "extensionAttribute8": null,
    "extensionAttribute9": null,
    "extensionAttribute10": null,
    "extensionAttribute11": null,
    "extensionAttribute12": null,
    "extensionAttribute13": null,
    "extensionAttribute14": null,
    "extensionAttribute15": null
  },
  "onPremisesProvisioningErrors": [],
  "passwordProfile": {
    "password": null,
    "forceChangePasswordNextSignIn": true,
    "forceChangePasswordNextSignInWithMfa": false
  },
  "provisionedPlans": []
}
```

v1.0と比べて、取得できる属性がかなり多い。

```json:v1.0ユーザー情報の例
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "businessPhones": [],
  "displayName": "Joe Test",
  "givenName": null,
  "jobTitle": null,
  "mail": null,
  "mobilePhone": null,
  "officeLocation": null,
  "preferredLanguage": null,
  "surname": null,
  "userPrincipalName": "joe@xxxxxxxxx.onmicrosoft.com",
  "id": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```

## betaでのユーザー情報の一部更新

betaで増えた属性の更新もURLの「v1.0」の箇所を「beta」にするとできる。

```
curl --request PATCH https://graph.microsoft.com/beta/users/{更新するユーザーのオブジェクトID} \
 -H "Authorization: Bearer {access_tokenの値}" \
 -H "Content-Type: application/json" \
 -d '{ "jobTitle": "beta test user", "onPremisesExtensionAttributes": {"extensionAttribute2": "extra attribute test"} }'
```

更新後に再度ユーザー情報を取得すれば、指定した属性が更新されている。カスタム属性を使わなくても、extensionAttribute1 〜 extensionAttribute15 が利用できるので情報を追加で保持するのに使えそう。