Настройка уведомлений об изменениях 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-адресе ресурса подписки.

Настройка приложения для уведомлений

Перед созданием подписки с данными ресурса настройте доступ к приложению для объекта субъекта-службы, представляющего пару "клиент— приложение", задав свойство appRoleAssignmentRequired следующим образом:

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

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

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

  • 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 содержит два маркера, требующих проверки.

Совет

Значение nullvalidationTokens указывает, что 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.

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

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

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

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

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

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

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

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

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

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

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

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

    • Убедитесь, что "аудитория" в маркере совпадает с идентификатором вашего приложения.
    • Если уведомления об изменениях получают несколько приложений, выполните проверку по нескольким идентификаторам.
  4. Проверьте удостоверение вызывающего маркера, чтобы убедиться, что уведомление поступает из службы уведомлений об изменениях Microsoft Graph. Ожидаемое удостоверение вызывающего — 0bf30f3b-4a52-48df-9a82-234910c4a086. Утверждение, используемое для представления вызывающего приложения, зависит от версии маркера:

    • Для маркеров версии 1.0 (ver = "1.0") удостоверение вызывающего пользователя представлено утверждением appid .
    • Для маркеров версии 2.0 (ver = "2.0") удостоверение вызывающего пользователя представлено утверждением azp .

    Важно!

    Сбой проверки соответствующего утверждения может привести к принятию уведомлений от недоверенного издателя.

Пример маркера JWT

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

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