次の方法で共有


リソース データを使用して Microsoft Graph の変更通知を設定する

Microsoft Graph を使用すると、アプリはリソースへの変更に関する通知をサブスクライブして受信できます。 この記事では、通知ペイロードにリソース データを直接含む リッチ通知を設定する方法について説明します。

リッチ通知により、更新されたリソースを取得するための追加の API 呼び出しが不要になり、ビジネス ロジックの実行が迅速かつ容易になります。

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

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

注:

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

リソース サポートされているリソース パス 制限事項
Copilot aiInteraction 特定のユーザーが参加している Copilot AI の相互作用: copilot/users/{userId}/interactionHistory/getAllEnterpriseInteractions

organizationでの Copilot AI の相互作用:copilot/interactionHistory/getAllEnterpriseInteractions
最大サブスクリプションのクォータ:
  • アプリとテナントの組み合わせ (テナント全体の AI 対話を追跡するサブスクリプションの場合): 1
  • アプリとユーザーの組み合わせ (特定のユーザーが属している AI の相互作用を追跡するサブスクリプションの場合): 1
  • ユーザーごと (特定のユーザーが参加している AI の相互作用を追跡するサブスクリプションの場合): 10 個のサブスクリプション。
  • organizationあたり: 合計 10,000 個のサブスクリプション。
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

テナント全体のすべてのチームのメンバーシップに対する変更: /teams/getAllMembers

特定のチームのすべてのチャネルのメンバーシップに対する変更: /teams/{id}/channels/getAllMembers

テナント全体のすべてのチャネルのメンバーシップに対する変更: /teams/getAllChannels/getAllMembers

特定のチャットでのメンバーシップの変更: /chats/{id}/members

すべての Teams チャットのメンバーシップの変更: /chats/getAllMembers
選択したプロパティのみを返す $select の使用はサポートされていません。 リッチ通知は、変更されたインスタンスのすべてのプロパティで構成されます。
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 プロパティで見つかった、サブスクリプションで指定されたとおりに暗号化されたリソース インスタンスのすべてのプロパティ値。
  • リソースの特定のプロパティ。リソースに応じて、またはサブスクリプションのリソース URL で $select パラメーターを使用して要求された場合。

通知のアプリケーション構成

リソース データを使用してサブスクリプションを作成する前に、 appRoleAssignmentRequired プロパティを次のように設定して、テナントとアプリのペアを表すサービス プリンシパル オブジェクトのアプリケーション アクセスを構成します。

どちらの条件も満たされていない場合、通知ペイロードには null検証トークンが含まれます。

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

リッチ通知を設定するには、 基本的な変更通知と同じ手順に従いますが、次の必須プロパティを含めます。

  • includeResourceData: リソース データを要求するには、これを true に設定します。
  • encryptionCertificate: Microsoft Graph がリソース データの暗号化に使用する公開キーを指定します。 詳細については、「 変更通知からのリソース データの暗号化解除」を参照してください。
  • encryptionCertificateId: 通知と正しい暗号化解除キーを一致させる証明書の識別子を指定します。

通知エンドポイントの検証の説明に従って、両方の通知エンドポイントを検証します。 両方のエンドポイントで同じ 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": "{customId}",
  "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
  "clientState": "{secretClientState}"
}

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

イベントによって、サブスクリプション内の変更通知のフローが中断される可能性があります。 ライフサイクル通知は、フローを中断しない状態に保つために実行するアクションを示します。 リソース変更通知とは異なり、ライフサイクル通知はサブスクリプションの状態に焦点を当てます。

詳細については、「 不足しているサブスクリプションを減らし、通知を変更する」を参照してください。

通知の信頼性を検証する

変更通知を処理する前に、変更通知の信頼性を常に確認してください。 これにより、サード パーティからの偽の通知を使用して、アプリで不適切なビジネス ロジックがトリガーされるのを防ぐことができます。

基本的な通知については、「変更通知の処理」で説明されているように 、clientState 値を使用して検証 します。 リッチ通知の場合は、追加の検証手順を実行します。

変更通知の検証トークン

リッチ通知には、JSON Web トークン (JWT) の配列を含む validationTokens プロパティが含まれます。 各トークンは、アプリとテナントのペアに一意です。 変更通知には、同じ notificationUrl を使用してサブスクライブしたさまざまなアプリとテナントの項目が混在している場合があります。

注:

Microsoft Graph では、サブスクリプション サービスが Event Hubs の notificationUrl を検証する必要がないため、Azure Event Hubs経由で配信される変更通知の検証トークンは送信されません。

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

ヒント

validationTokensnull値は、アプリの構成が正しくないため、Microsoft Graph でリソース データを暗号化できなかったことを示します。 この問題を解決 するには、「通知のアプリケーション構成 」セクションを確認してください。

{
    "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 トークンに対して検証を実行します。 合格しないトークンが含まれる変更通知は不審な通知とみなし、詳しく調査します。

トークンとトークンを生成するアプリを検証するには、次の手順に従います。

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

  2. Microsoft ID プラットフォームがトークンを発行し、改ざんされていないことを検証します。

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

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

  3. アプリに対してトークンが発行されたことを確認します。

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

    • トークン内の "audience" がアプリ ID と一致していることを確認します。
    • 変更通知を受け取るアプリが複数ある場合は、複数の ID について確認する必要があります。
  4. トークンの azp プロパティが、Microsoft Graph 変更通知発行元を表す 0bf30f3b-4a52-48df-9a82-234910c4a086 の予期される値と一致することを検証します。

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 によって暗号化された完全なリソース データがあります。 このプロパティには、確認と復号に必要な値も含まれています。 この暗号化は、変更通知を介してアクセスされる顧客データのセキュリティを強化するために行われます。 秘密キーをセキュリティで保護して、元の変更通知を傍受した場合でも、サード パーティが顧客データの暗号化を解除できないようにします。

このセクションでは、次の概念について説明します。

暗号化キーの管理

  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 段階の暗号化プロセスを使用しています。

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

変更通知の項目ごとに対称キーが異なると仮定します。

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

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

  2. 秘密キーを使用して RSA 暗号化コンポーネントを初期化します。 RSA コンポーネントを簡単に初期化する方法は、「暗号化キーの管理」で説明されている秘密キーを含む X509Certificate2 インスタンスで RSACertificateExtensions.GetRSAPrivateKey(X509Certificate2) メソッドを使用することです。

  3. 秘密キーを使用して、変更通知の各項目の dataKey プロパティの対称キーを復号化します。 暗号化解除アルゴリズムとして最適な非対称暗号化パディング (OAEP) を使用します。

  4. 対称キーを使用して、データ内の値の HMAC-SHA256 署名を計算 します。 その署名を dataSignature の値と比較します。 一致しない場合は、ペイロードが改ざんされていると想定し、復号化しないでください。

  5. .NET Aes などの Advanced Encryption Standard (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, RSAEncryptionPadding.OaepSHA1);

// 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.