Configurar notificações de alteração que incluem dados de recursos (notificações avançadas)
Artigo
31/01/2025
O Microsoft Graph permite que as aplicações subscrevam e recebam notificações de alterações aos recursos em que estão interessadas. Embora possa subscrever notificações de alteração básicas, os recursos, como a mensagem de chat do Microsoft Teams e os recursos de presença, por exemplo, suportam notificações avançadas.
As notificações avançadas incluem os dados de recursos que foram alterados, permitindo que a sua aplicação execute lógica de negócio sem ter de fazer uma chamada à API separada para obter o recurso alterado. Este artigo orienta-o ao longo do processo de configuração de notificações avançadas na sua aplicação.
Recursos com suporte
Estão disponíveis notificações avançadas para os seguintes recursos.
Observação
As notificações avançadas para subscrições para pontos finais marcadas com um asterisco (*) só estão disponíveis no /beta ponto final.
Todas as gravações numa organização: communications/onlineMeetings/getAllRecordings
Todas as gravações para uma reunião específica: communications/onlineMeetings/{onlineMeetingId}/recordings
Uma gravação de chamada que fica disponível numa reunião organizada por um utilizador específico: users/{id}/onlineMeetings/getAllRecordings
Uma gravação de chamada que fica disponível numa reunião onde está instalada uma determinada aplicação do Teams: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllRecordings *
Cotas máximas de assinaturas:
Combinação por aplicação e reunião online: 1
Combinação por aplicação e utilizador: 1
Por utilizador (para subscrições que monitorizam as gravações em todos os OnlineMeetings organizados pelo utilizador): 10 subscrições.
Todas as transcrições numa organização: communications/onlineMeetings/getAllTranscripts
Todas as transcrições de uma reunião específica: communications/onlineMeetings/{onlineMeetingId}/transcripts
Uma transcrição de chamada que fica disponível numa reunião organizada por um utilizador específico: users/{id}/onlineMeetings/getAllTranscripts
Uma transcrição de chamadas que fica disponível numa reunião onde está instalada uma determinada aplicação do Teams: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllTrancripts *
Cotas máximas de assinaturas:
Combinação por aplicação e reunião online: 1
Combinação por aplicação e utilizador: 1
Por utilizador (para subscrições que monitorizam transcrições em todos os OnlineMeetings organizados pelo utilizador): 10 subscrições.
Alterações às mensagens de chat em todos os canais em todas as equipas: /teams/getAllMessages
Alterações às mensagens de chat num canal específico: /teams/{id}/channels/{id}/messages
Alterações às mensagens de chat em todas as conversas: /chats/getAllMessages
Alterações às mensagens de chat num chat específico: /chats/{id}/messages
As alterações às mensagens de chat em todas as conversas dos utilizadores específicos fazem parte: /users/{id}/chats/getAllMessages
Não suporta a utilização $select para devolver apenas as propriedades selecionadas. A notificação avançada consiste em todas as propriedades da instância alterada.
Alterações à presença de um único utilizador: /communications/presences/{id}
Alterações à presença de vários utilizadores: /communications/presences?$filter=id in ({id},{id}...)
A subscrição para a presença de vários utilizadores está limitada a 650 utilizadores distintos. Não suporta a utilização $select para devolver apenas as propriedades selecionadas. A notificação avançada consiste em todas as propriedades da instância alterada. Uma subscrição permitida por aplicação por utilizador delegado. Para obter mais informações, consulte Obter notificações de alteração para atualizações de presença no Microsoft Teams.
As notificações avançadas incluem os seguintes dados de recursos no payload:
ID e tipo de instância de recurso alterado, retornados na propriedade resourceData.
Todos os valores de propriedade da instância de recurso, criptografados conforme especificado na assinatura, retornados na propriedade encryptedContent.
Or, dependendo do recurso, propriedades específicas retornadas na propriedade resourceData. Para obter somente propriedades específicas, especifique-as como parte da URL do recurso na assinatura, usando um parâmetro $select.
Criar uma assinatura
As notificações avançadas são configuradas da mesma forma que as notificações de alteração básicas, exceto que tem de especificar as seguintes propriedades:
includeResourceData que deve ser definido como true para solicitar explicitamente os dados de recursos.
encryptionCertificate , que contém apenas a chave pública que o Microsoft Graph utiliza para encriptar os dados de recursos devolvidos à sua aplicação. Por motivos de segurança, o Microsoft Graph encripta os dados de recursos devolvidos numa notificação avançada. Tem de fornecer uma chave de encriptação pública como parte da criação da subscrição. Para obter mais informações sobre como criar e gerir chaves de encriptação, veja Desencriptar dados de recursos a partir de notificações de alteração.
encryptionCertificateId é o seu próprio identificador para o certificado. Use esse ID para corresponder a cada notificação de alteração cujo certificado foi utilizado para descriptografia.
Também tem de validar ambos os pontos finais, conforme descrito em Validação do ponto final de notificação. Se optar por utilizar o mesmo URL para ambos os pontos finais, receberá e deverá responder a dois pedidos de validação.
Exemplo de solicitação de assinatura
O exemplo a seguir assina as mensagens de canal que estão sendo criadas ou atualizadas no Microsoft Teams.
Certos eventos podem interferir no fluxo de notificação de alterações em uma assinatura existente. As notificações sobre o ciclo de vida da assinatura informam as ações a serem tomadas para manter um fluxo ininterrupto. Ao contrário de uma notificação de alteração de recursos que informa uma alteração a uma instância de recurso, uma notificação de ciclo de vida é sobre a própria subscrição e o estado atual no ciclo de vida.
Antes de executar a lógica de negócio com base nos dados de recursos incluídos nas notificações de alteração, primeiro tem de verificar a autenticidade de cada notificação de alteração. Caso contrário, um terceiro pode falsificar a sua aplicação com notificações de alteração falsas e fazê-la executar a lógica de negócio incorretamente, o que pode levar a um incidente de segurança.
Para notificações de alteração básicas que não contêm dados de recursos, basta validá-las com base no valor clientState , conforme descrito em Processar a notificação de alteração. Esta validação é aceitável, uma vez que pode fazer chamadas fidedignas subsequentes do Microsoft Graph para obter acesso aos dados de recursos e, por conseguinte, o impacto de quaisquer tentativas de spoofing é limitado.
Para notificações avançadas, execute uma validação mais completa antes de processar os dados.
Nesta secção, vai explorar os seguintes conceitos de validação:
Uma notificação de alteração com dados de recursos contém uma propriedade extra, validationTokens, que contém uma matriz de JSON Web Tokens (JWT) gerados pelo Microsoft Graph. O Microsoft Graph gera um único token para cada par de inquilinos e aplicações diferentes para os quais existe um item na matriz de valores . Tenha em atenção que as notificações de alteração podem conter uma combinação de itens para várias aplicações e inquilinos que subscreveram com o mesmo notificationUrl.
No exemplo a seguir, a notificação de alteração contém dois itens para o mesmo aplicativo e para dois locatários diferentes, portanto, o conjunto validationTokens contém dois tokens que precisam ser validados.
Certifique-se de sempre enviar um HTTP 202 Accepted código de status como parte da resposta à notificação de alteração.
Responda antes de validar a notificação de alteração, mesmo que a validação falhe mais tarde. Ou seja, responda imediatamente ao receber a notificação de alteração, quer armazene notificações em filas para processamento posterior ou processe-as de imediato.
A aceitação de uma notificação de alteração evita novas tentativas desnecessárias de entrega e também impede que possíveis atores invasores descubram se foram ou não aprovados na validação. Pode sempre optar por ignorar uma notificação de alteração inválida depois de a receber.
Especificamente, realize a validação em todos os tokens JWT na coleção validationTokens. Se algum token falhar, considere a notificação de alteração suspeita e investigue mais.
Use as etapas a seguir para validar tokens e aplicativos que geram tokens:
Confirme que o token não expirou.
Confirme que o plataforma de identidade da Microsoft emitiu o token e que o token não foi adulterado.
Obtenha as chaves de assinatura do ponto de extremidade de configuração comum: https://login.microsoftonline.com/common/.well-known/openid-configuration. A sua aplicação pode colocar esta configuração em cache durante algum tempo. A configuração é atualizada frequentemente à medida que as chaves de assinatura são rodadas diariamente.
Verifique a assinatura do token JWT usando essas chaves.
Não aceite tokens emitidos por nenhuma outra autoridade.
Valide se o token foi emitido para o aplicativo que está inscrito para alterar as notificações.
As etapas a seguir fazem parte da lógica de validação padrão nas bibliotecas de token JWT e podem ser tipicamente executadas como uma única chamada de função.
Valide a “audiência” no token que corresponde à ID do seu aplicativo.
Se você tiver mais de um aplicativo recebendo notificações de alterações, verifique a existencia de vários IDs.
Crítico: valide se o aplicativo que gerou o token representa o distribuidor de notificação de alteração do Microsoft Graph.
Verifique se a azp propriedade no token corresponde ao valor esperado de 0bf30f3b-4a52-48df-9a82-234910c4a086.
Este marcar garante que uma aplicação diferente que não seja o Microsoft Graph não enviou as notificações de alteração.
Exemplo de Token JWT
O exemplo seguinte mostra as propriedades incluídas no token JWT que são necessárias para validação.
{
// 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"
}
Descriptografar os dados de recursos de notificações de alteração
A propriedade resourceData de uma notificação de alteração inclui apenas o ID básico e as informações do tipo de uma instância de recurso. A propriedade encryptedData contém os dados de recursos completos, criptografados pelo Microsoft Graph usando a chave pública fornecida na assinatura. A propriedade também contém valores necessários para verificação e descriptografia. Esta encriptação é feita para aumentar a segurança dos dados dos clientes acedidos através de notificações de alteração. É da sua responsabilidade proteger a chave privada para garantir que terceiros não conseguem desencriptar os dados do cliente, mesmo que consigam intercetar as notificações de alteração originais.
Nesta secção, irá aprender os seguintes conceitos:
Obtenha um certificado com um par de chaves assimétricas.
Pode utilizar um certificado autoassinado, uma vez que o Microsoft Graph não verifica o emissor do certificado e utiliza a chave pública apenas para encriptação.
Utilize o Azure Key Vault para criar, rodar e gerir certificados de forma segura. Cerifique-se de que as chaves atendem aos seguintes critérios:
A chave tem de ser do tipo RSA.
O tamanho da chave tem de estar entre 2048 bits e 4096 bits.
Exporte o certificado no formato X.509 codificado com Base64 e inclua apenas a chave pública.
Ao criar uma assinatura:
Forneça o certificado na propriedade encryptionCertificate , utilizando o conteúdo codificado em Base64 no qual o certificado foi exportado.
Forneça seu próprio identificador na propriedade encryptionCertificateId.
Esse identificador permite corresponder seus certificados às notificações de alterações recebidas e recuperar certificados do seu repositório de certificados. O identificador pode ter no máximo 128 caracteres.
Gerencie com segurança a chave privada com para que seu código de processamento de notificação de alteração acesse a chave privada para decriptar os dados do recurso.
Chaves de rotação
Para minimizar o risco de uma chave privada ser comprometida, altere periodicamente suas chaves assimétricas. Siga estas etapas para introduzir um novo par de chaves:
Obtenha um novo certificado com um novo par de chaves assimétricas. Use-o para todas as novas assinaturas sendo criadas.
Atualize as assinaturas existentes com a nova chave de certificado.
Faça desta atualização parte da renovação regular da subscrição.
Ou enumere todas as assinaturas e forneça a chave. Use a operação PATCH na assinatura e atualize as propriedades encryptionCertificate e encryptionCertificateId.
Tenha em atenção os seguintes princípios:
Durante algum tempo, o certificado antigo ainda pode ser utilizado para encriptação. Seu aplicativo deve ter acesso a certificados novos e antigos para poder descriptografar o conteúdo.
Use a propriedade encryptionCertificateId em cada notificação de alteração para identificar a chave correta a ser utilizada.
Elimine o certificado antigo apenas quando não vir notificações de alteração recentes a referenciá-lo.
Descriptografar dados de recursos
Para otimizar o desempenho, o Microsoft Graph usa um processo de criptografia de duas etapas:
Gera uma chave simétrica de utilização única e utiliza-a para encriptar dados de recursos.
Ele usa a chave pública assimétrica (que você forneceu ao fazer a assinatura) para criptografar a chave simétrica e a incluir em cada notificação de alteração desta assinatura.
Sempre assuma que a chave simétrica é diferente para cada item na notificação de alteração.
Para decriptar os dados de recursos, o seu aplicativo deve executar as etapas inversas, usando as propriedades encryptedContent em cada notificação de alteração:
Use a propriedade encryptionCertificateId para identificar o certificado a ser usado.
Decripte a chave simétrica entregue na propriedade dataKey de cada item na notificação de alteração.
Use o Preenchimento de Criptografia Assimétrica Ideal (OAEP) para o algoritmo de descriptografia.
Use a chave simétrica para calcular a assinatura HMAC-SHA256 do valor em dados.
Compare-o com o valor em dataSignature. Se não corresponderem, suponha que o payload foi adulterado e não o desencripte.
Utilize a chave simétrica com um Standard de Encriptação Avançada (AES) (como os .NET Aes) para desencriptar o conteúdo nos dados.
Use os seguintes parâmetros de descriptografia para o algoritmo AES:
Preenchimento: PKCS7
Modo de criptografia: CBC
Defina o “vetor de inicialização” copiando os primeiros 16 bytes da chave simétrica usada para descriptografia.
O valor decriptado é uma sequência JSON que representa a instância do recurso na notificação de alteração.
Exemplo: decriptando uma notificação com dados de recurso criptografados
O exemplo JSON seguinte mostra uma notificação de alteração que inclui valores de propriedade encriptadas de uma instância chatMessage numa mensagem de canal. O @odata.id valor especifica a instância.
{
"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..."
]
}
// 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.
String storename = ""; //name/path of the jks store
String storepass = ""; //password used to open the jks store
String alias = ""; //alias of the certificate when store in the jks store, should be passed as encryptionCertificateId when subscribing and retrieved from the notification
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(storename), storepass.toCharArray());
Key asymmetricKey = ks.getKey(alias, storepass.toCharArray());
byte[] encryptedSymmetricKey = Base64.decodeBase64("<value from dataKey property>");
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, asymmetricKey);
byte[] decryptedSymmetricKey = cipher.doFinal(encryptedSymmetricKey);
// Can now use decryptedSymmetricKey with the AES algorithm.
const base64encodedKey = 'base 64 encoded dataKey value';
const asymmetricPrivateKey = 'pem encoded private key';
const decodedKey = Buffer.from(base64encodedKey, 'base64');
const decryptedSymmetricKey = crypto.privateDecrypt(asymmetricPrivateKey, decodedKey);
// Can now use decryptedSymmetricKey with the AES algorithm.
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.
}
byte[] decryptedSymmetricKey = "<the aes key decrypted in the previous step>";
byte[] decodedEncryptedData = Base64.decodeBase64("data property from encryptedContent object");
Mac mac = Mac.getInstance("HMACSHA256");
SecretKey skey = new SecretKeySpec(decryptedSymmetricKey, "HMACSHA256");
mac.init(skey);
byte[] hashedData = mac.doFinal(decodedEncryptedData);
String encodedHashedData = new String(Base64.encodeBase64(hashedData));
if (comparisonSignature.equals(encodedHashedData))
{
// Continue with decryption of the encryptedPayload.
}
else
{
// Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
}
const decryptedSymmetricKey = []; //Buffer provided by previous step
const base64encodedSignature = 'base64 encoded value from the dataSignature property';
const hmac = crypto.createHmac('sha256', decryptedSymmetricKey);
hmac.write(base64encodedPayload, 'base64');
if(base64encodedSignature === hmac.digest('base64'))
{
// 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.
SecretKey skey = new SecretKeySpec(decryptedSymmetricKey, "AES");
IvParameterSpec ivspec = new IvParameterSpec(Arrays.copyOf(decryptedSymmetricKey, 16));
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, skey, ivspec);
String decryptedResourceData = new String(cipher.doFinal(Base64.decodeBase64(encryptedData)));
const base64encodedPayload = 'base64 encoded value from data property';
const decryptedSymmetricKey = []; //Buffer provided by previous step
const iv = Buffer.alloc(16, 0);
decryptedSymmetricKey.copy(iv, 0, 0, 16);
const decipher = crypto.createDecipheriv('aes-256-cbc', decryptedSymmetricKey, iv);
let decryptedPayload = decipher.update(base64encodedPayload, 'base64', 'utf8');
decryptedPayload += decipher.final('utf8');
Subscreva as notificações de ciclo de vida do Microsoft Graph para o ajudar a minimizar o risco de notificações de alteração perdidas ou subscrições removidas.
Assine as alterações nos recursos do Outlook (criar, atualizar e excluir), altere os dados de recursos nas APIs do Microsoft Graph e receba notificações por meio de webhooks.
As notificações de alteração podem ser entregues através de diferentes canais, incluindo webhooks e Hubs de Eventos do Azure. Este artigo explica-lhe como obter notificações de alteração através de webhooks.