リソース データが含まれる変更通知を設定する

Microsoft Graph を使用すると、アプリは さまざまな配信チャネルを介してリソースの変更通知をサブスクライブして受信できます。 変更されたリソース データ (Microsoft Teams のチャット メッセージや Microsoft Teams のプレゼンス情報のコンテンツなど) を変更通知に含めるようにサブスクリプションを設定できます。 リソース変更データを含む変更通知は、リッチ通知と呼ばれます。 アプリでは、変更されたリソースをフェッチするために別の API 呼び出しを行うことなく、リッチ通知を使用してビジネス ロジックを実行できます。

この記事では、アプリケーションでリッチ通知を設定するプロセスについて説明します。

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

リッチ通知は、次のリソースで使用できます。

注:

アスタリスク (*) でマークされたエンドポイントへのサブスクリプションのリッチ通知は、エンドポイントでのみ使用できます /beta

リソース サポートされているリソース パス 制限事項
Outlook イベント ユーザーのメールボックス内のすべてのイベントに対する変更: /users/{id}/events リッチ通知でプロパティのサブセットのみを返す必要 $select があります。 詳細については、「 Outlook リソースの通知を変更する」を参照してください。
Outlook メッセージ ユーザーのメールボックス内のすべてのメッセージに対する変更: /users/{id}/messages

ユーザーの受信トレイ内のメッセージに対する変更: /users/{id}/mailFolders/{id}/messages
リッチ通知でプロパティのサブセットのみを返す必要 $select があります。 詳細については、「 Outlook リソースの通知を変更する」を参照してください。
Outlook 個人用連絡先 ユーザーのメールボックス内のすべての個人用連絡先に対する変更: /users/{id}/contacts

ユーザーの contactFolder 内のすべての個人用連絡先に対する変更: /users/{id}/contactFolders/{id}/contacts
リッチ通知でプロパティのサブセットのみを返す必要 $select があります。 詳細については、「 Outlook リソースの通知を変更する」を参照してください。
Teams callRecording organization内のすべての録音:communications/onlineMeetings/getAllRecordings

特定の会議のすべての記録: communications/onlineMeetings/{onlineMeetingId}/recordings

特定のユーザーが開催した会議で使用できる通話記録: users/{id}/onlineMeetings/getAllRecordings

特定の Teams アプリがインストールされている会議で使用できる通話記録: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllRecordings *
最大サブスクリプションのクォータ:
  • アプリとオンライン会議の組み合わせごとに: 1
  • アプリとユーザーの組み合わせごとに: 1
  • ユーザーごと (ユーザーが整理したすべての onlineMeetings の記録を追跡するサブスクリプションの場合): 10 個のサブスクリプション。
  • organizationあたり: 合計 10,000 個のサブスクリプション。
  • Teams callTranscript organization内のすべてのトランスクリプト:communications/onlineMeetings/getAllTranscripts

    特定の会議のすべてのトランスクリプト: communications/onlineMeetings/{onlineMeetingId}/transcripts

    特定のユーザーが開催した会議で使用できる通話トランスクリプト: users/{id}/onlineMeetings/getAllTranscripts

    特定の Teams アプリがインストールされている会議で使用できる通話トランスクリプト: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllTrancripts *
    最大サブスクリプションのクォータ:
  • アプリとオンライン会議の組み合わせごとに: 1
  • アプリとユーザーの組み合わせごとに: 1
  • ユーザーごと (ユーザーが整理したすべての onlineMeetings のトランスクリプトを追跡するサブスクリプションの場合): 10 個のサブスクリプション。
  • organizationあたり: 合計 10,000 個のサブスクリプション。
  • Teams チャネル すべてのチームのチャネルに対する変更: /teams/getAllChannels

    特定のチームのチャネルに対する変更: /teams/{id}/channels
    -
    Teams チャット テナント内のすべてのチャットに対する変更: /chats

    特定のチャットに対する変更: /chats/{id}
    -
    Teams chatMessage すべてのチームのすべてのチャネルでチャット メッセージに対する変更: /teams/getAllMessages

    特定のチャネルでのチャット メッセージの変更: /teams/{id}/channels/{id}/messages

    すべてのチャットでのチャット メッセージの変更: /chats/getAllMessages

    特定のチャット内のチャット メッセージに対する変更: /chats/{id}/messages

    特定のユーザーが含まれるすべてのチャット内のチャット メッセージに対する変更: /users/{id}/chats/getAllMessages
    を使用して $select 選択したプロパティのみを返すのをサポートしていません。 リッチ通知は、変更されたインスタンスのすべてのプロパティで構成されます。
    Teams conversationMember 特定のチームのメンバーシップの変更: /teams/{id}/members



    特定のチャットでのメンバーシップの変更: /chats/{id}/members
    -
    Teams onlineMeeting * オンライン会議の変更: /communications/onlineMeetings/?$filter=JoinWebUrl eq '{joinWebUrl} * を使用して $select 選択したプロパティのみを返すのをサポートしていません。 リッチ通知は、変更されたインスタンスのすべてのプロパティで構成されます。
    Teams プレゼンス 1 人のユーザーのプレゼンスに対する変更: /communications/presences/{id} を使用して $select 選択したプロパティのみを返すのをサポートしていません。 リッチ通知は、変更されたインスタンスのすべてのプロパティで構成されます。
    Teams チーム テナント内の任意のチームに対する変更: /teams

    特定のチームへの変更: /teams/{id}
    -

    通知ペイロードのリソース データ

    一般に、この種類の変更通知には、ペイロードに次のリソース データが含まれます。

    • resourceData プロパティにより返される、変更されたリソース インスタンスの ID および種類。
    • そのリソース インスタンスのすべてのプロパティ値。これはサブスクリプションの指定に従って暗号化され、encryptedContent プロパティで返されます。
    • または、リソースによっては、resourceData プロパティで返される特定のプロパティ。 特定のプロパティのみを取得するには、$select パラメーターを使用して、サブスクリプションのリソース URL の一部としてそれらのプロパティを指定します。

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

    リッチ通知は、 基本的な変更通知と同じ方法で設定されます。

    セキュリティを確保するために、Microsoft Graph はリッチ通知で返されるリソース データを暗号化します。 サブスクリプションの作成の一環として、公開暗号化キーを指定する必要があります。 暗号化キーの作成と管理の詳細については、「 変更通知からのリソース データの暗号化解除」を参照してください。

    リッチ通知を含むサブスクリプションを作成するには、次のプロパティを指定 する必要があります

    • includeResourceData: これは、true に設定して、リソース データを明示的に要求する必要があります。
    • encryptionCertificate 。Microsoft Graph がアプリに返すリソース データを暗号化するために使用する公開キーのみを含みます。
    • encryptionCertificateId: 証明書用の独自の識別子です。 この ID は、復号に使用する証明書を照合するために各変更通知で使用します。

    以下の点にご注意ください:

    • 通知エンドポイントの検証の説明に従って、両方の通知エンドポイントを検証します。 両方のエンドポイントで同じ URL を使用する場合は、2 つの検証要求を受け取り、応答する必要があります。

    サブスクリプション要求の例

    Microsoft Teams で作成または更新されるチャネル メッセージをサブスクライブする例は次の通りです。

    POST https://graph.microsoft.com/v1.0/subscriptions
    Content-Type: application/json
    {
      "changeType": "created,updated",
      "notificationUrl": "https://webhook.azurewebsites.net/api/resourceNotifications",
      "resource": "/teams/{id}/channels/{id}/messages",
      "includeResourceData": true,
      "encryptionCertificate": "{base64encodedCertificate}",
      "encryptionCertificateId": "{customId}",
      "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
      "clientState": "{secretClientState}"
    }
    

    サブスクリプションの応答

    HTTP/1.1 201 Created
    Content-Type: application/json
    
    {
      "changeType": "created,updated",
      "notificationUrl": "https://webhook.azurewebsites.net/api/resourceNotifications",
      "resource": "/teams/{id}/channels/{id}/messages",
      "includeResourceData": true,
      "encryptionCertificateId": "{custom ID}",
      "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
      "clientState": "{secret client state}"
    }
    

    サブスクリプション ライフサイクル通知

    既存のサブスクリプションでの変更通知フローは、特定のイベントにより妨げられる場合があります。 サブスクリプション ライフサイクル通知では、継続的なフローを維持するために実行する必要があるアクションが通知されます。 リソース インスタンスへの変更を通知するリソース変更通知とは異なり、ライフサイクル通知はサブスクリプション自体とライフサイクルの現在の状態に関するものです。

    ライフサイクル通知を受信して応答する方法の詳細については、「 不足しているサブスクリプションを減らし、通知を変更する」を参照してください。

    通知の真正性を検証する

    アプリでは多くの場合、変更通知に含まれるリソース データに基づいてビジネス ロジックが実行されます。 各変更通知の真正性を最初に確認しておくことが重要です。 これを行わない場合、第三者が偽の変更通知を用いてアプリに対してなりすましを行い、アプリのビジネス ロジックを不正に実行することができ、セキュリティ インシデントの発生につながる可能性があります。

    リソース データを含まない基本的な変更通知については、「変更通知の処理」の説明に従って、clientState 値に基づいて検証するだけです。 これは、信頼できる Microsoft Graph 呼び出しを後から行ってリソース データにアクセスできるため、問題ありません。そのため、スプーフィングが試行された場合でも、その影響は限定的です。

    リソース データを送信する変更通知の場合は、データを処理する前により徹底した検証を行います。

    このセクションで説明する項目:

    変更通知の検証トークン

    リソース データを含む変更通知には、追加のプロパティ validationTokens が含まれています。このプロパティには、Microsoft Graph によって生成された JSON Web トークン (JWT) の配列が含まれています。 Microsoft Graph では、 配列に項目がある個別のアプリとテナントのペアごとに 1 つのトークンが生成されます。 変更通知には、同じ notificationUrl を使用してサブスクライブしたさまざまなアプリとテナントの項目が混在している可能性があることに注意してください。

    注:Azure Event Hubs を介して配信される変更通知を設定している場合、Microsoft Graph は検証トークンを送信しません。 Microsoft Graph は、notificationUrl を検証する必要はありません。

    次の例では、1 つのアプリおよび 2 つの異なるテナントに対して 2 つのアイテムがこの変更通知には含まれているため、validationTokens 配列には検証が必要なトークンが 2 つ含まれています。

    {
        "value": [
            {
                "subscriptionId": "76619225-ff6b-4489-96ca-4ef547e78b22",
                "tenantId": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
                "changeType": "created",
                ...
            }
        ],
        "validationTokens": [
            "eyJ0eXAiOiJKV1QiLCJhb...",
            "cGlkYWNyIjoiMiIsImlkc..."
        ]
    }
    

    注: 変更通知が配信されたときに送信されるデータの詳細については、changeNotificationCollection を参照してください。

    検証方法

    MSAL を使用して、トークンの検証、または別のプラットフォーム用のサード パーティ製ライブラリの処理に役立ちます。

    次の点に注意してください:

    • 変更通知に対する応答の一部として HTTP 202 Accepted 状態コードを必ず送信するようにします。
    • 検証が失敗した場合でも、変更通知の検証前 (たとえば、後で処理するために変更通知をキューに保存する場合) または検証後 (その場ですぐ検証する場合) に実行します。
    • 変更通知を承認することにより、配信の不必要なリトライを防ぐことができ、悪意を持っている可能性がある者が検証に合格したかどうかを自ら確認できないようにもします。 無効な変更通知は、その通知を承認した後にいつでも無視することもできます。

    特に、validationTokens コレクション内のすべての JWT トークンに対して検証を実行します。 合格しないトークンが含まれる変更通知は不審な通知とみなし、詳しく調査します。

    次の手順を使用して、トークンおよびトークンを生成するアプリを検証します。

    1. トークンの有効期限が切れていないことを検証します。

    2. トークンが改ざんされておらず、予想される機関によって発行されたことを検証しますMicrosoft ID プラットフォーム。

      • 共通の構成エンドポイント https://login.microsoftonline.com/common/.well-known/openid-configuration から、署名キーを取得します。 この構成は、しばらくの間アプリによってキャッシュされます。 署名キーは毎日ローテーションされるため、構成は頻繁に更新される点にご注意ください。
      • これらのキーを使用して、JWT トークンの署名を確認します。

      他の機関によって発行されたトークンを受け入れないでください。

    3. 変更通知をサブスクライブしているアプリ用に発行されたトークンであることを確認します。

      次の手順は、JWT トークン ライブラリでの標準的な検証ロジックの一部であり、通常は 1 つの関数呼び出しとして実行できます。

      • トークン内の "audience" がアプリ ID と一致していることを確認します。
      • 変更通知を受け取るアプリが複数ある場合は、複数の ID について確認する必要があります。
    4. 重要: トークンを生成したアプリが Microsoft Graph 変更通知の発行元を表していることを確認します。

      • トークンの appid プロパティが、想定値である 0bf30f3b-4a52-48df-9a82-234910c4a086 と一致していることを確認します。
      • これにより、Microsoft Graph ではない別のアプリから変更通知が送信されないようにします。

    JWT トークンの例

    検証が必要な JWT トークンに含まれるプロパティの例を次に示します。

    {
      // aud is your app's id 
      "aud": "8e460676-ae3f-4b1e-8790-ee0fb5d6148f",                           
      "iss": "https://sts.windows.net/84bd8158-6d4d-4958-8b9f-9d6445542f95/",
      "iat": 1565046813,
      "nbf": 1565046813,
      // Expiration date 
      "exp": 1565075913,                                                        
      "aio": "42FgYKhZ+uOZrHa7p+7tfruauq1HAA==",
      // appid represents the notification publisher and must always be the same value of 0bf30f3b-4a52-48df-9a82-234910c4a086 
      "appid": "0bf30f3b-4a52-48df-9a82-234910c4a086",                          
      "appidacr": "2",
      "idp": "https://sts.windows.net/84bd8158-6d4d-4958-8b9f-9d6445542f95/",
      "tid": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
      "uti": "-KoJHevhgEGnN4kwuixpAA",
      "ver": "1.0"
    }
    

    例: 検証トークンの検証

    // add Microsoft.IdentityModel.Protocols.OpenIdConnect and System.IdentityModel.Tokens.Jwt nuget packages to your project
    public async Task<bool> ValidateToken(string token, string tenantId, IEnumerable<string> appIds)
    {
        var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
        var openIdConfig = await configurationManager.GetConfigurationAsync();
        var handler = new JwtSecurityTokenHandler();
        try
        {
        handler.ValidateToken(token, new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
            ValidIssuer = $"https://sts.windows.net/{tenantId}/",
            ValidAudiences = appIds,
            IssuerSigningKeys = openIdConfig.SigningKeys
        }, out _);
        return true;
        }
        catch (Exception ex)
        {
        Trace.TraceError($"{ex.Message}:{ex.StackTrace}");
        return false;
        }
    }
    

    変更通知からのリソース データの復号

    変更通知の resourceData プロパティには、リソース インスタンスの基本 ID と型情報のみが含まれています。 encryptedData プロパティには、サブスクリプションで提供されている公開キーを使用して Microsoft Graph により暗号化された完全なリソース データが含まれています。 このプロパティには、確認と復号に必要な値も含まれています。 これを行う理由は、変更通知経由でアクセスする顧客のデータのセキュリティを強化するためです。 顧客データが元の変更通知を傍受したとしても、サード パーティが暗号化を解除できないように秘密キーをセキュリティで保護するのは、お客様の責任です。

    このセクションで説明する項目:

    暗号化キーの管理

    1. 非対称キーのペアを使用して証明書を取得します。

      • Microsoft Graph は証明書発行者を検証せず、暗号化にのみ公開キーを使用するため、証明書を自己署名できます。

      • 証明書の作成、ローテーション、および安全な管理を行うためのソリューションとして、Azure Key Vault を使用します。 キーは、次の条件を満たしている必要があります。

        • キーの種類が RSA である必要があります。
        • キー サイズは、2,048 ビットから 4,096 ビットの間である必要があります
    2. 証明書を Base64 でエンコードされた X.509 形式でエクスポートし、公開キーのみを含めます

    3. サブスクリプションを作成するときに、以下を行います。

      • 証明書がエクスポートされた Base64 でエンコードされたコンテンツを使用して、encryptionCertificate プロパティで証明書を指定します。

      • encryptionCertificateId プロパティで独自の識別子を指定します。

        この識別子を使用することで、受信する変更通知に証明書を一致させ、証明書ストアから証明書を取得することができます。 識別子には、最大 128 文字まで使用できます。

    4. 変更通知処理コードが秘密キーにアクセスしてリソース データを復号できるよう、秘密キーを安全に管理にします。

    キーのローテーション

    秘密キーの漏洩リスクを最小限に抑えるために、非対称キーは定期的に変更します。 次の手順に従って、新しいキーのペアを導入します。

    1. 新しい非対称キーのペアを使用して新しい証明書を取得します。 作成するすべての新しいサブスクリプションでこれを使用します。

    2. 新しい証明書キーを使用して既存のサブスクリプションを更新します。

      • 定期的なサブスクリプション更新の一部としてこの作業を行います。
      • または、すべてのサブスクリプションを列挙して、キーを提供します。 サブスクリプション で PATCH 操作を使用して、encryptionCertificate プロパティと encryptionCertificateId プロパティを更新します。
    3. 以下の点に注意してください。

      • しばらくの間、古い証明書は暗号化に引き続き使用される場合があります。 コンテンツを復号できるよう、アプリは古い証明書と新しい証明書の両方にアクセスできる必要があります。
      • 各変更通知で encryptionCertificateId プロパティを使用して、使用する正しいキーを特定します。
      • 古い証明書は、最近の変更通知で古い証明書への参照が行われなくなった場合にのみ破棄します。

    リソース データの復号

    パフォーマンスを最適化するために、Microsoft Graph では 2 段階の暗号化プロセスを使用しています。

    • このプロセスでは、1 回限りの使用の対称キーが生成され、リソース データの暗号化に使用されます。
    • このプロセスでは、対称公開キー (サブスクライブする際に指定したもの) を使用して対称キーが暗号化され、それが当該サブスクリプションの各変更通知に含められます。

    変更通知の各アイテムで対称キーが異なることを常に想定してください。

    リソース データを復号するには、アプリは各変更通知の encryptedContent のプロパティを使用して手順を逆に進める必要があります。

    1. encryptionCertificateId プロパティを使用して、使用する証明書を特定します。

    2. 秘密キーを使用して、RSA 暗号化コンポーネント (.NET RSACryptoServiceProvider など) を初期化します。

    3. 変更通知内の各アイテムの dataKey プロパティで提供された対称キーを復号します。

      復号アルゴリズムには、最適非対称暗号化パディング (OAEP) を使用します。

    4. 対称キーを使用して、データ内の値の HMAC-SHA256 署名を計算します。

      その署名を dataSignature の値と比較します。 一致しない場合は、ペイロードが改ざんされ、復号化されていないと仮定します。

    5. Advanced Encryption Standard (AES) (.NET AesCryptoServiceProvider など) を用いて対称キー使用し、データ内のコンテンツを復号します。

      • AES アルゴリズムでは、次の復号パラメーターを使用します。

        • パディング: PKCS7
        • 暗号モード: CBC
      • 復号に使用する対称キーの最初の 16 バイトをコピーして、"初期化ベクター" を設定します。

    6. 復号された値は、変更通知のリソース インスタンスを表す JSON 文字列です。

    例: 暗号化されたリソース データが使用されている通知の復号

    チャネル メッセージ内の chatMessage インスタンスの暗号化されたプロパティ値を含む変更通知例を次に示します。 インスタンスは、@odata.id 値によって指定されています。

    {
        "value": [
            {
                "subscriptionId": "76222963-cc7b-42d2-882d-8aaa69cb2ba3",
                "changeType": "created",
                // Other properties typical in a resource change notification
                "resource": "teams('d29828b8-c04d-4e2a-b2f6-07da6982f0f0')/channels('19:f127a8c55ad949d1a238464d22f0f99e@thread.skype')/messages('1565045424600')/replies('1565047490246')",
                "resourceData": {
                    "id": "1565293727947",
                    "@odata.type": "#Microsoft.Graph.ChatMessage",
                    "@odata.id": "teams('88cbc8fc-164b-44f0-b6a6-b59b4a1559d3')/channels('19:8d9da062ec7647d4bb1976126e788b47@thread.tacv2')/messages('1565293727947')/replies('1565293727947')"
                },
                "encryptedContent": {
                    "data": "{encrypted data that produces a full resource}",
            "dataSignature": "<HMAC-SHA256 hash>",
                    "dataKey": "{encrypted symmetric key from Microsoft Graph}",
                    "encryptionCertificateId": "MySelfSignedCert/DDC9651A-D7BC-4D74-86BC-A8923584B0AB",
                    "encryptionCertificateThumbprint": "07293748CC064953A3052FB978C735FB89E61C3D"
                }
            }
        ],
        "validationTokens": [
            "eyJ0eXAiOiJKV1QiLCJhbGciOiJSU..."
        ]
    }
    

    注: 変更通知が配信されたときに送信されるデータの詳細については、changeNotificationCollection を参照してください。

    このセクションでは、復号の各ステージで役立つ、 C# および .NET を使用するコード スニペットをいくつか示します。

    対称キーを復号する

    // Initialize with the private key that matches the encryptionCertificateId.
    RSACryptoServiceProvider rsaProvider = ...;        
    byte[] encryptedSymmetricKey = Convert.FromBase64String(<value from dataKey property>);
    
    // Decrypt using OAEP padding.
    byte[] decryptedSymmetricKey = rsaProvider.Decrypt(encryptedSymmetricKey, fOAEP: true);
    
    // Can now use decryptedSymmetricKey with the AES algorithm.
    

    HMAC-SHA256 を使用してデータの署名を比較する

    byte[] decryptedSymmetricKey = <the aes key decrypted in the previous step>;
    byte[] encryptedPayload = <the value from the data property, still encrypted>;
    byte[] expectedSignature = <the value from the dataSignature property>;
    byte[] actualSignature;
    
    using (HMACSHA256 hmac = new HMACSHA256(decryptedSymmetricKey))
    {
        actualSignature = hmac.ComputeHash(encryptedPayload);
    }
    if (actualSignature.SequenceEqual(expectedSignature))
    {
        // Continue with decryption of the encryptedPayload.
    }
    else
    {
        // Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
    }
    

    リソース データ コンテンツを復号する

    AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider();
    aesProvider.Key = decryptedSymmetricKey;
    aesProvider.Padding = PaddingMode.PKCS7;
    aesProvider.Mode = CipherMode.CBC;
    
    // Obtain the intialization vector from the symmetric key itself.
    int vectorSize = 16;
    byte[] iv = new byte[vectorSize];
    Array.Copy(decryptedSymmetricKey, iv, vectorSize);
    aesProvider.IV = iv;
    
    byte[] encryptedPayload = Convert.FromBase64String(<value from data property>);
    
    string decryptedResourceData;
    // Decrypt the resource data content.
    using (var decryptor = aesProvider.CreateDecryptor())
    {
      using (MemoryStream msDecrypt = new MemoryStream(encryptedPayload))
      {
          using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
          {
              using (StreamReader srDecrypt = new StreamReader(csDecrypt))
              {
                  decryptedResourceData = srDecrypt.ReadToEnd();
              }
          }
      }
    }
    
    // decryptedResourceData now contains a JSON string that represents the resource.