Поделиться через


Настройка уведомлений об изменениях Microsoft Graph с данными ресурсов

Microsoft Graph позволяет приложениям подписываться на ресурсы и получать уведомления об изменениях в ресурсах. В этой статье объясняется, как настроить расширенные уведомления, которые включают данные ресурсов непосредственно в полезные данные уведомления.

Расширенные уведомления устраняют необходимость в дополнительных вызовах API для получения обновленных ресурсов, что ускоряет и упрощает выполнение бизнес-логики.

Поддерживаемые ресурсы

Расширенные уведомления доступны для следующих ресурсов.

Примечание.

Расширенные уведомления для подписок на конечные точки, помеченные звездочкой (*), доступны только в конечной точке /beta .

Ресурс Поддерживаемые пути к ресурсам Ограничения
Copilot aiInteraction Взаимодействие с ИИ Copilot, частью чего является конкретный пользователь: copilot/users/{userId}/interactionHistory/getAllEnterpriseInteractions

Взаимодействие Copilot AI в организации: copilot/interactionHistory/getAllEnterpriseInteractions
Квоты максимальной подписки:
  • Сочетание приложения и клиента (для подписок, отслеживая взаимодействие ИИ в клиенте): 1
  • Для каждого приложения и пользователя (для подписок, отслеживающего взаимодействие СИ, частью является конкретный пользователь): 1
  • На пользователя (для подписок, отслеживаемых взаимодействием СИ, частью является конкретный пользователь): 10 подписок.
  • На организацию: всего 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.
    Вызовы TeamsRecording Все записи в организации: communications/onlineMeetings/getAllRecordings

    Все записи для определенного собрания: communications/onlineMeetings/{onlineMeetingId}/recordings

    Запись звонка, которая становится доступной на собрании, организованном определенным пользователем: users/{id}/onlineMeetings/getAllRecordings

    Запись звонка, которая становится доступной на собрании, где установлено определенное приложение Teams: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllRecordings *
    Квоты максимальной подписки:
  • Для каждого приложения и сочетания онлайн-собраний: 1
  • Для каждого приложения и пользователя: 1
  • На пользователя (для записей отслеживания подписок во всех onlineMeetings, организованных пользователем): 10 подписок.
  • На организацию: всего 10 000 подписок.
  • Вызов TeamsTranscript Все расшифровки в организации: communications/onlineMeetings/getAllTranscripts

    Все расшифровки для определенного собрания: communications/onlineMeetings/{onlineMeetingId}/transcripts

    Расшифровка звонка, которая становится доступной на собрании, организованном определенным пользователем: users/{id}/onlineMeetings/getAllTranscripts

    Расшифровка звонка, которая становится доступной на собрании, где установлено определенное приложение Teams: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllTrancripts *
    Квоты максимальной подписки:
  • Для каждого приложения и сочетания онлайн-собраний: 1
  • Для каждого приложения и пользователя: 1
  • На пользователя (для подписок, отслеживающих расшифровки во всех onlineMeetings, упорядоченных пользователем): 10 подписок.
  • На организацию: всего 10 000 подписок.
  • Канал Teams Изменения каналов во всех командах: /teams/getAllChannels

    Изменения канала в определенной команде: /teams/{id}/channels
    -
    Чат Teams Изменения в любом чате в клиенте: /chats

    Изменения в конкретном чате: /chats/{id}
    -
    chatMessage Teams Изменения в сообщениях чата во всех каналах во всех командах: /teams/getAllMessages

    Изменения в сообщениях чата в определенном канале: /teams/{id}/channels/{id}/messages

    Изменения в сообщениях чата во всех чатах: /chats/getAllMessages

    Изменения в сообщениях чата в определенном чате: /chats/{id}/messages

    Изменения в сообщениях чата во всех чатах, в которые входит конкретный пользователь: /users/{id}/chats/getAllMessages
    Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра.
    conversationMember в Teams Изменения членства в определенной команде: /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 для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра. Одна подписка разрешена для каждого приложения на собрание по сети. Дополнительные сведения см. в разделе Получение уведомлений об изменениях для обновлений событий звонков в Microsoft Teams.
    presence в Teams Изменения в присутствии одного пользователя: /communications/presences/{id}

    Изменения в присутствии нескольких пользователей: /communications/presences?$filter=id in ({id},{id}...)
    Подписка на присутствие нескольких пользователей ограничена 650 отдельными пользователями. Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра. Допускается одна подписка на одно приложение на каждого делегированного пользователя. Дополнительные сведения см. в разделе Получение уведомлений об изменениях для обновлений присутствия в Microsoft Teams.
    Команда Teams Изменения в любой команде в клиенте: /teams

    Изменения в конкретной команде: /teams/{id}
    -

    Сведения ресурсов в полезных данных уведомлений

    Расширенные уведомления включают данные ресурсов со следующими сведениями:

    • Идентификатор и тип измененного экземпляра ресурса, найденные в свойстве resourceData .
    • Все значения свойств экземпляра ресурса, зашифрованные, как указано в подписке, находятся в свойстве encryptedContent .
    • Конкретные свойства ресурса в зависимости от ресурса или запроса с помощью $select параметра в URL-адресе ресурса подписки.

    Создание подписки

    Чтобы настроить расширенные уведомления, выполните те же действия, что и базовые уведомления об изменениях, но укажите следующие обязательные свойства:

    • includeResourceData: задайте значение для true запроса данных ресурсов.
    • encryptionCertificate. Укажите открытый ключ, который Microsoft Graph использует для шифрования данных ресурса. Дополнительные сведения см. в статье Расшифровка данных ресурсов из уведомлений об изменениях.
    • encryptionCertificateId. Укажите идентификатор сертификата для сопоставления уведомлений с правильным ключом расшифровки.

    Проверьте обе конечные точки, как описано в ст. Проверка конечной точки уведомлений. Если вы используете один и тот же URL-адрес для обеих конечных точек, вы получите и должны ответить на два запроса на проверку.

    Пример. Запрос подписки

    В этом примере создается подписка на сообщения канала в 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 , как описано в разделе Обработка уведомления об изменениях. Для расширенных уведомлений выполните дополнительные действия по проверке.

    Маркеры проверки в уведомлении об изменениях

    Расширенные уведомления включают свойство validationTokens , содержащее массив веб-маркеров JSON (JWT). Каждый токен уникален для пары приложения и клиента. Уведомление об изменениях может содержать набор элементов для различных приложений и клиентов, которые подписаны с помощью одного и того же notificationUrl.

    Примечание.

    Microsoft Graph не отправляет маркеры проверки для уведомлений об изменениях, доставленных через Центры событий Azure, так как службе подписки не нужно проверять notificationUrl для Центров событий.

    В следующем примере уведомление об изменении содержит два элемента для одного приложения и двух разных клиентов, поэтому массив validationTokens содержит два маркера, требующих проверки.

    {
        "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.

    Способ проверки

    Используйте библиотеку проверки подлинности Майкрософт (MSAL) или стороннюю библиотеку для проверки маркеров. Выполните следующие действия:

    Помните о следующих принципах:

    • Немедленно ответьте на уведомление кодом HTTP 202 Accepted состояния.
    • Ответьте перед проверкой уведомления об изменениях, даже если проверка завершается неудачей позже. Отвечайте сразу после получения уведомления об изменениях, независимо от того, храните ли вы уведомления в очередях для последующей обработки или обработки на лету.
    • Принятие уведомления об изменениях и реагирование на них предотвращает ненужные повторные попытки доставки и скрывает результаты проверки от потенциальных злоумышленников. Вы всегда можете игнорировать уведомление о недопустимом изменении после его получения.

    В частности, выполняйте проверку каждого маркера JWT в коллекции validationTokens. Если любой из маркеров не прошел проверку, считайте уведомление об изменении подозрительным и выполните дальнейшее исследование.

    Выполните следующие действия, чтобы проверить маркеры и приложения, которые их создают:

    1. Убедитесь, что срок действия маркера не истек.

    2. Убедитесь, что платформа удостоверений Майкрософт выдал маркер и что он не был изменен.

      • Получите ключи подписи от общей конечной точки конфигурации: https://login.microsoftonline.com/common/.well-known/openid-configuration. Приложение может кэшировать эту конфигурацию в течение некоторого времени. Конфигурация часто обновляется, так как ключи подписывания сменяются ежедневно.
      • Проверьте подпись маркера JWT, использующего эти ключи.

      Не принимайте маркеры, выданные каким-либо другим центром.

    3. Убедитесь, что маркер был выдан для вашего приложения.

      Следующие действия являются частью стандартной логики проверки в библиотеках маркеров JWT, и обычно их можно выполнять в виде вызова одной функции.

      • Убедитесь, что "аудитория" в маркере совпадает с идентификатором вашего приложения.
      • Если уведомления об изменениях получают несколько приложений, выполните проверку по нескольким идентификаторам.
    4. Убедитесь, что свойство маркера 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 в уведомлении об изменениях содержит базовые сведения об идентификаторе и типе экземпляра ресурса. Свойство encryptedData содержит полные данные ресурса, зашифрованные Microsoft Graph с открытым ключом, указанным в подписке. Это свойство также содержит значения, необходимые для проверки и расшифровки. Это шифрование выполняется для повышения безопасности данных клиентов, к которые обращаются через уведомления об изменениях. Защитите закрытый ключ, чтобы третья сторона не могла расшифровать данные клиента, даже если она перехватывает исходные уведомления об изменениях.

    В этом разделе вы узнаете о следующих понятиях:

    Управление ключами шифрования

    1. Получите сертификат с парой асимметричных ключей.

      • Вы можете использовать самозаверяющий сертификат, так как Microsoft Graph не проверяет издателя сертификата и использует открытый ключ только для шифрования.

      • Используйте Azure Key Vault для создания, смены сертификатов и безопасного управления ими. Убедитесь, что ключи удовлетворяют следующим условиям:

        • Ключ должен иметь тип RSA.
        • Размер ключа должен находиться в диапазоне от 2048 до 4096 бит.
    2. Экспортируйте сертификат в формате X.509 в кодировке Base64 и включите только открытый ключ.

    3. При создании подписки:

      • Укажите сертификат в свойстве encryptionCertificate , используя содержимое в кодировке Base64, в которое был экспортирован сертификат.

      • Укажите ваш собственный идентификатор в свойстве encryptionCertificateId.

        Этот идентификатор позволяет сопоставлять сертификаты с получаемыми уведомлениями об изменениях, а также получать сертификаты из хранилища сертификатов. Длина идентификатора не должна превышать 128 символов.

    4. Обеспечьте защиту закрытого ключа, чтобы ваш код обработки уведомлений об изменениях мог обращаться к закрытому ключу для расшифровки данных ресурсов.

    Ротация ключей

    Периодически изменяйте асимметричные ключи, чтобы свести к минимуму риск компрометации закрытого ключа. Чтобы добавить новую пару ключей, выполните следующие действия:

    1. Получите новый сертификат с новой парой асимметричных ключей. Используйте его для всех создаваемых подписок.

    2. Обновите существующие подписки с использованием нового ключа сертификата.

      • Сделайте это обновление частью регулярного продления подписки.
      • Или перечислите все подписки и укажите ключ. Используйте операцию PATCH для подписки и обновите свойства encryptionCertificate и encryptionCertificateId.
    3. Помните о следующих принципах.

      • Старый сертификат может по-прежнему использоваться для шифрования в течение некоторого времени. Чтобы расшифровать контент, у вашего приложения должен быть доступ как к старым, так и к новым сертификатам.
      • Используйте свойство encryptionCertificateId в каждом уведомлении об изменении, чтобы определить правильный ключ для использования.
      • Отмена старого сертификата только в том случае, если вы не видите последних уведомлений об изменениях, ссылающихся на него.

    Расшифровка данных ресурсов

    Microsoft Graph использует двухэтапный процесс шифрования с целью оптимизации работы:

    • Он создает одноразовый симметричный ключ и использует его для шифрования данных ресурсов.
    • Используется открытый асимметричный ключ (указанный при подписке), чтобы зашифровать симметричный ключ и добавить его в каждое уведомление об изменении этой подписки.

    Предположим, симметричный ключ отличается для каждого элемента в уведомлении об изменении.

    Чтобы расшифровать данные ресурсов, приложение должно выполнить обратные действия, используя свойства в разделе encryptedContent в каждом уведомлении об изменении:

    1. Определите правильный сертификат с помощью свойства encryptionCertificateId .

    2. Инициализация криптографического компонента RSA с помощью закрытого ключа. Простой способ инициализации компонента RSA — использовать метод RSACertificateExtensions.GetRSAPrivateKey(X509Certificate2) с экземпляром X509Certificate2 , который содержит закрытый ключ, описанный в разделе Управление ключами шифрования.

    3. Расшифруйте симметричный ключ в свойстве dataKey каждого элемента в уведомлении об изменениях с помощью закрытого ключа. Используйте оптимальное заполнение асимметричного шифрования (OAEP) в качестве алгоритма расшифровки.

    4. Используйте симметричный ключ для вычисления сигнатуры HMAC-SHA256 для значения в данных. Сравните его со значением в объекте dataSignature. Если они не совпадают, предположим, что полезные данные были изменены и не расшифровывайте их.

    5. Расшифруйте свойство данных с помощью симметричного ключа с помощью расширенного Standard шифрования (AES), например Aes .NET.

      • Используйте следующие параметры расшифровки для алгоритма 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.