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

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

В этой статье описывается процесс настройки расширенных уведомлений в приложении.

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

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

Примечание.

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

Resource Поддерживаемые пути к ресурсам Ограничения
Событие 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/?$filter=JoinWebUrl eq '{joinWebUrl} * Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра.
    presence в Teams Изменения в присутствии одного пользователя: /communications/presences/{id} Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра.
    Команда Teams Изменения в любой команде в клиенте: /teams

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

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

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

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

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

    Расширенные уведомления настраиваются так же, как и базовые уведомления об изменениях.

    Для обеспечения безопасности Microsoft Graph шифрует данные ресурсов, возвращаемые в расширенном уведомлении. При создании подписки необходимо предоставить открытый ключ шифрования. Дополнительные сведения о создании ключей шифрования и управлении ими см. в разделе Расшифровка данных ресурсов из уведомлений об изменениях.

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

    • 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": "{custom ID}",
      "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
      "clientState": "{secret client state}"
    }
    

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

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

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

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

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

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

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

    Содержание:

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

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

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

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

    {
        "value": [
            {
                "subscriptionId": "76619225-ff6b-4489-96ca-4ef547e78b22",
                "tenantId": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
                "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.

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

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

    Ниже приведен пример свойств, входящих в маркер JWT, которые требуется проверить.

    {
      // aud is your app's id 
      "aud": "8e460676-ae3f-4b1e-8790-ee0fb5d6148f",                           
      "iss": "https://sts.windows.net/84bd8158-6d4d-4958-8b9f-9d6445542f95/",
      "iat": 1565046813,
      "nbf": 1565046813,
      // Expiration date 
      "exp": 1565075913,                                                        
      "aio": "42FgYKhZ+uOZrHa7p+7tfruauq1HAA==",
      // appid represents the notification publisher and must always be the same value of 0bf30f3b-4a52-48df-9a82-234910c4a086 
      "appid": "0bf30f3b-4a52-48df-9a82-234910c4a086",                          
      "appidacr": "2",
      "idp": "https://sts.windows.net/84bd8158-6d4d-4958-8b9f-9d6445542f95/",
      "tid": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
      "uti": "-KoJHevhgEGnN4kwuixpAA",
      "ver": "1.0"
    }
    

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

    // 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 (например, .NET RSACryptoServiceProvider) с помощью закрытого ключа.

    3. Расшифруйте симметричный ключ, указанный в свойстве dataKey каждого элемента в уведомлении об изменении.

      Используйте оптимальное асимметричное шифрование с дополнением (OAEP) в качестве алгоритма расшифровки.

    4. Используйте симметричный ключ, чтобы вычислить подпись HMAC-SHA256 значения в объекте data.

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

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

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

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

    6. Расшифрованное значение — это строка 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.
    RSACryptoServiceProvider rsaProvider = ...;        
    byte[] encryptedSymmetricKey = Convert.FromBase64String(<value from dataKey property>);
    
    // Decrypt using OAEP padding.
    byte[] decryptedSymmetricKey = rsaProvider.Decrypt(encryptedSymmetricKey, fOAEP: true);
    
    // Can now use decryptedSymmetricKey with the AES algorithm.
    

    Сравнение подписи данных с помощью 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.
    }
    

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

    AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider();
    aesProvider.Key = decryptedSymmetricKey;
    aesProvider.Padding = PaddingMode.PKCS7;
    aesProvider.Mode = CipherMode.CBC;
    
    // Obtain the intialization vector from the symmetric key itself.
    int vectorSize = 16;
    byte[] iv = new byte[vectorSize];
    Array.Copy(decryptedSymmetricKey, iv, vectorSize);
    aesProvider.IV = iv;
    
    byte[] encryptedPayload = Convert.FromBase64String(<value from 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.