Set up change notifications that include resource data

Microsoft Graph allows apps to subscribe to and receive change notifications for resources through different delivery channels. You can set up subscriptions to include the changed resource data (such as the content of a Microsoft Teams chat message or Microsoft Teams presence information) in change notifications. Change notifications that include the resource change data are called rich notifications. Your app can use rich notifications to run your business logic without having to make a separate API call to fetch the changed resource.

This article guides you through the process for setting up rich notifications in your application.

Supported resources

Rich notifications are available for the following resources.

Note

Rich notifications for subscriptions to endpoints marked with an asterisk (*) are only available on the /beta endpoint.

Resource Supported resource paths Limitations
Outlook event Changes to all events in a user's mailbox: /users/{id}/events Requires $select to return only a subset of properties in the rich notification. For more information, see Change notifications for Outlook resources.
Outlook message Changes to all messages in a user's mailbox: /users/{id}/messages

Changes to messages in a user's inbox: /users/{id}/mailFolders/{id}/messages
Requires $select to return only a subset of properties in the rich notification. For more information, see Change notifications for Outlook resources.
Outlook personal contact Changes to all personal contacts in a user's mailbox: /users/{id}/contacts

Changes to all personal contacts in a user's contactFolder: /users/{id}/contactFolders/{id}/contacts
Requires $select to return only a subset of properties in the rich notification. For more information, see Change notifications for Outlook resources.
Teams callRecording All recordings in an organization: communications/onlineMeetings/getAllRecordings

All recordings for a specific meeting: communications/onlineMeetings/{onlineMeetingId}/recordings

A call recording that becomes available in a meeting organized by a specific user: users/{id}/onlineMeetings/getAllRecordings

A call recording that becomes available in a meeting where a particular Teams app is installed: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllRecordings *
Maximum subscription quotas:
  • Per app and online-meeting combination: 1
  • Per app and user combination: 1
  • Per user (for subscriptions tracking recordings in all onlineMeetings organized by the user): 10 subscriptions.
  • Per organization: 10,000 total subscriptions.
  • Teams callTranscript All transcripts in an organization: communications/onlineMeetings/getAllTranscripts

    All transcripts for a specific meeting: communications/onlineMeetings/{onlineMeetingId}/transcripts

    A call transcript that becomes available in a meeting organized by a specific user: users/{id}/onlineMeetings/getAllTranscripts

    A call transcript that becomes available in a meeting where a particular Teams app is installed: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllTrancripts *
    Maximum subscription quotas:
  • Per app and online-meeting combination: 1
  • Per app and user combination: 1
  • Per user (for subscriptions tracking transcripts in all onlineMeetings organized by the user): 10 subscriptions.
  • Per organization: 10,000 total subscriptions.
  • Teams channel Changes to channels in all teams: /teams/getAllChannels

    Changes to channel in a specific team: /teams/{id}/channels
    -
    Teams chat Changes to any chat in the tenant: /chats

    Changes to a specific chat: /chats/{id}
    -
    Teams chatMessage Changes to chat messages in all channels in all teams: /teams/getAllMessages

    Changes to chat messages in a specific channel: /teams/{id}/channels/{id}/messages

    Changes to chat messages in all chats: /chats/getAllMessages

    Changes to chat messages in a specific chat: /chats/{id}/messages

    Changes to chat messages in all chats a particular user is part of: /users/{id}/chats/getAllMessages
    Doesn't support using $select to return only selected properties. The rich notification consists of all the properties of the changed instance.
    Teams conversationMember Changes to membership in a specific team: /teams/{id}/members



    Changes to membership in a specific chat: /chats/{id}/members
    -
    Teams onlineMeeting * Changes to an online meeting: /communications/onlineMeetings/?$filter=JoinWebUrl eq '{joinWebUrl} * Doesn't support using $select to return only selected properties. The rich notification consists of all the properties of the changed instance.
    Teams presence Changes to a single user's presence: /communications/presences/{id} Doesn't support using $select to return only selected properties. The rich notification consists of all the properties of the changed instance.
    Teams team Changes to any team in the tenant: /teams

    Changes to a specific team: /teams/{id}
    -

    Resource data in notification payload

    In general, this type of change notifications includes the following resource data in the payload:

    • ID and type of the changed resource instance, returned in the resourceData property.
    • All the property values of that resource instance, encrypted as specified in the subscription, returned in the encryptedContent property.
    • Or, depending on the resource, specific properties returned in the resourceData property. To get only specific properties, specify them as part of the resource URL in the subscription, using a $select parameter.

    Creating a subscription

    Rich notifications are set up in the same way as basic change notifications.

    For security, Microsoft Graph encrypts the resource data returned in a rich notification. You must provide a public encryption key as part of creating the subscription. For more information on creating and managing encryption keys, see Decrypting resource data from change notifications.

    To create a subscription that includes rich notifications, you must specify the following properties:

    • includeResourceData which should be set to true to explicitly request resource data.
    • encryptionCertificate which contains only the public key that Microsoft Graph uses to encrypt the resource data it returns to your app.
    • encryptionCertificateId which is your own identifier for the certificate. Use this ID to match in each change notification, which certificate to use for decryption.

    Keep the following in mind:

    • Validate both endpoints as described in Notification endpoint validation. If you choose to use the same URL for both endpoints, you receive and should respond to two validation requests.

    Subscription request example

    The following example subscribes to channel messages being created or updated in 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}"
    }
    

    Subscription response

    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}"
    }
    

    Subscription lifecycle notifications

    Certain events can interfere with change notification flow in an existing subscription. Subscription lifecycle notifications inform you actions to take in order to maintain an uninterrupted flow. Unlike a resource change notification that informs a change to a resource instance, a lifecycle notification is about the subscription itself, and its current state in the lifecycle.

    For more information about how to receive and respond to lifecycle notifications, see Reduce missing subscriptions and change notifications.

    Validating the authenticity of notifications

    Apps often run business logic based on resource data included in change notifications. Verifying the authenticity of each change notification first is important. Otherwise, a third party can spoof your app with false change notifications and make it run its business logic incorrectly, and this can lead to a security incident.

    For basic change notifications that don't contain resource data, simply validate them based on the clientState value as described in Processing the change notification. This is acceptable, as you can make subsequent trusted Microsoft Graph calls to get access to resource data, and therefore the impact of any spoofing attempts is limited.

    For change notifications that deliver resource data, perform a more thorough validation before processing the data.

    In this section:

    Validation tokens in the change notification

    A change notification with resource data contains an additional property, validationTokens, which contains an array of JSON Web Tokens (JWT) generated by Microsoft Graph. Microsoft Graph generates a single token for each distinct app and tenant pair for whom there's an item in the value array. Keep in mind that change notifications might contain a mix of items for various apps and tenants that subscribed using the same notificationUrl.

    Note: If you're setting up change notifications delivered through Azure Event Hubs, Microsoft Graph will not send the validation tokens. Microsoft Graph does not need to validate the notificationUrl.

    In the following example, the change notification contains two items for the same app, and for two different tenants, therefore the validationTokens array contains two tokens that need to be validated.

    {
        "value": [
            {
                "subscriptionId": "76619225-ff6b-4489-96ca-4ef547e78b22",
                "tenantId": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
                "changeType": "created",
                ...
            }
        ],
        "validationTokens": [
            "eyJ0eXAiOiJKV1QiLCJhb...",
            "cGlkYWNyIjoiMiIsImlkc..."
        ]
    }
    

    Note: for a full description of the data sent when change notifications are delivered, see changeNotificationCollection.

    How to validate

    Use MSAL to help you handle token validation, or a third-party library for a different platform.

    Be mindful of the following:

    • Make sure to always send an HTTP 202 Accepted status code as part of the response to the change notification.
    • Respond before validating the change notification (for example, if you store change notifications in queues for later processing) or after (if you process them on the fly), even if validation failed.
    • Accepting a change notification prevents unnecessary delivery retries and it also prevents any potential rogue actors from finding out if they passed or failed validation. You can always choose to ignore an invalid change notification after you have accepted it.

    In particular, perform validation on every JWT token in the validationTokens collection. If any tokens fail, consider the change notification suspicious and investigate further.

    Use the following steps to validate tokens and apps that generate tokens:

    1. Validate that the token hasn't expired.

    2. Validate the token hasn't been tampered with and was issued by the expected authority, Microsoft identity platform:

      • Obtain the signing keys from the common configuration endpoint: https://login.microsoftonline.com/common/.well-known/openid-configuration. This configuration is cached by your app for some time. Be aware that the configuration is updated frequently as signing keys are rotated daily.
      • Verify the signature of the JWT token using those keys.

      Don't accept tokens issued by any other authority.

    3. Validate that the token was issued for your app that is subscribing to change notifications.

      The following steps are part of standard validation logic in JWT token libraries and can typically be executed as a single function call.

      • Validate the "audience" in the token matches your app ID.
      • If you have more than one app receiving change notifications, make sure to check for multiple IDs.
    4. Critical: Validate that the app that generated the token represents the Microsoft Graph change notification publisher.

      • Check that the appid property in the token matches the expected value of 0bf30f3b-4a52-48df-9a82-234910c4a086.
      • This ensures that change notifications aren't sent by a different app that isn't Microsoft Graph.

    Example JWT token

    The following is an example of the properties included in the JWT token that are needed for validation.

    {
      // 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"
    }
    

    Example: Verifying validation tokens

    // 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;
        }
    }
    

    Decrypting resource data from change notifications

    The resourceData property of a change notification includes only the basic ID and type information of a resource instance. The encryptedData property contains the full resource data, encrypted by Microsoft Graph using the public key provided in the subscription. The property also contains values required for verification and decryption. This is done to increase the security of customer data accessed via change notifications. It is your responsibility to secure the private key to ensure that customer data can't be decrypted by a third party, even if they manage to intercept the original change notifications.

    In this section:

    Managing encryption keys

    1. Obtain a certificate with a pair of asymmetric keys.

      • You can self-sign the certificate, since Microsoft Graph doesn't verify the certificate issuer, and uses the public key for only encryption.

      • Use Azure Key Vault as the solution to create, rotate, and securely manage certificates. Make sure the keys satisfy the following criteria:

        • The key must be of type RSA
        • The key size must be between 2,048 bits and 4,096 bits
    2. Export the certificate in base64-encoded X.509 format, and include only the public key.

    3. When creating a subscription:

      • Provide the certificate in the encryptionCertificate property, using the base64-encoded content that the certificate was exported in.

      • Provide your own identifier in the encryptionCertificateId property.

        This identifier allows you to match your certificates to the change notifications you receive, and to retrieve certificates from your certificate store. The identifier can be up to 128 characters.

    4. Manage the private key securely, so that your change notification processing code can access the private key to decrypt resource data.

    Rotating keys

    To minimize the risk of a private key becoming compromised, periodically change your asymmetric keys. Follow these steps to introduce a new pair of keys:

    1. Obtain a new certificate with a new pair of asymmetric keys. Use it for all new subscriptions being created.

    2. Update existing subscriptions with the new certificate key.

      • Do this as part of regular subscription renewal.
      • Or, enumerate all subscriptions and provide the key. Use the PATCH operation on the subscription and update the encryptionCertificate and encryptionCertificateId properties.
    3. Keep in mind the following:

      • For some time, the old certificate may still be used for encryption. Your app must have access to both old and new certificates to be able to decrypt content.
      • Use the encryptionCertificateId property in each change notification to identify the correct key to use.
      • Discard of the old certificate only when you have seen no recent change notifications referencing it.

    Decrypting resource data

    To optimize performance, Microsoft Graph uses a two-step encryption process:

    • It generates a single use symmetric key, and uses it to encrypt resource data.
    • It uses the public asymmetric key (that you provided when subscribing) to encrypt the symmetric key and includes it in each change notification of that subscription.

    Always assume that the symmetric key is different for each item in the change notification.

    To decrypt resource data, your app should perform the reverse steps, using the properties under encryptedContent in each change notification:

    1. Use the encryptionCertificateId property to identify the certificate to use.

    2. Initialize an RSA cryptographic component (such as the .NET RSACryptoServiceProvider) with the private key.

    3. Decrypt the symmetric key delivered in the dataKey property of each item in the change notification.

      Use Optimal Asymmetric Encryption Padding (OAEP) for the decryption algorithm.

    4. Use the symmetric key to calculate the HMAC-SHA256 signature of the value in data.

      Compare it to the value in dataSignature. If they don't match, assume the payload has been tampered with and don't decrypt it.

    5. Use the symmetric key with an Advanced Encryption Standard (AES) (such as the .NET AesCryptoServiceProvider) to decrypt the content in data.

      • Use the following decryption parameters for the AES algorithm:

        • Padding: PKCS7
        • Cipher mode: CBC
      • Set the "initialization vector" by copying the first 16 bytes of the symmetric key used for decryption.

    6. The decrypted value is a JSON string that represents the resource instance in the change notification.

    Example: decrypting a notification with encrypted resource data

    The following is an example change notification that includes encrypted property values of a chatMessage instance in a channel message. The instance is specified by the @odata.id value.

    {
        "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..."
        ]
    }
    

    Note: for a full description of the data sent when change notifications are delivered, see changeNotificationCollection.

    This section contains some useful code snippets that use C# and .NET for each stage of decryption.

    Decrypt the symmetric key

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

    Compare data signature using 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.
    }
    

    Decrypt the resource data content

    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.