リソース データを含む変更通知を設定する (リッチ通知)
Microsoft Graph を使用すると、関心のあるリソースに対する変更のサブスクライブと通知をアプリで受け取ることができます。 基本的な変更通知をサブスクライブできますが、チャット メッセージやプレゼンス リソースなどのリソース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 * |
最大サブスクリプションのクォータ: |
Teams callTranscript | organization内のすべてのトランスクリプト:communications/onlineMeetings/getAllTranscripts 特定の会議のすべてのトランスクリプト: communications/onlineMeetings/{onlineMeetingId}/transcripts 特定のユーザーが開催した会議で使用できる通話トランスクリプト: users/{id}/onlineMeetings/getAllTranscripts 特定の Teams アプリがインストールされている会議で使用できる通話トランスクリプト: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllTrancripts * |
最大サブスクリプションのクォータ: |
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(joinWebUrl='{encodedJoinWebUrl}')/meetingCallEvents * |
を使用して $select 選択したプロパティのみを返すのをサポートしていません。 リッチ通知は、変更されたインスタンスのすべてのプロパティで構成されます。 オンライン会議ごとにアプリケーションごとに 1 つのサブスクリプションが許可されます。 詳細については、「 Microsoft Teams会議通話イベントの更新に関する変更通知を取得する」を参照してください。 |
Teams プレゼンス | 1 人のユーザーのプレゼンスに対する変更: /communications/presences/{id} 複数のユーザーのプレゼンスに対する変更: /communications/presences?$filter=id in ({id},{id}...) |
複数のユーザーのプレゼンスのサブスクリプションは、650 人の個別のユーザーに制限されています。 を使用して $select 選択したプロパティのみを返すのをサポートしていません。 リッチ通知は、変更されたインスタンスのすべてのプロパティで構成されます。 委任されたユーザーごとにアプリケーションごとに 1 つのサブスクリプションが許可されます。 詳細については、「 Microsoft Teamsでのプレゼンス更新プログラムの変更通知を取得する」を参照してください。 |
Teams チーム | テナント内の任意のチームに対する変更: /teams 特定のチームへの変更: /teams/{id} |
- |
通知ペイロードのリソース データ
リッチ通知には、ペイロードに次のリソース データが含まれます。
- resourceData プロパティにより返される、変更されたリソース インスタンスの ID および種類。
- そのリソース インスタンスのすべてのプロパティ値。これはサブスクリプションの指定に従って暗号化され、encryptedContent プロパティで返されます。
- または、リソースによっては、resourceData プロパティで返される特定のプロパティ。 特定のプロパティのみを取得するには、
$select
パラメーターを使用して、サブスクリプションのリソース URL の一部としてそれらのプロパティを指定します。
サブスクリプションの作成
リッチ通知は、次のプロパティを指定する必要がある場合を除き、基本的な変更通知と同じ方法で設定されます。
-
includeResourceData: これは、
true
に設定して、リソース データを明示的に要求する必要があります。 - encryptionCertificate 。Microsoft Graph がアプリに返すリソース データを暗号化するために使用する公開キーのみを含みます。 セキュリティを確保するために、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 を使用してサブスクライブしたさまざまなアプリとテナントの項目が混在している可能性があることに注意してください。
注:
Microsoft Graph では、サブスクリプション サービスが Event Hubs の notificationUrl を検証する必要がないため、Azure Event Hubs経由で配信される変更通知の検証トークンは送信されません。
次の例では、1 つのアプリおよび 2 つの異なるテナントに対して 2 つのアイテムがこの変更通知には含まれているため、validationTokens 配列には検証が必要なトークンが 2 つ含まれています。
{
"value": [
{
"subscriptionId": "76619225-ff6b-4489-96ca-4ef547e78b22",
"tenantId": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
"changeType": "created",
...
},
{
"subscriptionId": "5cfe2387-163c-4006-81bb-1b5e1e060afe",
"tenantId": "bbbbcccc-1111-dddd-2222-eeee3333ffff",
"changeType": "created",
...
}
],
"validationTokens": [
"eyJ0eXAiOiJKV1QiLCJhb...",
"cGlkYWNyIjoiMiIsImlkc..."
]
}
変更通知オブジェクトは、 changeNotificationCollection リソースの種類の構造にあります。
検証方法
Microsoft 認証ライブラリ (MSAL) を使用して、トークンの検証、または別のプラットフォーム用のサード パーティ製ライブラリの処理に役立ちます。
次の原則に留意してください。
- 変更通知に対する応答の一部として
HTTP 202 Accepted
状態コードを必ず送信するようにします。 - 後で検証が失敗した場合でも、変更通知を検証する前に応答します。 つまり、後で処理するためにキューに通知を格納するか、その場で処理するかに関係なく、すぐに変更通知を受け取ります。
- 変更通知を承認することにより、配信の不必要なリトライを防ぐことができ、悪意を持っている可能性がある者が検証に合格したかどうかを自ら確認できないようにもします。 無効な変更通知を受け取った後は、いつでも無視できます。
特に、validationTokens コレクション内のすべての JWT トークンに対して検証を実行します。 合格しないトークンが含まれる変更通知は不審な通知とみなし、詳しく調査します。
次の手順を使用して、トークンおよびトークンを生成するアプリを検証します。
トークンの有効期限が切れていないことを検証します。
Microsoft ID プラットフォームがトークンを発行し、トークンが改ざんされていないことを検証します。
- 共通の構成エンドポイント
https://login.microsoftonline.com/common/.well-known/openid-configuration
から、署名キーを取得します。 アプリでは、しばらくの間、この構成をキャッシュできます。 署名キーは毎日ローテーションされるため、構成は頻繁に更新されます。 - これらのキーを使用して、JWT トークンの署名を確認します。
他の機関によって発行されたトークンを受け入れないでください。
- 共通の構成エンドポイント
変更通知をサブスクライブしているアプリ用に発行されたトークンであることを確認します。
次の手順は、JWT トークン ライブラリでの標準的な検証ロジックの一部であり、通常は 1 つの関数呼び出しとして実行できます。
- トークン内の "audience" がアプリ ID と一致していることを確認します。
- 変更通知を受け取るアプリが複数ある場合は、複数の ID について確認する必要があります。
重要: トークンを生成したアプリが Microsoft Graph 変更通知の発行元を表していることを確認します。
- トークン内の プロパティが
azp
、 の予期される値0bf30f3b-4a52-48df-9a82-234910c4a086
と一致することを確認します。 - このチェックは、Microsoft Graph 以外の別のアプリが変更通知を送信しなかったことを保証します。
- トークン内の プロパティが
JWT トークンの例
次の例は、検証に必要な JWT トークンに含まれるプロパティを示しています。
{
// aud is your app's id
"aud": "925bff9f-f6e2-4a69-b858-f71ea2b9b6d0",
"iss": "https://login.microsoftonline.com/9f4ebab6-520d-49c0-85cc-7b25c78d4a93/v2.0",
"iat": 1624649764,
"nbf": 1624649764,
"exp": 1624736464,
"aio": "E2ZgYGjnuFglnX7mtjJzwR5lYaWvAA==",
// azp represents the notification publisher and must always be the same value of 0bf30f3b-4a52-48df-9a82-234910c4a086
"azp": "0bf30f3b-4a52-48df-9a82-234910c4a086",
"azpacr": "2",
"oid": "1e7d79fa-7893-4d50-bdde-164260d9c5ba",
"rh": "0.AX0AtrpOnw1SwEmFzHslx41KkzsP8wtSSt9ImoIjSRDEoIZ9AAA.",
"sub": "1e7d79fa-7893-4d50-bdde-164260d9c5ba",
"tid": "9f4ebab6-520d-49c0-85cc-7b25c78d4a93",
"uti": "mIB4QKCeZE6hK71XUHJ3AA",
"ver": "2.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 により暗号化された完全なリソース データが含まれています。 このプロパティには、確認と復号に必要な値も含まれています。 この暗号化は、変更通知を介してアクセスされる顧客データのセキュリティを強化するために行われます。 元の変更通知を傍受した場合でも、サード パーティが顧客データの暗号化を解除できないように秘密キーをセキュリティで保護するのは、お客様の責任です。
このセクションでは、次の概念について説明します。
暗号化キーの管理
非対称キーのペアを使用して証明書を取得します。
Microsoft Graph は証明書発行者を検証せず、暗号化にのみ公開キーを使用するため、自己署名証明書を使用できます。
Azure Key Vaultを使用して、証明書を作成、ローテーション、安全に管理します。 キーは、次の条件を満たしている必要があります。
- キーの型
RSA
は である必要があります。 - キー サイズは、2,048 ビットから 4,096 ビットの間である必要があります。
- キーの型
Base64 でエンコードされた X.509 形式で証明書をエクスポートし、 公開キーのみを含めます。
サブスクリプションを作成するときに、以下を行います。
証明書がエクスポートされた Base64 でエンコードされたコンテンツを使用して、 encryptionCertificate プロパティに証明書を指定します。
encryptionCertificateId プロパティで独自の識別子を指定します。
この識別子を使用することで、受信する変更通知に証明書を一致させ、証明書ストアから証明書を取得することができます。 識別子には、最大 128 文字まで使用できます。
変更通知処理コードが秘密キーにアクセスしてリソース データを復号できるよう、秘密キーを安全に管理にします。
キーのローテーション
秘密キーの漏洩リスクを最小限に抑えるために、非対称キーは定期的に変更します。 次の手順に従って、新しいキーのペアを導入します。
新しい非対称キーのペアを使用して新しい証明書を取得します。 作成するすべての新しいサブスクリプションでこれを使用します。
新しい証明書キーを使用して既存のサブスクリプションを更新します。
- この更新プログラムは、通常のサブスクリプション更新の一部にします。
- または、すべてのサブスクリプションを列挙して、キーを提供します。 サブスクリプション で PATCH 操作を使用して、encryptionCertificate プロパティと encryptionCertificateId プロパティを更新します。
次の原則に留意してください。
- しばらくの間、古い証明書は暗号化に引き続き使用される場合があります。 コンテンツを復号できるよう、アプリは古い証明書と新しい証明書の両方にアクセスできる必要があります。
- 各変更通知で encryptionCertificateId プロパティを使用して、使用する正しいキーを特定します。
- 古い証明書を破棄するのは、参照している最近の変更通知が表示されない場合のみです。
リソース データの復号
パフォーマンスを最適化するために、Microsoft Graph では 2 段階の暗号化プロセスを使用しています。
- これは、単一の使用対称キーを生成し、それを使用してリソース データを暗号化します。
- このプロセスでは、対称公開キー (サブスクライブする際に指定したもの) を使用して対称キーが暗号化され、それが当該サブスクリプションの各変更通知に含められます。
変更通知の各アイテムで対称キーが異なることを常に想定してください。
リソース データを復号するには、アプリは各変更通知の encryptedContent のプロパティを使用して手順を逆に進める必要があります。
encryptionCertificateId プロパティを使用して、使用する証明書を特定します。
秘密キーを使用して RSA 暗号化コンポーネントを初期化します。 RSA コンポーネントを簡単に初期化する方法は、「暗号化キーの管理」で説明されている秘密キーを含む X509Certificate2 インスタンスで RSACertificateExtensions.GetRSAPrivateKey(X509Certificate2) メソッドを使用することです。
変更通知内の各アイテムの dataKey プロパティで提供された対称キーを復号します。
復号アルゴリズムには、最適非対称暗号化パディング (OAEP) を使用します。
対称キーを使用して、データ内の値の HMAC-SHA256 署名を計算します。
その署名を dataSignature の値と比較します。 一致しない場合は、ペイロードが改ざんされ、復号化されていないと仮定します。
対称キーと Advanced Encryption Standard (AES) (.NET Aes など) を使用して 、データ内のコンテンツの暗号化を解除します。
AES アルゴリズムでは、次の復号パラメーターを使用します。
- パディング: PKCS7
- 暗号モード: CBC
復号に使用する対称キーの最初の 16 バイトをコピーして、"初期化ベクター" を設定します。
復号された値は、変更通知のリソース インスタンスを表す JSON 文字列です。
例: 暗号化されたリソース データが使用されている通知の復号
次の 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.
X509Certificate2 certificate = <instance of X509Certificate2 matching the encryptionCertificateId property>;
RSA rsa = certificate.GetRSAPrivateKey();
byte[] encryptedSymmetricKey = Convert.FromBase64String(<value from dataKey property>);
// Decrypt using OAEP padding.
byte[] decryptedSymmetricKey = rsa.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.
}
リソース データ コンテンツを復号する
Aes aesProvider = Aes.Create();
aesProvider.Key = decryptedSymmetricKey;
aesProvider.Padding = PaddingMode.PKCS7;
aesProvider.Mode = CipherMode.CBC;
// Obtain the initialization 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.