リソース データが含まれる変更通知を設定するSet up change notifications that include resource data

Microsoft Graph を使用すると、アプリは webhook 経由でリソースの変更通知をサブスクライブできるようになります。Microsoft Graph allows apps to subscribe to change notifications for resources via webhooks. 変更されたリソース データ (Microsoft Teams のチャット メッセージや Microsoft Teams のプレゼンス情報のコンテンツなど) を変更通知に含めるようにサブスクリプションを設定できます。You can set up subscriptions to include the changed resource data (such as the content of a Microsoft Teams chat message or Microsoft Teams presence information) in change notifications. これにより、アプリでは、変更されたリソースを取得するために改めて API 呼び出しを行うことなくビジネス ロジックを実行できます。Your app can then run its business logic without having to make a separate API call to fetch the changed resource. その結果、API 呼び出しの数が減るためにアプリのパフォーマンスが向上し、これは大規模なシナリオで役に立ちます。As a result, the app performs better by making fewer API calls, which is beneficial in large scale scenarios.

変更通知の一部としてリソース データを含めるには、次の追加のロジックを実装してデータ アクセスおよびセキュリティの要件を満たす必要があります。Including resource data as part of change notifications requires you to implement the following additional logic to satisfy data access and security requirements:

  • サブスクリプションの特別なライフサイクル通知 (プレビュー) を 処理 してデータの継続的な流れを維持します。Handle special subscription lifecycle notifications (preview) to maintain an uninterrupted flow of data. Microsoft Graph では、変更通知にリソース データを含めることで予期せぬアクセスの問題が発生しないよう、ライフサイクル通知を随時送信してアプリが再承認されるようにしています。Microsoft Graph sends lifecycle notifications from time to time to require an app to re-authorize, to make sure access issues have not unexpectedly cropped up for including resource data in change notifications.
  • 変更通知が Microsoft Graph から発信されたものであることの真正性を 検証 します。Validate the authenticity of change notifications as having originated from Microsoft Graph.
  • 公開暗号キーを 提供 し、変更通知で受け取ったリソース データの復号には秘密キーを使用します。Provide a public encryption key and use a private key to decrypt resource data received through change notifications.

通知ペイロードのリソース データResource data in notification payload

通常、この種類の変更通知では、次のリソース データがペイロードに含まれます。In general, this type of change notifications include the following resource data in the payload:

  • resourceData プロパティにより返される、変更されたリソース インスタンスの ID および種類。ID and type of the changed resource instance, returned in the resourceData property.
  • そのリソース インスタンスのすべてのプロパティ値。これはサブスクリプションの指定に従って暗号化され、encryptedContent プロパティで返されます。All the property values of that resource instance, encrypted as specified in the subscription, returned in the encryptedContent property.
  • または、リソースによっては、resourceData プロパティで返される特定のプロパティ。Or, depending on the resource, specific properties returned in the resourceData property. 特定のプロパティのみを取得するには、$select パラメーターを使用して、サブスクリプションのリソース URL の一部としてそれらのプロパティを指定します。To get only specific properties, specify them as part of the resource URL in the subscription, using a $select parameter.

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

現在、Microsoft Teams の chatMessageに加えて、Microsoft Teams の プレゼンス (プレビュー) リソースでは、リソース データが含まれる変更通知がサポートされています。Currently, the Microsoft Teams chatMessage as well as the Microsoft Teams presence (preview) resources supports change notifications that include resource data. 具体的には、次のいずれかに適用されるサブスクリプションを設定できます。Specifically, you can set up a subscription that applies to one of the following:

  • 特定の Teams チャネル内の新しいまたは変更されたメッセージ: /teams/{id}/channels/{id}/messagesNew or changed messages in a specific Teams channel: /teams/{id}/channels/{id}/messages
  • 特定の Teams チャット内の新しいまたは変更されたメッセージ: /chats/{id}/messagesNew or changed messages in a specific Teams chat: /chats/{id}/messages
  • ユーザーのプレゼンス情報の更新: /communications/presences/{id}User's presence information update: /communications/presences/{id}

chatMessageプレゼンス (プレビュー) リソースでは、変更通知内で変更されたインスタンスのすべてのプロパティがサポートされています。The chatMessage and the presence (preview) resources support including all the properties of a changed instance in a change notification. インスタンスの選択したプロパティのみを取得することはサポートされていません。They do not support returning only selective properties of the instance.

この記事では、Teams チャネルでメッセージの変更通知をサブスクライブする例を説明します。各変更通知には、変更された chatMessage インスタンスの完全なリソース データが含まれます。This article walks through an example of subscribing to change notifications of messages in a Teams channel, with each change notification including the full resource data of the changed chatMessage instance.

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

リソース データを変更通知に含めるには、通常はサブスクリプションの作成時に指定されるプロパティに加え、次のプロパティを指定する必要がありますTo have resource data included in change notifications, you must specify the following properties, in addition to those that are usually specified when creating a subscription:

  • includeResourceData: これは、true に設定して、リソース データを明示的に要求する必要があります。includeResourceData which should be set to true to explicitly request resource data.
  • encryptionCertificate: リソース データを暗号化するために Microsoft Graph で使用される公開キーのみがこれに含まれます。encryptionCertificate which contains only the public key that Microsoft Graph uses to encrypt resource data. コンテンツを復号するために、対応する秘密キーを保持しておきます。Keep the corresponding private key to decrypt the content.
  • encryptionCertificateId: 証明書用の独自の識別子です。encryptionCertificateId which is your own identifier for the certificate. この ID は、復号に使用する証明書を照合するために各変更通知で使用します。Use this ID to match in each change notification, which certificate to use for decryption.

以下の点にご注意ください:Keep the following in mind:

  • 通知エンドポイントの検証の説明に従って、両方の通知エンドポイントを検証します。Validate both endpoints as described in Notification endpoint validation. 両エンドポイントに同じ URL を使用することを選択した場合は、検証要求が 2 通配信されるので、両方に応答します。If you choose to use the same URL for both endpoints, you will receive and respond to two validation requests.

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

Microsoft Teams で作成または更新されるチャネル メッセージをサブスクライブする例は次の通りです。The following example subscribes to channel messages being created or updated in 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}"
}

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

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}"
}

サブスクリプション ライフサイクル通知 (プレビュー)Subscription lifecycle notifications (preview)

既存のサブスクリプションでの変更通知フローは、特定のイベントにより妨げられる場合があります。Certain events can interfere with change notification flow in an existing subscription. サブスクリプション ライフサイクル通知では、継続的なフローを維持するために実行する必要があるアクションが通知されます。Subscription lifecycle notifications inform you actions to take in order to maintain an uninterrupted flow. リソース インスタンスでの変更を通知するリソース変更通知とは異なり、ライフサイクル通知は、サブスクリプション自体と、サブスクリプションのライフサイクル内での現在の状態に関するものです。Unlike a resource change notification which informs a change to a resource instance, a lifecycle notification is about the subscription itself, and its current state in the lifecycle.

ライフサイクル通知 (プレビュー) の受信および応答方法の詳細については、「不足しているサブスクリプションの削減と通知の変更 (プレビュー)」を参照してください。For more information about how to receive, and respond to, lifecycle notifications (preview), see Reduce missing subscriptions and change notifications (preview)

通知の真正性を検証するValidating the authenticity of notifications

アプリでは多くの場合、変更通知に含まれるリソース データに基づいてビジネス ロジックが実行されます。Apps often run business logic based on resource data included in change notifications. 各変更通知の真正性を最初に確認しておくことが重要です。Verifying the authenticity of each change notification first is important. これを行わない場合、第三者が偽の変更通知を用いてアプリに対してなりすましを行い、アプリのビジネス ロジックを不正に実行することができ、セキュリティ インシデントの発生につながる可能性があります。Otherwise, a third party can spoof your app with false change notifications and make it run its business logic incorrectly, and this can lead to a security incident.

リソース データが含まれていない基本的な変更通知は、「変更通知を処理する」の説明に従い、clientState 値に基づいて簡単に検証します。For basic change notifications that do not contain resource data, simply validate them based on the clientState value as described in Processing the change notification. これは、信頼できる Microsoft Graph 呼び出しを後から行ってリソース データにアクセスできるため、問題ありません。そのため、スプーフィングが試行された場合でも、その影響は限定的です。This is acceptable, as you can make subsequent trusted Microsoft Graph calls to get access to resource data, and therefore the impact of any spoofing attempts is limited.

リソース データを送信する変更通知の場合は、データを処理する前により徹底した検証を行います。For change notifications that deliver resource data, perform a more thorough validation before processing the data.

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

変更通知の検証トークンValidation tokens in the change notification

リソース データが含まれる変更通知には、validationTokens という追加のプロパティが含まれており、これには、Microsoft Graph により生成される JWT トークンの配列が含まれています。A change notification with resource data contains an additional property, validationTokens, which contains an array of JWT tokens generated by Microsoft Graph. Microsoft Graph では、value 配列にアイテムが存在するアプリとテナントのペアそれぞれにつきトークンが 1 つ生成されます。Microsoft Graph generates a single token for each distinct app and tenant pair for whom there is an item in the value array. 変更通知には、同じ notificationUrl を使用してサブスクライブを行ったさまざまなアプリやテナントのアイテムが混在している場合があることをご留意ください。Keep in mind that change notifications may contain a mix of items for various apps and tenants that subscribed using the same notificationUrl.

次の例では、1 つのアプリおよび 2 つの異なるテナントに対して 2 つのアイテムがこの変更通知には含まれているため、validationTokens 配列には検証が必要なトークンが 2 つ含まれています。In the following example, the change notification contains two items for the same app, and for two different tenants, therefore the validationTokens array contains two tokens that need to be validated.

{
    "value": [
          {
            "subscriptionId": "76619225-ff6b-4489-96ca-4ef547e78b22",
      "tenantId": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
            "changeType": "created",
            ...
          },
      {
            "subscriptionId": "e990d58f-fd93-40af-acf7-a7c907c5d8ea",
      "tenantId": "46d9e3bd-6309-4177-a016-b256a411e30f",
            "changeType": "created",
            ...
            }
    ],
    "validationTokens": [
        "eyJ0eXAiOiJKV1QiLCJhb...",
    "cGlkYWNyIjoiMiIsImlkc..."
    ]
}

注: 変更通知が配信されたときに送信されるデータの詳細については、changeNotificationCollection を参照してください。Note: for a full description of the data sent when change notifications are delivered, see changeNotificationCollection.

検証方法How to validate

トークンの検証を初めて使用する場合は、「トークンの検証の原則」 で概要を確認してください。If you're new to token validation, see Principles of Token Validation for an overview. .NET 用の System.IdentityModel.Tokens.Jwt ライブラリなどの SDK または他のプラットフォーム用のサードパーティ ライブラリを使用します。Use an SDK, such as the System.IdentityModel.Tokens.Jwt library for .NET, or a third-party library for a different platform.

次の点に注意してください:Be mindful of the following:

  • 変更通知に対する応答の一部として HTTP 202 Accepted 状態コードを必ず送信するようにします。Make sure to always send an HTTP 202 Accepted status code as part of the response to the change notification.
  • 検証が失敗した場合でも、変更通知の検証前 (たとえば、後で処理するために変更通知をキューに保存する場合) または検証後 (その場ですぐ検証する場合) に実行します。Respond before validating the change notification (for example, if you store change notifications in queues for later processing) or after (if you process them on the fly), even if validation failed.
  • 変更通知を承認することにより、配信の不必要なリトライを防ぐことができ、悪意を持っている可能性がある者が検証に合格したかどうかを自ら確認できないようにもします。Accepting a change notification prevents unnecessary delivery retries and it also prevents any potential rogue actors from finding out if they passed or failed validation. 無効な変更通知は、その通知を承認した後にいつでも無視することもできます。You can always choose to ignore an invalid change notification after you have accepted it.

特に、validationTokens コレクション内のすべての JWT トークンに対して検証を実行します。In particular, perform validation on every JWT token in the validationTokens collection. 合格しないトークンが含まれる変更通知は不審な通知とみなし、詳しく調査します。If any tokens fail, consider the change notification suspicious and investigate further.

次の手順を使用して、トークンおよびトークンを生成するアプリを検証します。Use the following steps to validate tokens and apps that generate tokens:

  1. トークンの有効期限が切れていないことを確認します。Validate that the token has not expired.

  2. トークンが改ざんされていないこと、また、想定された機関である Microsoft ID プラットフォームにより発行されたものであることを検証します。Validate the token has not been tampered with and was issued by the expected authority, Microsoft identity platform:

    • 共通の構成エンドポイント https://login.microsoftonline.com/common/.well-known/openid-configuration から、署名キーを取得します。 Obtain the signing keys from the common configuration endpoint: https://login.microsoftonline.com/common/.well-known/openid-configuration. この構成は、アプリで一定期間キャッシュされます。This configuration is cached by your app for a period of time. 署名キーは毎日ローテーションされるため、構成は頻繁に更新される点にご注意ください。Be aware that the configuration is updated frequently as signing keys are rotated daily.
    • これらのキーを使用して、JWT トークンの署名を確認します。Verify the signature of the JWT token using those keys.

    他の機関が発行したトークンは承認しなでください。Do not accept tokens issued by any other authority.

  3. 変更通知をサブスクライブしているアプリ用に発行されたトークンであることを確認します。Validate that the token was issued for your app that is subscribing to change notifications.

    次の手順は、JWT トークン ライブラリでの標準的な検証ロジックの一部であり、通常は 1 つの関数呼び出しとして実行できます。The following steps are part of standard validation logic in JWT token libraries and can typically be executed as a single function call.

    • トークン内の "audience" がアプリ ID と一致していることを確認します。Validate the "audience" in the token matches your app ID.
    • 変更通知を受け取るアプリが複数ある場合は、複数の ID について確認する必要があります。If you have more than one app receiving change notifications, make sure to check for multiple IDs.
  4. 重要: トークンを生成したアプリが Microsoft Graph 変更通知の発行元を表していることを確認します。Critical: Validate that the app that generated the token represents the Microsoft Graph change notification publisher.

    • トークンの appid プロパティが、想定値である 0bf30f3b-4a52-48df-9a82-234910c4a086 と一致していることを確認します。Check that the appid property in the token matches the expected value of 0bf30f3b-4a52-48df-9a82-234910c4a086.
    • これにより、変更通知が Microsoft Graph 以外の別のアプリから送信されたものでないことを確認できます。This ensures that change notifications are not sent by a different app that is not Microsoft Graph.

JWT トークンの例Example JWT token

検証が必要な JWT トークンに含まれるプロパティの例を次に示します。The following is an example of the properties included in the JWT token that are needed for validation.

{
  // 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"
}

例: 検証トークンの検証Example: Verifying validation tokens

// 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;
    }
}
private boolean IsValidationTokenValid(String[] appIds, String tenantId, String serializedToken) {
    try {
        JwkKeyResolver jwksResolver = new JwkKeyResolver();
        Jws<Claims> token = Jwts.parserBuilder()
        .setSigningKeyResolver(jwksResolver)
        .build()
        .parseClaimsJws(serializedToken);
        Claims body = token.getBody();
        String audience = body.getAudience();
        boolean isAudienceValid = false;
        for(String appId : appIds) {
        isAudienceValid = isAudienceValid || appId.equals(audience);
        }
        boolean isTenantValid = body.getIssuer().endsWith(tenantId + "/");
        return isAudienceValid  && isTenantValid; //nbf,exp and signature are already validated by library
    } catch (Exception e) {
        LOGGER.error("could not validate token");
        LOGGER.error(e.getMessage());
        return false;
    }
}
import jwt from 'jsonwebtoken';
import jkwsClient from 'jwks-rsa';

const client = jkwsClient({
  jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
});

export function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    var signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
  });
}

export function isTokenValid(token, appId, tenantId) {
  return new Promise((resolve) => {
    const options = {
      audience: [appId],
      issuer: [`https://sts.windows.net/${tenantId}/`]
    };
    jwt.verify(token, getKey, options, (err) => {
      if (err) {
        // eslint-disable-next-line no-console
        console.error(err);
        resolve(false);
      } else {
        resolve(true);
      }
    });
  });
}

Java サンプルが動作するには、JwkKeyResolver も実装する必要があります。For the Java sample to work, you will also need to implement the JwkKeyResolver.

package com.example.restservice;

import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwk.Jwk;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import java.security.Key;
import java.net.URI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JwkKeyResolver extends SigningKeyResolverAdapter {
    private JwkProvider keyStore;
    private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
    public JwkKeyResolver() throws java.net.URISyntaxException, java.net.MalformedURLException {
        this.keyStore = new UrlJwkProvider((new URI("https://login.microsoftonline.com/common/discovery/keys").toURL()));
    }
    @Override
    public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
        try {
            String keyId = jwsHeader.getKeyId();
            Jwk pub = keyStore.get(keyId);
            return pub.getPublicKey();
        } catch (Exception e) {
            LOGGER.error(e.getMessage());
            return null;
        }
    }
}

変更通知からのリソース データの復号Decrypting resource data from change notifications

変更通知の resourceData プロパティには、リソース インスタンスの基本 ID と型情報のみが含まれています。The resourceData property of a change notification includes only the basic ID and type information of a resource instance. encryptedData プロパティには、サブスクリプションで提供されている公開キーを使用して Microsoft Graph により暗号化された完全なリソース データが含まれています。The encryptedData property contains the full resource data, encrypted by Microsoft Graph using the public key provided in the subscription. このプロパティには、確認と復号に必要な値も含まれています。The property also contains values required for verification and decryption. これを行う理由は、変更通知経由でアクセスする顧客のデータのセキュリティを強化するためです。This is done to increase the security of customer data accessed via change notifications. たとえ元の変更通知が第三者により傍受された場合でもその者が顧客のデータを復号できないよう、管理者の責任において秘密キーの安全を確保する必要があります。It is your responsibility to secure the private key to ensure that customer data cannot be decrypted by a third party, even if they manage to intercept the original change notifications.

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

暗号化キーの管理Managing encryption keys

  1. 非対称キーのペアを使用して証明書を取得します。Obtain a certificate with a pair of asymmetric keys.

    • 証明書は、自己署名することができます。これは、Microsoft Graph では証明書の発行元が検証されず、公開キーは暗号化のみに使用されるためです。You can self-sign the certificate, since Microsoft Graph does not verify the certificate issuer, and uses the public key for only encryption.

    • 証明書の作成、ローテーション、および安全な管理を行うためのソリューションとして、Azure Key Vault を使用します。Use Azure Key Vault as the solution to create, rotate, and securely manage certificates. キーは、次の条件を満たしている必要があります。Make sure the keys satisfy the following criteria:

      • キーの種類が RSA である必要があります。The key must be of type RSA
      • キーのサイズが 2048 ビットから 4096 ビットの範囲である必要があります。The key size must be between 2048 and 4096 bits
  2. 証明書を Base64 でエンコードされた X.509 形式でエクスポートし、公開キーのみを含めますExport the certificate in base64-encoded X.509 format, and include only the public key.

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

    • 証明書がエクスポートされた Base64 でエンコードされたコンテンツを使用して、encryptionCertificate プロパティで証明書を指定します。Provide the certificate in the encryptionCertificate property, using the base64-encoded content that the certificate was exported in.

    • encryptionCertificateId プロパティで独自の識別子を指定します。Provide your own identifier in the encryptionCertificateId property.

      この識別子を使用することで、受信する変更通知に証明書を一致させ、証明書ストアから証明書を取得することができます。This identifier allows you to match your certificates to the change notifications you receive, and to retrieve certificates from your certificate store. 識別子には、最大 128 文字まで使用できます。The identifier can be up to 128 characters.

  4. 変更通知処理コードが秘密キーにアクセスしてリソース データを復号できるよう、秘密キーを安全に管理にします。Manage the private key securely, so that your change notification processing code can access the private key to decrypt resource data.

キーのローテーションRotating keys

秘密キーの漏洩リスクを最小限に抑えるために、非対称キーは定期的に変更します。To minimize the risk of a private key becoming compromised, periodically change your asymmetric keys. 次の手順に従って、新しいキーのペアを導入します。Follow these steps to introduce a new pair of keys:

  1. 新しい非対称キーのペアを使用して新しい証明書を取得します。Obtain a new certificate with a new pair of asymmetric keys. 作成するすべての新しいサブスクリプションでこれを使用します。Use it for all new subscriptions being created.

  2. 新しい証明書キーを使用して既存のサブスクリプションを更新します。Update existing subscriptions with the new certificate key.

    • 定期的なサブスクリプション更新の一部としてこの作業を行います。Do this as part of regular subscription renewal.
    • または、すべてのサブスクリプションを列挙して、キーを提供します。Or, enumerate all subscriptions and provide the key. サブスクリプション で PATCH 操作を使用して、encryptionCertificate プロパティと encryptionCertificateId プロパティを更新します。Use the PATCH operation on the subscription and update the encryptionCertificate and encryptionCertificateId properties.
  3. 以下の点に注意してください。Keep in mind the following:

    • 一定の期間、暗号化のために古い証明書が使用される場合があります。For a period of time, the old certificate may still be used for encryption. コンテンツを復号できるよう、アプリは古い証明書と新しい証明書の両方にアクセスできる必要があります。Your app must have access to both old and new certificates to be able to decrypt content.
    • 各変更通知で encryptionCertificateId プロパティを使用して、使用する正しいキーを特定します。Use the encryptionCertificateId property in each change notification to identify the correct key to use.
    • 古い証明書は、最近の変更通知で古い証明書への参照が行われなくなった場合にのみ破棄します。Discard of the old certificate only when you have seen no recent change notifications referencing it.

リソース データの復号Decrypting resource data

パフォーマンスを最適化するために、Microsoft Graph では 2 段階の暗号化プロセスを使用しています。To optimize performance, Microsoft Graph uses a two-step encryption process:

  • このプロセスでは、1 回限りの使用の対称キーが生成され、リソース データの暗号化に使用されます。It generates a single use symmetric key, and uses it to encrypt resource data.
  • このプロセスでは、対称公開キー (サブスクライブする際に指定したもの) を使用して対称キーが暗号化され、それが当該サブスクリプションの各変更通知に含められます。It uses the public asymmetric key (that you provided when subscribing) to encrypt the symmetric key and includes it in each change notification of that subscription.

変更通知の各アイテムで対称キーが異なることを常に想定してください。Always assume that the symmetric key is different for each item in the change notification.

リソース データを復号するには、アプリは各変更通知の encryptedContent のプロパティを使用して手順を逆に進める必要があります。To decrypt resource data, your app should perform the reverse steps, using the properties under encryptedContent in each change notification:

  1. encryptionCertificateId プロパティを使用して、使用する証明書を特定します。Use the encryptionCertificateId property to identify the certificate to use.

  2. 秘密キーを使用して、RSA 暗号化コンポーネント (.NET RSACryptoServiceProvider など) を初期化します。Initialize an RSA cryptographic component (such as the .NET RSACryptoServiceProvider) with the private key.

  3. 変更通知内の各アイテムの dataKey プロパティで提供された対称キーを復号します。Decrypt the symmetric key delivered in the dataKey property of each item in the change notification.

    復号アルゴリズムには、最適非対称暗号化パディング (OAEP) を使用します。Use Optimal Asymmetric Encryption Padding (OAEP) for the decryption algorithm.

  4. 対称キーを使用して、データ内の値の HMAC-SHA256 署名を計算します。Use the symmetric key to calculate the HMAC-SHA256 signature of the value in data.

    その署名を dataSignature の値と比較します。Compare it to the value in dataSignature. これらが一致しない場合は、ペイロードが改ざんされたものとみなし、復号しないようにします。If they do not match, assume the payload has been tampered with and do not decrypt it.

  5. Advanced Encryption Standard (AES) (.NET AesCryptoServiceProvider など) を用いて対称キー使用し、データ内のコンテンツを復号します。Use the symmetric key with an Advanced Encryption Standard (AES) (such as the .NET AesCryptoServiceProvider) to decrypt the content in data.

    • AES アルゴリズムでは、次の復号パラメーターを使用します。Use the following decryption parameters for the AES algorithm:

      • パディング: PKCS7Padding: PKCS7
      • 暗号モード: CBCCipher mode: CBC
    • 復号に使用する対称キーの最初の 16 バイトをコピーして、"初期化ベクター" を設定します。Set the "initialization vector" by copying the first 16 bytes of the symmetric key used for decryption.

  6. 復号された値は、変更通知のリソース インスタンスを表す JSON 文字列です。The decrypted value is a JSON string that represents the resource instance in the change notification.

例: 暗号化されたリソース データが使用されている通知の復号Example: decrypting a notification with encrypted resource data

チャネル メッセージ内の chatMessage インスタンスの暗号化されたプロパティ値を含む変更通知例を次に示します。The following is an example change notification that includes encrypted property values of a chatMessage instance in a channel message. インスタンスは、@odata.id 値によって指定されています。The instance is specified by the @odata.id value.

{
    "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 を参照してください。Note: for a full description of the data sent when change notifications are delivered, see changeNotificationCollection.

このセクションでは、復号の各ステージで役立つ、 C# および .NET を使用するコード スニペットをいくつか示します。This section contains some useful code snippets that use C# and .NET for each stage of decryption.

対称キーを復号するDecrypt the symmetric key

// 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.
String storename = ""; //name/path of the jks store
String storepass = ""; //password used to open the jks store
String alias = ""; //alias of the certificate when store in the jks store, should be passed as encryptionCertificateId when subscribing and retrieved from the notification
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(storename), storepass.toCharArray());
Key asymmetricKey = ks.getKey(alias, storepass.toCharArray());
byte[] encryptedSymetricKey = Base64.decodeBase64("<value from dataKey property>");
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, asymmetricKey);
byte[] decryptedSymmetricKey = cipher.doFinal(encryptedSymetricKey);
// Can now use decryptedSymmetricKey with the AES algorithm.
const base64encodedKey = 'base 64 encoded dataKey value';
const asymetricPrivateKey = 'pem encoded private key';
const decodedKey = Buffer.from(base64encodedKey, 'base64');
const decryptedSymetricKey = crypto.privateDecrypt(asymetricPrivateKey, decodedKey);
// Can now use decryptedSymmetricKey with the AES algorithm.

HMAC-SHA256 を使用してデータの署名を比較するCompare data signature using 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.
}
byte[] decryptedSymmetricKey = "<the aes key decrypted in the previous step>";
byte[] decodedEncryptedData = Base64.decodeBase64("data property from encryptedContent object");
Mac mac = Mac.getInstance("HMACSHA256");
SecretKey skey = new SecretKeySpec(decryptedSymmetricKey, "HMACSHA256");
mac.init(skey);
byte[] hashedData = mac.doFinal(decodedEncryptedData);
String encodedHashedData = new String(Base64.encodeBase64(hashedData));
if (comparisonSignature.equals(encodedHashedData))
{
    // Continue with decryption of the encryptedPayload.
}
else
{
    // Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
}
const decryptedSymetricKey = []; //Buffer provided by previous step
const base64encodedSignature = 'base64 encodded value from the dataSignature property';
const hmac = crypto.createHmac('sha256', decryptedSymetricKey);
hmac.write(base64encodedPayload, 'base64');
if(base64encodedSignature === hmac.digest('base64'))
{
    // Continue with decryption of the encryptedPayload.
}
else
{
    // Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
}

リソース データ コンテンツを復号するDecrypt the resource data content

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 dataKey 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.
SecretKey skey = new SecretKeySpec(decryptedSymmetricKey, "AES");
IvParameterSpec ivspec = new IvParameterSpec(Arrays.copyOf(decryptedSymmetricKey, 16));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, skey, ivspec);
String decryptedResourceData = new String(cipher.doFinal(Base64.decodeBase64(encryptedData)));
const base64encodedPayload = 'base64 encoded value from data property';
const decryptedSymetricKey = []; //Buffer provided by previous step
const iv = Buffer.alloc(16, 0);
decryptedSymetricKey.copy(iv, 0, 0, 16);
const decipher = crypto.createDecipheriv('aes-256-cbc', decryptedSymetricKey, iv);
let decryptedPayload = decipher.update(base64encodedPayload, 'base64', 'utf8');
decryptedPayload += decipher.final('utf8');

関連項目See also