【GraphAPI】リソースの変更を検知する (1)：Webhook 変更通知 

# はじめに

Microsoft Graph 変更通知を利用すると、テナント内のリソースが変更された際に通知を受信することができます。この記事では、Webhook による変更通知について検証した内容をまとめてみました。

# リソースが変更されたことを検知する 2 つの方法

Graph API でリソースが変更されたことを検知する方法には、以下の 2 つの方法があります。

* 変更通知
  リソースが変更されたタイミングで即時にクラウド側から通知を受け取る方法です
  クラウド側からの通知の送信先となる通知エンドポイントを用意する必要があります
* デルタ クエリ
  クライアント アプリケーションからクラウド側へリクエストを送信して、変更されたリソースの情報を取得する方法です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/81859777-b912-4980-8ce5-cf44fa95b1bb.png)

| 項目 | 変更通知 | デルタ クエリ |
|:-:|:-:|:-:|
| 検知方法 | プッシュ | プル |
| 検知タイミング | 即時 | ポーリングの間隔 | 
| 通知エンドポイント | 必要 | 不要 | 
  
※ プッシュ / プル
**"プッシュ"** はクラウド側から情報を送信する方式 、**"プル"** はクライアントからクラウド側へリクエストを送信して情報を取得する方式です。

※ ポーリング
プル方式の場合に、クライアントが一定間隔でクラウド側へリクエストを送信し情報を取得する方法です。

この記事では、変更通知についてまとめています。デルタ クエリについては、以下の記事で紹介しています。

https://qiita.com/atsmin77/private/1c175906f1878ba67799

(参考：リソースデータの変更に関する通知を設定する)
https://learn.microsoft.com/ja-jp/graph/change-notifications-overview

(参考：デルタ クエリを使用して、Microsoft Graph データの変更を追跡する)
https://learn.microsoft.com/ja-jp/graph/delta-query-overview

# Microsoft Graph 変更通知

## 変更通知をサポートしているリソース

変更通知をサポートしているリソースは、以下の公開情報に記載されています。OneDrive 上のファイル、グループ、Teams チーム、Outlook や Teams のメッセージなどのリソースがサポートされています。

(参考：サポートされているリソース)
https://learn.microsoft.com/ja-jp/graph/change-notifications-overview#supported-resources

## 変更通知を受信する方法

変更通知を受信するためには、以下の手順が必要です。

1. 通知エンドポイントの準備
   変更通知を受信するアプリケーションと通知エンドポイントを用意します
1. サブスクリプションの作成
   1 の通知エンドポイントを指定してサブスクリプション Graph API を実行し、変更通知を有効化します
1. 有効期限の更新
   サブスクリプション Graph API でサブスクリプションを定期的に更新します

::: note
変更通知を有効化している状態は **"サブスクリプション"** としてクラウド側で管理されています。
:::

::: note warn
サブスクリプションには有効期限があるので、定期的に更新する必要があります。
:::

(参考：サブスクリプション ライフタイム)
https://learn.microsoft.com/ja-jp/graph/api/resources/subscription?view=graph-rest-1.0#subscription-lifetime

変更通知では通知を行う方法は、Webhook / Azure EventHub / Azure EventGtid から選択できます。

## テナントにチームが作成された際に Webhook 変更通知を受信する例

例として、テナントにチームが作成された際の Webhook による変更通知を有効化してみます。

(参考：Webhook を使用して変更通知を受信する)
https://learn.microsoft.com/ja-jp/graph/change-notifications-delivery-webhooks?tabs=http

::: note
**"Webhook"** では、リソースに変更があった際にクラウド側から通知エンドポイントに対して HTTPS で通知が行われます。 送信元の IP アドレスは公開情報の **[Microsoft Graph の変更通知]** に記載されています。
:::

(参考：Microsoft 365 IP アドレスと URL Web サービスに含まれていないその他のエンドポイント)
https://learn.microsoft.com/ja-jp/microsoft-365/enterprise/additional-office365-ip-addresses-and-urls?view=o365-worldwide

### 1. 通知エンドポイントの準備

Webhook 変更通知を利用するためには、クラウド側からの HTTPS リクエストを受信することのできるエンドポイントが必要です。今回は Azure Logic Apps を使用してみます。

::: note 
PowerAutomate でも Logic Apps と同様にエンドポイントを作成することができます。
:::

::: note warn
PowerAutomate の場合には PowerAutomate Premium のライセンスが必要です。
:::

#### 1.1 Logic Apps の作成

Azure ポータルから以下の手順で Logic Apps を作成します。

1. **[リソースの作成]** から検索ボックスに "logic apps" と入力して、**[ロジック アプリ] - [作成]** をクリックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/911bb9c2-df8f-4749-a705-9f5e94b7588a.png)
1. 今回は検証なので **[マルチテナント]** を選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/38cbfc73-6c3b-4c49-a3cd-ad5410a7569e.png)
1. リソースグループ、名前、リージョンを入力して **[確認および作成]** からリソースを作成します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/3d08eadd-bda7-4da8-8e8c-cd160bebeb65.png)

#### 1.2 Logic Apps の編集

作成した Logic Apps の **[概要] - [Edit]** から Logic Apps を編集します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/aaf9f8b2-fb2d-4fed-9379-f14dc1b90714.png)

今回は以下のようなワークフローを作成して、チームが作成された変更通知を受信したら管理者にメールで通知するようにしてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/0eb123d6-0269-4b19-8708-331a25ca602c.png)

フローを保存すると生成される **[When a HTTP request is received]** の **[HTTP URL]** が通知エンドポイントとして使用する URL です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/365cffed-2d72-4f6e-873c-2f2ddfed5db7.png)

### フローの解説

::: note
Webhook 変更通知を正しく受信するためには、エンドポイントで以下の処理を行う必要があります。

* 検証トークン ("validationToken") を応答する処理
  クラウド側がエンドポイントを検証するために必要です
* "clientState" を検証する処理
  エンドポイントが送信されたデータを検証するために必要です
:::

#### 検証トークン ("validationToken") を応答する処理

変更通知のサブスクリプションを作成する際に、クラウド側からエンドポイントの検証が行われます。

変更通知を有効化するためには、エンドポイントはクラウド側から送信された **"validationToken"** を Body に含めて応答する必要があります。

この例では **Response** アクションの **[Body]** で以下のように設定しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/b4ce650e-d0d9-452b-a8b9-043b99161185.png)

```
triggerOutputs()?['queries']?['validationToken']
```

(参考：notificationUrl 検証)
https://learn.microsoft.com/ja-jp/graph/change-notifications-delivery-webhooks?tabs=http#notificationurl-validation

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/9cfd9bfb-afd9-4a74-9792-00a16faf6e1a.png)

#### "clientState" を検証する処理

送信データの検証は送信データに含まれる **"clientState"** を確認することで行います。

変更通知を有効化する際には、任意の文字列を "clientState" としてクラウド側へ送信します。クラウド側から送信された正規の変更通知には、有効化する際に送信した "clientState" が含まれています。

エンドポイントはこの値をチェックすることで、送信データを検証することができます。

::: note warn
Webhook 変更通知では、エンドポイントの URL が分かれば誰でもエンドポイントに対して通知を送信することができてしまいます。そのため、エンドポイントでも通知されたデータの正当性を検証します。
:::

この例では条件コントロールで "clientState" の値をチェックして、一致している場合だけ処理を行うようにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/9cf25f0f-0bec-4159-8ffa-7315a74eec1e.png)

送信された clientState の内容は以下のように取得しています。

```
triggerBody()?['value'][0]?['clientState']
```

(参考：変更通知の処理)
https://learn.microsoft.com/ja-jp/graph/change-notifications-delivery-webhooks?tabs=http#processing-the-change-notification
>変更通知を受け取ったとき:
clientState プロパティを検証します。 これは、サブスクリプション作成要求で当初送られた値に一致していなければなりません。

### 2. サブスクリプションの作成

通知を受信するエンドポイントの準備ができたら、次に サブスクリプション作成 Graph API を実行して、変更通知を有効化します。

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

サブスクリプションを作成するために必要なアクセス許可は、通知対象とするリソースごとに異なります。公開情報の **[アクセス許可]** セクションに記載されています。

(参考：サブスクリプションを作成する)
https://learn.microsoft.com/ja-jp/graph/api/subscription-post-subscriptions?view=graph-rest-1.0&tabs=http

テナント内でチームが作成されたこと場合に通知を受信するためには、[アプリケーション許可権限](https://qiita.com/atsmin77/items/6bcc9fb88b305e124399) で "Team.ReadBasic.All" を付与する必要があります。

#### 2.2 サブスクリプション作成 API の実行

今回は [Graph PowerShell](https://qiita.com/atsmin77/items/264d10bd9d7e9dfeaace) を使用して実行してみます。

```powershell
$params = @{
   "changeType" = "<通知する変更内容>"
   "notificationUrl" = "<通知用エンドポイントの URL>"
   "resource" = "<サブスクリプションの対象とするリソース>"
   "expirationDateTime" = "<サブスクリプションの有効期限>"
   "clientState" = "<送信データの検証に使用する任意の文字列>"
   "lifecycleNotificationUrl" = "<通知用エンドポイントの URL>"
}

Invoke-MgGraphRequest -Method POST -Uri "/v1.0/subscriptions" -Body $params
```

| パラメータ | 説明 |
|:-:|:-:|
| changeType | 通知する変更内容を設定します。<br>作成、変更、削除のすべてを対象とする場合には "created,updated,deleted" と指定します。 |
| notificationUrl | 通知エンドポイントの URL を指定します。  |
| resource | 通知対象とするリソースを指定します。 |
| expirationDateTime | サブスクリプションの有効期限を指定します。<br>指定できる有効期間の上限は[リソースごとに異なります](https://learn.microsoft.com/ja-jp/graph/api/resources/subscription?view=graph-rest-1.0#subscription-lifetime)。 |
| clientState | 送信データの検証に使用する任意の文字列を指定します。 |
| lifecycleNotificationUrl | サブスクリプション期限切れ通知エンドポイントの URL を指定します。  |

::: note
現在時刻よりも 1 時間以上先の時刻を expirationDateTime で指定する場合、lifecycleNotificationUrl も併せて指定する必要があります。notificationUrl と同じ URL を指定することもできます。
:::

(参考：サブスクリプションと変更通知の消失を減らす)
https://learn.microsoft.com/ja-jp/graph/change-notifications-lifecycle-events?tabs=http

※ テナント内にチームが作成されたことを通知する場合の例

```powershell
$params = @{
   "changeType" = "created"
   "notificationUrl" = "https://prod-35.japaneast.logic.azure.com:443/workflows/b808459406594636bb0cc374f09052af/triggers/When_a_HTTP_request_is_received/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2FWhen_a_HTTP_request_is_received%2Frun&sv=1.0&sig=O2k1b1MAUv1sWRO8Gr6ZBj8FSel86vwRHsJARYwnxIw"
   "resource" = "teams"
   "expirationDateTime" = "2025-05-29T10:00:00Z"
   "clientState" = "secretClientValue"
   "lifecycleNotificationUrl" = "https://prod-35.japaneast.logic.azure.com:443/workflows/b808459406594636bb0cc374f09052af/triggers/When_a_HTTP_request_is_received/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2FWhen_a_HTTP_request_is_received%2Frun&sv=1.0&sig=O2k1b1MAUv1sWRO8Gr6ZBj8FSel86vwRHsJARYwnxIw"
}

Invoke-MgGraphRequest -Method POST -Uri "/v1.0/subscriptions" -Body $params
```

※ 実行結果の例

```
Name                           Value
----                           -----
encryptionCertificate
expirationDateTime             2025/05/29 10:00:00
resource                       /teams
creatorId                      7ebe5f0c-4fef-46da-a384-fb06b4956101
@odata.context                 https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity
changeType                     created
id                             baf93079-4e9b-4b50-a1a9-127acefb3b13
notificationUrlAppId
lifecycleNotificationUrl       https://prod-35.japaneast.logic.azure.com:443/workflows/b808459406594636bb0cc374f0905...
latestSupportedTlsVersion      v1_2
encryptionCertificateId
includeResourceData
notificationQueryOptions
notificationUrl                https://prod-35.japaneast.logic.azure.com:443/workflows/b808459406594636bb0cc374f0905...
applicationId                  49828373-a90b-416b-9660-e122b92e412c
clientState                    secretClientValue
```

サブスクリプションを作成した後、任意のユーザーで Teams チームを作成すると、エンドポイントに対して通知が送信されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/80de3089-acbb-40e3-9ecd-f6caf0a85dee.png)

Logic Apps に設定したワークフローによって管理者にメールが送信されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2680397/98d36e26-81c6-4a03-be6d-653c899ae82d.png)

通知された内容には作成したチームの ID が含まれています。この ID を使って Graph API でチームの情報を取得する処理を追加すれば、チームの詳細情報もメールに含めることができます。

::: note
**多機能な通知 (Rich Notification)** という機能を利用すれば、通知される内容にリソースの詳細情報を含めることができます。
:::

::: note warn
Rich Notification の通知に含まれるリソースの情報は暗号化されているため、エンドポイントで復号する処理を作り込む必要があります。
:::

(参考：リソース データを使用して Microsoft Graph の変更通知を設定する)
https://learn.microsoft.com/ja-jp/graph/change-notifications-with-resource-data?tabs=csharp

### 3. 有効期限の更新

サブスクリプションには有効期限があります。有効期限が切れると通知が行われなくなります。

#### サブスクリプションの更新

サブスクリプションの有効期限は以下のようなリクエストで更新することができます。

```powershell
$params = @{
   "expirationDateTime" = "<更新後の有効期限>"
}
Invoke-MgGraphRequest -Method PATCH -Uri "/v1.0/subscriptions/<サブスクリプションの ID>" -Body $params
```

※ 実行例

```powershell
$params = @{
   "expirationDateTime" = "2025-05-29T11:00:00Z"
}
Invoke-MgGraphRequest -Method PATCH -Uri "/v1.0/subscriptions/2514641c-defc-4837-8078-2489438a37e9" -Body $params
```

(参考：サブスクリプションを更新する)
https://learn.microsoft.com/ja-jp/graph/change-notifications-delivery-webhooks?tabs=http#renew-a-subscription

#### reauthorizationRequired 通知

::: note
サブスクリプションの有効期限が切れる 1 ~ 3 時間前に、lifecycleNotificationUrl に指定したエンドポイントへ 以下のような reauthorizationRequired 通知が届きます。
:::

※ reauthorizationRequired 通知の例

```json
"body": {
    "value": [
      {
        "subscriptionId": "a88e90f3-4dd1-4cb8-a881-e6978abda6b8",
        "resource": "Subscriptions/a88e90f3-4dd1-4cb8-a881-e6978abda6b8",
        "clientState": "secretClientValue",
        "resourceData": {
          "@odata.type": "#microsoft.graph.subscription",
          "@odata.id": "subscriptions/a88e90f3-4dd1-4cb8-a881-e6978abda6b8",
          "id": "a88e90f3-4dd1-4cb8-a881-e6978abda6b8"
        },
        "encryptedContent": null,
        "organizationId": "ac2852a6-88d6-4ddc-9a63-93391e191144",
        "subscriptionExpirationDateTime": "2025-06-01T01:00:00-07:00",
        "lifecycleEvent": "reauthorizationRequired"
      }
    ]
  }
```

(参考：reauthorizationRequired 通知)
https://learn.microsoft.com/ja-jp/graph/change-notifications-lifecycle-events?tabs=http#reauthorizationrequired-notifications