Настройка уведомлений об изменениях, включающих данные ресурсов (расширенные уведомления)
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 * |
Квоты максимальной подписки: |
Вызов TeamsTranscript | Все расшифровки в организации: communications/onlineMeetings/getAllTranscripts Все расшифровки для определенного собрания: communications/onlineMeetings/{onlineMeetingId}/transcripts Расшифровка звонка, которая становится доступной на собрании, организованном определенным пользователем: users/{id}/onlineMeetings/getAllTranscripts Расшифровка звонка, которая становится доступной на собрании, где установлено определенное приложение Teams: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllTrancripts * |
Квоты максимальной подписки: |
Канал 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. Если любой из маркеров не прошел проверку, считайте уведомление об изменении подозрительным и выполните дальнейшее исследование.
Для проверки маркеров и приложений, создающих маркеры, выполните следующие действия.
Убедитесь, что срок действия маркера не истек.
Убедитесь, что платформа удостоверений Майкрософт выдал маркер и что маркер не был изменен.
- Получите ключи подписи от общей конечной точки конфигурации:
https://login.microsoftonline.com/common/.well-known/openid-configuration
. Приложение может кэшировать эту конфигурацию в течение некоторого времени. Конфигурация часто обновляется, так как ключи подписывания сменяются ежедневно. - Проверьте подпись маркера JWT, использующего эти ключи.
Не принимайте маркеры, выданные каким-либо другим центром.
- Получите ключи подписи от общей конечной точки конфигурации:
Проверьте, что маркер выпущен для вашего приложения, подписавшегося на уведомления об изменениях.
Следующие действия являются частью стандартной логики проверки в библиотеках маркеров JWT, и обычно их можно выполнять в виде вызова одной функции.
- Убедитесь, что "аудитория" в маркере совпадает с идентификатором вашего приложения.
- Если уведомления об изменениях получают несколько приложений, выполните проверку по нескольким идентификаторам.
Важно. Убедитесь, что приложение, создавшее маркер, представляет издателя уведомления об изменениях 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 с использованием открытого ключа, указанного в подписке. Это свойство также содержит значения, необходимые для проверки и расшифровки. Это шифрование выполняется для повышения безопасности данных клиентов, к которые обращаются через уведомления об изменениях. Вы несете ответственность за защиту закрытого ключа, чтобы третья сторона не могла расшифровать данные клиента, даже если ей удастся перехватить исходные уведомления об изменениях.
В этом разделе вы узнаете о следующих понятиях:
- Управление ключами шифрования
- Расшифровка данных ресурсов
- Пример: расшифровка уведомления с зашифрованными данными ресурсов
Управление ключами шифрования
Получите сертификат с парой асимметричных ключей.
Вы можете использовать самозаверяющий сертификат, так как Microsoft Graph не проверяет издателя сертификата и использует открытый ключ только для шифрования.
Используйте Azure Key Vault для создания, смены сертификатов и безопасного управления ими. Убедитесь, что ключи удовлетворяют следующим условиям:
- Ключ должен иметь тип
RSA
. - Размер ключа должен находиться в диапазоне от 2048 до 4096 бит.
- Ключ должен иметь тип
Экспортируйте сертификат в формате X.509 в кодировке Base64 и включите только открытый ключ.
При создании подписки:
Укажите сертификат в свойстве encryptionCertificate , используя содержимое в кодировке Base64, в которое был экспортирован сертификат.
Укажите ваш собственный идентификатор в свойстве encryptionCertificateId.
Этот идентификатор позволяет сопоставлять сертификаты с получаемыми уведомлениями об изменениях, а также получать сертификаты из хранилища сертификатов. Длина идентификатора не должна превышать 128 символов.
Обеспечьте защиту закрытого ключа, чтобы ваш код обработки уведомлений об изменениях мог обращаться к закрытому ключу для расшифровки данных ресурсов.
Ротация ключей
Чтобы уменьшить риск компрометации закрытого ключа, периодически изменяйте ассиметричные ключи. Чтобы добавить новую пару ключей, выполните следующие действия:
Получите новый сертификат с новой парой асимметричных ключей. Используйте его для всех создаваемых подписок.
Обновите существующие подписки с использованием нового ключа сертификата.
- Сделайте это обновление частью регулярного продления подписки.
- Или перечислите все подписки и укажите ключ. Используйте операцию PATCH для подписки и обновите свойства encryptionCertificate и encryptionCertificateId.
Помните о следующих принципах.
- В течение некоторого времени старый сертификат может по-прежнему использоваться для шифрования. Чтобы расшифровать контент, у вашего приложения должен быть доступ как к старым, так и к новым сертификатам.
- Используйте свойство encryptionCertificateId в каждом уведомлении об изменении, чтобы определить правильный ключ для использования.
- Отмена старого сертификата только в том случае, если вы не видите последних уведомлений об изменениях, ссылающихся на него.
Расшифровка данных ресурсов
Microsoft Graph использует двухэтапный процесс шифрования с целью оптимизации работы:
- Он создает одноразовый симметричный ключ и использует его для шифрования данных ресурсов.
- Используется открытый асимметричный ключ (указанный при подписке), чтобы зашифровать симметричный ключ и добавить его в каждое уведомление об изменении этой подписки.
Всегда предполагайте, что для каждого элемента в уведомлении об изменении используется разный симметричный ключ.
Чтобы расшифровать данные ресурсов, приложение должно выполнить обратные действия, используя свойства в разделе encryptedContent в каждом уведомлении об изменении:
Используйте свойство encryptionCertificateId, чтобы определить сертификат для использования.
Инициализация криптографического компонента RSA с помощью закрытого ключа. Простой способ инициализации компонента RSA — использовать метод RSACertificateExtensions.GetRSAPrivateKey(X509Certificate2) с экземпляром X509Certificate2 , который содержит закрытый ключ, описанный в разделе Управление ключами шифрования.
Расшифруйте симметричный ключ, указанный в свойстве dataKey каждого элемента в уведомлении об изменении.
Используйте оптимальное асимметричное шифрование с дополнением (OAEP) в качестве алгоритма расшифровки.
Используйте симметричный ключ, чтобы вычислить подпись HMAC-SHA256 значения в объекте data.
Сравните его со значением в объекте dataSignature. Если они не совпадают, предположим, что полезные данные были изменены и не расшифровывайте их.
Используйте симметричный ключ с расширенным стандартом шифрования (AES) (например, .NET Aes) для расшифровки содержимого в данных.
Используйте следующие параметры расшифровки для алгоритма AES:
- Дополнение: PKCS7
- Режим шифрования: CBC
Настройте "вектор инициализации", скопировав первые 16 байт симметричного ключа, использованного для расшифровки.
Расшифрованное значение — это строка JSON, представляющая экземпляр ресурса в уведомлении об изменении.
Пример: расшифровка уведомления с зашифрованными данными ресурсов
В следующем примере JSON показано уведомление об изменении, содержащее зашифрованные значения свойств экземпляра chatMessage в сообщении канала. Значение @odata.id
указывает экземпляр .
{
"value": [
{
"subscriptionId": "76222963-cc7b-42d2-882d-8aaa69cb2ba3",
"changeType": "created",
// Other properties typical in a resource change notification
"resource": "teams('d29828b8-c04d-4e2a-b2f6-07da6982f0f0')/channels('19:f127a8c55ad949d1a238464d22f0f99e@thread.skype')/messages('1565045424600')/replies('1565047490246')",
"resourceData": {
"id": "1565293727947",
"@odata.type": "#Microsoft.Graph.ChatMessage",
"@odata.id": "teams('88cbc8fc-164b-44f0-b6a6-b59b4a1559d3')/channels('19:8d9da062ec7647d4bb1976126e788b47@thread.tacv2')/messages('1565293727947')/replies('1565293727947')"
},
"encryptedContent": {
"data": "{encrypted data that produces a full resource}",
"dataSignature": "<HMAC-SHA256 hash>",
"dataKey": "{encrypted symmetric key from Microsoft Graph}",
"encryptionCertificateId": "MySelfSignedCert/DDC9651A-D7BC-4D74-86BC-A8923584B0AB",
"encryptionCertificateThumbprint": "07293748CC064953A3052FB978C735FB89E61C3D"
}
}
],
"validationTokens": [
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSU..."
]
}
Полное описание данных, отправляемых при доставке уведомлений об изменениях, см. в разделе Тип ресурса changeNotificationCollection.
Расшифровка симметричного ключа
В этом разделе содержатся некоторые полезные фрагменты кода, использующие C# и .NET для каждого этапа расшифровки.
// Initialize with the private key that matches the encryptionCertificateId.
X509Certificate2 certificate = <instance of X509Certificate2 matching the encryptionCertificateId property>;
RSA rsa = certificate.GetRSAPrivateKey();
byte[] encryptedSymmetricKey = Convert.FromBase64String(<value from dataKey property>);
// Decrypt using OAEP padding.
byte[] decryptedSymmetricKey = rsa.Decrypt(encryptedSymmetricKey, RSAEncryptionPadding.OaepSHA1);
// Can now use decryptedSymmetricKey with the AES algorithm.
Сравнение подписи данных с помощью HMAC-SHA256
byte[] decryptedSymmetricKey = <the aes key decrypted in the previous step>;
byte[] encryptedPayload = <the value from the data property, still encrypted>;
byte[] expectedSignature = <the value from the dataSignature property>;
byte[] actualSignature;
using (HMACSHA256 hmac = new HMACSHA256(decryptedSymmetricKey))
{
actualSignature = hmac.ComputeHash(encryptedPayload);
}
if (actualSignature.SequenceEqual(expectedSignature))
{
// Continue with decryption of the encryptedPayload.
}
else
{
// Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
}
Расшифровка содержимого данных ресурсов
Aes aesProvider = Aes.Create();
aesProvider.Key = decryptedSymmetricKey;
aesProvider.Padding = PaddingMode.PKCS7;
aesProvider.Mode = CipherMode.CBC;
// Obtain the initialization vector from the symmetric key itself.
int vectorSize = 16;
byte[] iv = new byte[vectorSize];
Array.Copy(decryptedSymmetricKey, iv, vectorSize);
aesProvider.IV = iv;
byte[] encryptedPayload = Convert.FromBase64String(<value from data property>);
string decryptedResourceData;
// Decrypt the resource data content.
using (var decryptor = aesProvider.CreateDecryptor())
{
using (MemoryStream msDecrypt = new MemoryStream(encryptedPayload))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
decryptedResourceData = srDecrypt.ReadToEnd();
}
}
}
}
// decryptedResourceData now contains a JSON string that represents the resource.