[Microsoft Graph] Azure ADでユーザー削除を検知してみる [変更通知サブスクリプション]

2022.04.06

Microsoft Azure ADを使ってログインするようにAuth0を設定していたのですが、 Azure ADからユーザーを削除したときにAuth0の方に連携されて登録されたユーザー情報は削除されませんでした。 Auth0側のユーザー削除も一緒に行うにはどうしたらいいか調べていたところ、 Microsoft Graphにある変更通知を配信するAPIを使えばできそうなので、実際に試してみようかと思います。

やってみる

変更通知サブスクリプションの作成の方法は以下のラーニングサイトに沿って行っていくことにします、

Microsoft Graph の変更通知

変更通知サブスクリプションはメカニズムを使用するとのことなので、変更通知を受け取るためのアプリケーションの作成が必要になります。

サポートされているリソース

Azure ADアプリの登録、構成

変更通知を受信するためのアプリケーションが必要になってきます。

Microsoft Graph から変更通知を受信するための最初のステップは、Azure AD アプリケーションを登録し、必要なアクセス許可を使用して構成することです

なので、

Azureポータル に移動して作成していきます。

Azure Active Directory に新しいアプリケーション登録を追加する を参照

アプリには、クエリを実行して変更通知で使用するデータセットを返すために必要なアクセス許可が必要 なので、

今回やろうとしているユーザー削除の通知を受け取るにはUser.Read.Allが必要ですので、先ほど登録したアプリケーションの設定から追加しました。

アクセス許可

Azure Active Directory でアプリ登録のアクセス許可を委任する

usersエンドポイントに対する変更に基づく変更通知サブスクリプションを作成する

サブスクリプションの作成には、

https://graph.microsoft.com/v1.0/subscriptions に対してHTTP POSTでリクエストを行います。

本文にサブスクリプションの詳細を含めます。

リクエスト例)

POST https://graph.microsoft.com/v1.0/subscriptions HTTP/1.1
Authorization: bearer <<アクセストークン>>
Content-Type: application/json; charset=utf-8
Host: graph.microsoft.com
Content-Length: 199

{
  "changeType": "deleted",
  "clientState": "<<SecretClientState>>",
  "notificationUrl": "<<変更通知を受信するエンドポイントのURL>>",
  "resource": "/users",
  "expirationDateTime": "2020-03-11T04:30:28.2257768+00:00"
}
  • changeType(必須)
    • 登録しているリソース内の、変更通知を上げる変更の種類を示します。サポートされている値は created、updated、deleted です
  • notificationUrl(必須)
    • 変更通知を受信するエンドポイントのURLです
  • expirationDateTime(必須)
    • Webhook サブスクリプションの有効期限が切れる日時を指定
  • resource(必須)
    • 変更の監視対象となるリソースを指定
  • clientState
    • 各変更通知と共に受信されたclientStateプロパティの値を比較することで、その変更通知がサービスから来たことを確認できます

clientState は必須ではありませんが、推奨される変更通知の処理プロセスに準拠するには含める必要があります。 このプロパティを設定すると、受け取る変更通知が Microsoft Graph サービスから来たものであることを確認することができます。

※ 有効期限について

変更通知サブスクリプションには有効期限があります。 ほとんどのリソースでは、サブスクリプションの最長期間は 3 日となっていますが、リソースごとにサポートされているサブスクリプションの最長期間を確認してください。 この期間が過ぎると、サブスクリプションは自動的に Microsoft Graph から消去されます。 つまり、サブスクリプションの作成後にアプリケーションで何の対策もとらなければ、変更通知を受けるのは指定された有効期限までのみとなります。

とあります。今回のユーザーリソースに関しては、

リソースの種類別のサブスクリプションの最大の長さ をみると41760 分 (29 日以内)となっていました。

有効期限が切れるとサブスクリプションを受信できなくなるので、

アプリケーションで受信する変更通知のそれぞれで、サブスクリプションの有効期限のタイムスタンプを確認し、 有効期限が切れるまでの期間が特定の時間枠に入った場合、アプリケーションでは変更通知を処理するだけでなく、サブスクリプションを更新をする

といったことを行う必要がありますね。

実際にリクエストしたところ、

{
    "error": {
        "code": "InvalidRequest",
        "message": "Subscription validation request failed. Notification endpoint must respond with 200 OK to validation request.",
        "innerError": {
            "date": "2022-04-06T01:25:39",
            "request-id": "25338778-f020-41fa-a9fe-108f2f549e42",
            "client-request-id": "25338778-f020-41fa-a9fe-108f2f549e42"
        }
    }
}

といったエラーが返ってきました。 これは、サブスクリプション登録のエンドポイントが

サブスクリプションが作成されるとすぐに、Microsoft Graph はサブスクリプションに登録されているエンドポイントに HTTP POST を送信します。 サブスクリプション要求で指定したエンドポイントは、5 秒以内に Microsoft Graph に応答して、サブスクリプション エンドポイントが有効であり、 動作中であることを通知する必要があります。 要求に含まれる URL に、クエリ パラメーターとして値が組み込まれています。 確認応答では、クエリ パラメーターからこの値を取り、応答の本文に文字列として含めて返す必要があります。

といった動きをするためで、変更通知を受信するためのアプリケーションにこのような処理を実装する必要が出てきます。

そして、Microsoft Graphからのリクエストを受け取れるようにHTTPのエンドポイントを立てないといけませんね。

アプリケーションの作成

変更通知を受信するためのアプリケーションを作る必要があります。

以下のことを意識して実装していく必要があるとのことです

  • 変更通知サブスクリプション要求をリッスンして応答する
    • サブスクリプションエンドポイントが有効であり、動作中であることを通知する必要があるため
  • サブスクリプションライフサイクルを管理する
    • 変更通知サブスクリプションには有効期限があり、その有効期限が切れていないこと、または有効期限が切れるまでに一定の猶予があることを確認するプロセスが必要であるため
    • サブスクリプションの有効期限が切れた場合は、新しいサブスクリプションを作成 / 期限切れになっていない限り既存のサブスクリプションを事前に更新
  • サブスクリプションを受信した後の独自の処理
    • Azure ADのユーザーでログインしたときに作成されるAuth0のユーザー情報を削除する など

全て試したわけではありませんが、今回はローカルPCにnodejsの受信用のアプリを作成しました。

Content-Type: text/plain; charset=utf-8
POST https://{notificationUrl}?validationToken={opaqueTokenCreatedByMicrosoftGraph}

のようにエンドポイントに送られてくるため、

expressを使ってPOSTリクエストを受け取るようにしてみます。

  • status code: HTTP 200 OK。
  • content-type: text/plain。
  • body: URLデコード済み検証トークンを含む本文。 validationTokenクエリパラメーターで送信された同じ文字列を反映します。

サンプルコード

<br />const express = require('express');
const app = express();

app.post('/subscriptions', (req, res) => {
    if(req.query.validationToken !== undefined) {
        res.contentType('text/plain');
        res.send(req.query.validationToken);
        return
    }
});

このコードを実行し、

node hook.js

Microsoft Graphからのリクエストが受け付けられるようにします。

今回はngrokを使用してlocalhostのアプリを外部からアクセスできるようにしました。

ngrok http 3000

もう一度変更通知サブスクリプションの作成リクエストを送信

ngrokを実行してForwardingされているURLで再度 POST https://graph.microsoft.com/v1.0/subscriptions を行って登録します。

正しく動作すると正しく動作すると

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity",
    "id": "1585bd55-4060-414a-84a0-113d5c011092",
    "resource": "users",
    "applicationId": "02ce2f3a-c922-432b-a632-0d0ff3ac11aa",
    "changeType": "updated,deleted",
    "clientState": "hogehogehoge",
    "notificationUrl": "https://de90-240f-6c-ee-1-52d4-00-1031.ngrok.io/subscriptions",
    "notificationQueryOptions": null,
    "lifecycleNotificationUrl": null,
    "expirationDateTime": "2022-04-11T04:30:28.2257768Z",
    "creatorId": "68f0fe16-e812-49a0-b813-6ad86d1e6d35",
    "includeResourceData": null,
    "latestSupportedTlsVersion": "v1_2",
    "encryptionCertificate": null,
    "encryptionCertificateId": null,
    "notificationUrlAppId": null
}

このような感じのjsonが返却されます.

これで変更通知を受け取る準備ができました。

色々と動作検証

validationTokenが送られてこない場合のリクエストでは、以下のようなpayloadが送信されてきました。

{
  value: [
    {
      changeType: 'deleted',
      clientState: 'hogehogehoge',
      resource: 'Users/539277ec-b3b9-4a6c-b398-5914b70bb109',
      resourceData: [Object],
      subscriptionExpirationDateTime: '2022-04-10T21:30:28.2257768-07:00',
      subscriptionId: 'c17e0871-9bee-4c97-9219-6d27db2ccc15',
      tenantId: 'ea466916-8101-4613-800a-c3c6b47d1140'
    }
  ]
}

changeTypeにはサブスクリプションを作成するときに指定したものと同じになっています。

通知Payloadについて

このpayloadの情報を使い、後続の処理に使っていくことになります。

body-parserを使ってpayloadを受け取るサンプルコード

<br />const express = require('express');
const app = express();

const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(bodyParser.json());

app.post('/subscriptions', (req, res) => {
    if(req.query.validationToken !== undefined) {
        res.contentType('text/plain');
        res.send(req.query.validationToken);
        return
    }

    const payload = req.body.value[0]

    console.log(payload)
    res.send('user deleted event')
});

設定したclientStateのチェックなら以下のように行えそうです。

if(payload.clientState !== process.env.CLIENTSTATE){
        res.status(500).send('Something broke!')
        return
}

ユーザーの情報など、resourceDataの中に変更のあったリソースに関してのデータが格納されています。

userのイベントを実行した時のresourceData例)

resourceData: {
    '@odata.type': '#Microsoft.Graph.User',
    '@odata.id': 'Users/d94cab0b-xxxx-xxxx-xxxx-xxxxxxxx',
    id: 'd94cab0b-xxxx-xxxx-xxxx-xxxxxxxx',
    organizationId: 'ea466916-xxxx-xxxx-xxxx-xxxxxxxxxxx',
    sequenceNumber: 637848184437509400
}

idがユーザーを特定する情報として使えます。

外部に連携している場合などは、このidを使ってユーザーの検索を行ったり、削除したりができますね。

oidでAuth0のユーザーを検索する例)

※ app_metadata にoidを格納しています(Auth0のAPIではoid属性での検索ができない)

const options = {
        url: 'https://<<Auth0_DOMAIN>>/api/v2/users',
        method: 'GET',
        headers: {
            "Authorization": "Bearer " + process.env.auth_token
        },
        qs: {
            "q": "app_metadata.oid:" + payload.resourceData.id
        }
}

request(options, function (error, response, body) {
        console.log(body);
})

changeTypeがdeletedの時にハマった点

Azureポータルからユーザー一覧画面で削除しても通知されませんでした。

updatedイベントとしても通知が送られてはきませんでした。

最初の削除は、完全に削除されているわけではないので(ユーザーの復元ができる)、deletedの通知がされるには完全に削除をする必要がありました。