SharePoint Webhook検証(2): Webhookレシーバーの設定

### はじめに

前回の記事 [SharePoint Webhook検証(1): Webhook作成＆通知フロー](https://qiita.com/mingchun_zhao/items/cebb64512a1ccbd42027) では、
SharePoint Webhookの仕組みおよび作成＆通知フローを確認しました。
今回は、Webhookレシーバーの要件と実装について確認します。

### Webhookの作成＆通知フローのおさらい

- Webhook作成フロー

```plantuml
@startuml

actor "管理者" as admin #lightskyblue
control "Webhookレシーバー" as recv #lightgreen
participant "SharePoint" as sp #hotpink

|||
admin -> sp : サブスクリプションの作成リクエストを送信
deactivate admin
|||
activate sp
sp -> recv : 検証トークンが含まれたリクエストを送信
deactivate sp
|||
activate recv
recv --> sp : 同じ検証トークンを返却
deactivate recv
|||
activate sp
sp --> admin : サブスクリプションの作成結果を返す
deactivate sp
@enduml
```

- Webhook通知フロー
```plantuml
@startuml

actor "ユーザ" as user #lightpink
actor "管理者" as admin #lightskyblue
control "Webhookレシーバー" as recv #lightgreen
participant "SharePoint" as sp #hotpink
|||
activate user
user -> sp : SharePoint上でドキュメントを作成・更新・移動
deactivate user
|||
activate sp
sp -> recv : ドキュメントが更新されたとイベント通知
deactivate sp
|||
activate recv
recv --> sp : イベント通知を受け取りましたと返事
deactivate recv
recv -> admin : Webhook変更通知が管理者に届く
activate admin
admin -> admin : 変更通知に対して、必要なアクションを行います
|||
@enduml
```

### Webhookレシーバー(Receiver)の役割

上のシーケンス図で示しているように、Webhookレシーバーは、SharePointとユーザ・管理者の間で仲介役を担います。
SharePointからの変更通知を、一旦Webhookレシーバーが受け取り、管理者に情報を渡すなど制御を行います。

### Webhookレシーバーの要件

Webhookレシーバーは、以下二つの機能を持つ必要があります。

- Webhook作成時の検証機能
    - SharePointからのリクエストから`validationToken`の値を取得
    - `validationToken`の値をレスポンスに含め、SharePointに返す
- Webhook通知を受け取りハンドリング
    - SharePointからの更新通知を受信したら、レスポンスを返す
    - 受信イベントの内容を記録または管理者に渡す

### Webhookレシーバーの実装

Webhookレシーバーの実装はいくつか方法が考えられます。

- Microsoftチュートリアルでは、Visual Studio Web APIを使用しています

https://learn.microsoft.com/ja-jp/sharepoint/dev/apis/webhooks/get-started-webhooks#step-2-build-a-webhook-receiver

- 本記事では、NGINX(エンジンエクス)を使用して、Webhookレシーバーを作成します

Wikipediaから、NGINXとは、

https://ja.wikipedia.org/wiki/Nginx

- フリーかつオープンソースなWebサーバ
- 処理性能・高い並行性・メモリ使用量の小ささに焦点を当てて開発された
- HTTP, HTTPS, SMTP, POP3, IMAPのリバースプロキシの機能を持つ
- ロードバランサ、HTTPキャッシュなどの機能も持つ


### NGINXを用いてWebhookレシーバーを作成する手順

- 必要なNGINX拡張モジュール
    - [ngx_http_js_module.so](http://nginx.org/en/docs/http/ngx_http_js_module.html)
        - ロケーションと変数ハンドラーの実装にnjsスクリプト言語を使用するため必要
    - [ngx_http_echo_module.so](https://github.com/openresty/echo-nginx-module#readme)
        - リクエストのヘッダーとボディ情報をNGINXアクセスログに記録するため必要

- Webhookレシーバーの処理を行うnjsスクリプト

```webhook.js
import qs from 'querystring';

// リクエストheaderをJSON形式で取得
function stringify_headers(r) {
    return JSON.stringify(r.headersIn);
}

// リクエストから検証トークンを取得
function get_validation_token(r) {
    var token = r.args["validationToken"];
    return token ? token : '';
}

export default { stringify_headers, get_validation_token };
```

- `nginx.conf`にWebhookレシーバーに必要な設定を追加

```nginx.conf
... ...
# 動的モジュールをロード
load_module modules/ngx_http_echo_module.so;
load_module modules/ngx_http_js_module.so;
... ...
http {
    js_import webhook.js;
    js_set $headers_json webhook.stringify_headers;
    js_set $validation_token webhook.get_validation_token;

    # アクセスログにリクエストのヘッダーとボディを記録
    log_format ltsv escape=none 'request_method:$request_method\t'
                            'uri:$request_uri\t'
                            'status:$status\t'
                            'request_header:$headers_json\t'
                            'request_body:$request_body';
    access_log /var/log/nginx/access.log ltsv;
    ... ...
    server {
       location /sharepoint-webhook {
            # SharePointからのWebhook作成リクエストの場合、検証トークンが含まれる
            if ( $validation_token ) {
                return 200 $validation_token;
            }
            # SharePointからのWebhook通知リクエストの場合、ヘッダーとボディをログに記録
            echo_read_request_body;
            echo $request_body;
        }
```

### Webhook作成の動作確認

- Graph APIを使用して、`Subscription`作成リクエストを送る

```
POST https://graph.microsoft.com/v1.0/subscriptions
ボディ:
{
  "changeType": "updated",
  "notificationUrl": "https://test.net/sharepoint-webhook",
  "resource": "/sites/test.sharepoint.com,zzzzzz/drives/b!xxxxxxxx/root",
  "expirationDateTime": "2023-06-01T18:23:45.9356913Z",
  "clientState": "yyyyyyyyyyyyy"
}
```

- SharePointからのNGINXへ検証リクエストが届く

NGINXのアクセスログ
```
request_method:POST
uri:/sharepoint-webhook?validationToken=Validation%3a+Testing+client+application+reachability+for+subscription+Request-Id%3a+yyyyyyyy
status:200
request_header:{
  "Accept":"text/plain",
  "Content-Type":"text/plain; charset=utf-8",
  "Host":"test.net",
  "Content-Length":"0",
  "Connection":"Keep-Alive"
}
request_body:
```

- NGINXからSharePointへ検証トークンを含めたレスポンスを返却
```
{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity",
    "id": "aaaaaaaaaaaaaaaaaaa",
    "resource": "/sites/test.sharepoint.com,zzzzzz/drives/b!xxxxxxxx/root",
    "applicationId": "bbbbbbbbbb",
    "changeType": "updated",
    "clientState": "yyyyyyyyyyyyy",
    "notificationUrl": "https://test.net/sharepoint-webhook",
    "notificationQueryOptions": null,
    "lifecycleNotificationUrl": null,
    "expirationDateTime": "2023-06-01T18:23:45.9356913Z",
    "creatorId": "cccccccccccccccccccccc",
    "includeResourceData": null,
    "latestSupportedTlsVersion": "v1_2",
    "encryptionCertificate": null,
    "encryptionCertificateId": null,
    "notificationUrlAppId": null
}
```

### Webhook通知の動作確認

- SharePoint上でWebhook対象のドキュメントに対して、コピーなどの操作を行う

- SharePointから変更通知がNGINXに届く

NGINXのアクセスログ
```
request_method:POST
uri:/sharepoint-webhook
status:200
request_header:{
  "Content-Type":"application/json; charset=utf-8",
  "Host":"test.net",
  "Content-Length":"xxx",
  "Connection":"Keep-Alive"
}
request_body:{
  "value":[{
    "subscriptionId":"xxxxxxxxxxxxxxx",
    "clientState":"yyyyyyyyyyyyy",
    "resource":"/sites/test.sharepoint.com,zzzzzz/drives/b!xxxxxxx/root",
    "tenantId":"kkkkkkkkkkkkkk",
    "resourceData":null,
    "subscriptionExpirationDateTime":"2023-06-01T18:23:45.9356913+00:00",
    "changeType":"updated"}]
}
```

### おわりに
SharePointのWebhookレシーバーについて検証してみました。
SharePointドキュメントに対して、予期せぬ追加・変更が行われた場合など、
Webhookを用いて、ほぼリアルタイムで監視できるようになりそうです。