Настройка уведомлений об изменениях, включающих данные ресурсов (расширенные уведомления)

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

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

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

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

Примечание.

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

Ресурс Поддерживаемые пути к ресурсам Ограничения
Событие Outlook Изменения во всех событиях в почтовом ящике пользователя: /users/{id}/events Требует $select возвращать только подмножество свойств в расширенном уведомлении. Дополнительные сведения см. в разделе Уведомления об изменениях для ресурсов Outlook.
Сообщение Outlook Изменения во всех сообщениях в почтовом ящике пользователя: /users/{id}/messages

Изменения в сообщениях в папке "Входящие" пользователя: /users/{id}/mailFolders/{id}/messages
Требует $select возвращать только подмножество свойств в расширенном уведомлении. Дополнительные сведения см. в разделе Уведомления об изменениях для ресурсов Outlook.
Личный контакт Outlook Изменения во всех личных контактах в почтовом ящике пользователя: /users/{id}/contacts

Изменения во всех личных контактах в папке contactFolder пользователя: /users/{id}/contactFolders/{id}/contacts
Требует $select возвращать только подмножество свойств в расширенном уведомлении. Дополнительные сведения см. в разделе Уведомления об изменениях для ресурсов Outlook.
Вызовы 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



    Изменения в членстве в определенном чате: /chats/{id}/members
    -
    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.
    • Или зависимые от ресурса определенные свойства, возвращаемые в свойстве resourceData. Чтобы получить только определенные свойства, их нужно указать в URL-адресе объекта resource в подписке, используя параметр $select.

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

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

    • includeResourceData, которому следует присвоить значение true, чтобы явно запросить данные ресурса.
    • encryptionCertificate , содержащий только открытый ключ, который Microsoft Graph использует для шифрования данных ресурса, возвращаемого в приложение. Для обеспечения безопасности 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": "{custom ID}",
      "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
      "clientState": "{secret client state}"
    }
    

    Уведомления жизненного цикла подписки

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

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

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

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

    Для базовых уведомлений об изменениях, которые не содержат данные ресурсов, просто проверьте их на основе значения clientState , как описано в разделе Обработка уведомления об изменениях. Такая проверка допустима, так как вы можете выполнять последующие доверенные вызовы Microsoft Graph для получения доступа к данным ресурсов, поэтому влияние любых попыток спуфингов ограничено.

    Для расширенных уведомлений выполните более тщательную проверку перед обработкой данных.

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

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

    Уведомление об изменении с данными ресурса содержит дополнительное свойство validationTokens, которое содержит массив веб-маркеров JSON (JWT), созданных Microsoft Graph. Microsoft Graph создает один маркер для каждой отдельной пары приложений и клиентов, для которых в массиве значений есть элемент. Помните, что уведомления об изменениях могут содержать набор элементов для различных приложений и клиентов, подписываемых с помощью одного и того же 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. Важно. Убедитесь, что приложение, создавшее маркер, представляет издателя уведомления об изменениях Microsoft Graph.

      • Убедитесь, что azp свойство в маркере соответствует ожидаемому значению 0bf30f3b-4a52-48df-9a82-234910c4a086.
      • Это проверка гарантирует, что другое приложение, которое не является Microsoft Graph, не отправляет уведомления об изменениях.

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

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

    {
      // aud is your app's id
      "aud": "925bff9f-f6e2-4a69-b858-f71ea2b9b6d0",
      "iss": "https://login.microsoftonline.com/9f4ebab6-520d-49c0-85cc-7b25c78d4a93/v2.0",
      "iat": 1624649764,
      "nbf": 1624649764,
      "exp": 1624736464,
      "aio": "E2ZgYGjnuFglnX7mtjJzwR5lYaWvAA==",
      // azp represents the notification publisher and must always be the same value of 0bf30f3b-4a52-48df-9a82-234910c4a086
      "azp": "0bf30f3b-4a52-48df-9a82-234910c4a086",
      "azpacr": "2",
      "oid": "1e7d79fa-7893-4d50-bdde-164260d9c5ba",
      "rh": "0.AX0AtrpOnw1SwEmFzHslx41KkzsP8wtSSt9ImoIjSRDEoIZ9AAA.",
      "sub": "1e7d79fa-7893-4d50-bdde-164260d9c5ba",
      "tid": "9f4ebab6-520d-49c0-85cc-7b25c78d4a93",
      "uti": "mIB4QKCeZE6hK71XUHJ3AA",
      "ver": "2.0"
    }
    

    Пример: подтверждение маркеров проверки

    // add Microsoft.IdentityModel.Protocols.OpenIdConnect and System.IdentityModel.Tokens.Jwt nuget packages to your project
    public async Task<bool> ValidateToken(string token, string tenantId, IEnumerable<string> appIds)
    {
        var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
            "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
            new OpenIdConnectConfigurationRetriever());
        var openIdConfig = await configurationManager.GetConfigurationAsync();
        var handler = new JwtSecurityTokenHandler();
        try
        {
        handler.ValidateToken(token, new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
            ValidIssuer = $"https://sts.windows.net/{tenantId}/",
            ValidAudiences = appIds,
            IssuerSigningKeys = openIdConfig.SigningKeys
        }, out _);
        return true;
        }
        catch (Exception ex)
        {
        Trace.TraceError($"{ex.Message}:{ex.StackTrace}");
        return false;
        }
    }
    

    Расшифровка данных ресурсов из уведомлений об изменениях

    Свойство resourceData уведомления об изменении содержит только основной идентификатор и сведения о типе экземпляра ресурса. Свойство 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 значения в объекте data.

      Сравните его со значением в объекте dataSignature. Если они не совпадают, предположим, что полезные данные были изменены и не расшифровывайте их.

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

      • Используйте следующие параметры расшифровки для алгоритма AES:

        • Дополнение: PKCS7
        • Режим шифрования: CBC
      • Настройте "вектор инициализации", скопировав первые 16 байт симметричного ключа, использованного для расшифровки.

    6. Расшифрованное значение — это строка JSON, представляющая экземпляр ресурса в уведомлении об изменении.

    Пример: расшифровка уведомления с зашифрованными данными ресурсов

    В следующем примере JSON показано уведомление об изменении, содержащее зашифрованные значения свойств экземпляра chatMessage в сообщении канала. Значение @odata.id указывает экземпляр .

    {
      "value": [
        {
          "subscriptionId": "76222963-cc7b-42d2-882d-8aaa69cb2ba3",
          "changeType": "created",
          // Other properties typical in a resource change notification
          "resource": "teams('d29828b8-c04d-4e2a-b2f6-07da6982f0f0')/channels('19:f127a8c55ad949d1a238464d22f0f99e@thread.skype')/messages('1565045424600')/replies('1565047490246')",
          "resourceData": {
            "id": "1565293727947",
            "@odata.type": "#Microsoft.Graph.ChatMessage",
            "@odata.id": "teams('88cbc8fc-164b-44f0-b6a6-b59b4a1559d3')/channels('19:8d9da062ec7647d4bb1976126e788b47@thread.tacv2')/messages('1565293727947')/replies('1565293727947')"
          },
          "encryptedContent": {
            "data": "{encrypted data that produces a full resource}",
            "dataSignature": "<HMAC-SHA256 hash>",
            "dataKey": "{encrypted symmetric key from Microsoft Graph}",
            "encryptionCertificateId": "MySelfSignedCert/DDC9651A-D7BC-4D74-86BC-A8923584B0AB",
            "encryptionCertificateThumbprint": "07293748CC064953A3052FB978C735FB89E61C3D"
          }
        }
      ],
      "validationTokens": [
        "eyJ0eXAiOiJKV1QiLCJhbGciOiJSU..."
      ]
    }
    

    Полное описание данных, отправляемых при доставке уведомлений об изменениях, см. в разделе Тип ресурса changeNotificationCollection.

    Расшифровка симметричного ключа

    В этом разделе содержатся некоторые полезные фрагменты кода, использующие C# и .NET для каждого этапа расшифровки.

    // Initialize with the private key that matches the encryptionCertificateId.
    X509Certificate2 certificate = <instance of X509Certificate2 matching the encryptionCertificateId property>;
    RSA rsa = certificate.GetRSAPrivateKey();
    byte[] encryptedSymmetricKey = Convert.FromBase64String(<value from dataKey property>);
    
    // Decrypt using OAEP padding.
    byte[] decryptedSymmetricKey = rsa.Decrypt(encryptedSymmetricKey, fOAEP: true);
    
    // Can now use decryptedSymmetricKey with the AES algorithm.
    

    Сравнение подписи данных с помощью HMAC-SHA256

    byte[] decryptedSymmetricKey = <the aes key decrypted in the previous step>;
    byte[] encryptedPayload = <the value from the data property, still encrypted>;
    byte[] expectedSignature = <the value from the dataSignature property>;
    byte[] actualSignature;
    
    using (HMACSHA256 hmac = new HMACSHA256(decryptedSymmetricKey))
    {
        actualSignature = hmac.ComputeHash(encryptedPayload);
    }
    if (actualSignature.SequenceEqual(expectedSignature))
    {
        // Continue with decryption of the encryptedPayload.
    }
    else
    {
        // Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
    }
    

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

    Aes aesProvider = Aes.Create();
    aesProvider.Key = decryptedSymmetricKey;
    aesProvider.Padding = PaddingMode.PKCS7;
    aesProvider.Mode = CipherMode.CBC;
    
    // Obtain the initialization vector from the symmetric key itself.
    int vectorSize = 16;
    byte[] iv = new byte[vectorSize];
    Array.Copy(decryptedSymmetricKey, iv, vectorSize);
    aesProvider.IV = iv;
    
    byte[] encryptedPayload = Convert.FromBase64String(<value from data property>);
    
    string decryptedResourceData;
    // Decrypt the resource data content.
    using (var decryptor = aesProvider.CreateDecryptor())
    {
      using (MemoryStream msDecrypt = new MemoryStream(encryptedPayload))
      {
          using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
          {
              using (StreamReader srDecrypt = new StreamReader(csDecrypt))
              {
                  decryptedResourceData = srDecrypt.ReadToEnd();
              }
          }
      }
    }
    
    // decryptedResourceData now contains a JSON string that represents the resource.