DEV Community

Christos Matskas for The 425 Show

Posted on

Graph Change Notification Web Hook with Azure Functions

I was recently asked on our Identity Discord how to implement a Microsoft Graph Notification hook with Azure Functions. I knew it was possible and I decided how hard it would be to put this end-to-end scenario together. The requirement is:

Users upload files on a designated OneDrive location, the Azure Function receives a notification about this via MS Graph and then the Azure Function does something with the file.

Create an Azure Function to catch the Graph Notifications

Before we can set up the notification in Graph, we first need to have an endpoint that can listen and respond to the Graph notifications. This endpoint needs to implement two things:

  1. The ability to respond to the validation request
  2. The ability to process the notification

Let's get coding! The easiest and most exciting way to create an event-driven solution is through Azure Functions. In this instance, I'll use Azure Functions v3 with .NET Core 3.1. Open your favorite terminal to create an Azure Function:

func init
func new -> HttpTrigger
Enter fullscreen mode Exit fullscreen mode

When prompted for a name, give your Function a meaningful one. I went with GraphNotificationHook. The code you need is attached below:

public static class GraphNotificationHook
{
    [FunctionName("GraphNotificationHook")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        // parse query parameter
        var validationToken =req.Query["validationToken"];
        log.LogInformation(validationToken);
        if(!string.IsNullOrEmpty(validationToken))
        {
            log.LogInformation("validationToken: " + validationToken);
            return new ContentResult{Content=validationToken, ContentType="text/plain"};
        }

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            var data = JsonConvert.DeserializeObject<GraphNotification>(requestBody);

        if(!data.value.FirstOrDefault().ClientState.Equals("<YourClientStateValue>",StringComparison.OrdinalIgnoreCase))
        {
            //client state is not valid (doesn't much the one submitted with the subscription)
            return new BadRequestResult();
        }
        //do something with the notification data

        return new OkResult();
    }
}
Enter fullscreen mode Exit fullscreen mode

We also need a small class to handle the deserialization. The expected notification payload is wrapped in a value so to make this work, add the following class somewhere in your code:

public class GraphNotification
{
    public List<ChangeNotification> value { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Save and build the Function to ensure that it's all good

dotnet build

Create the Graph Subscription

Since for this example we want to work with OneDrive, first we need to find the OneDrive ID. To do this, head out to the Graph Explorer (aka.ms/ge) and run the following query:

https://graph.microsoft.com/v1.0/me/drives

This should return the following payload (some content omitted for brevity)

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives",
    "value": [
        {
            "createdDateTime": "2020-03-01T06:10:34Z",
            "description": "",
            "id": "b!X6KRZ0000000000gVf3rsp8fG1qFJnyRF6ukM90-8i0ovGBNNSJ9LGV6NtuUR",
            "lastModifiedDateTime": "2021-03-22T22:25:25Z",
            "name": "OneDrive",
            "webUrl": "https://cmatskas-my.sharepoint.com/personal/<somename>_onmicrosoft_com/Documents",

>>>>> data omitted <<<<<<<<<<<<<<<
            "quota": {
                "deleted": 0,
                "remaining": 1099509630899,
                "state": "normal",
                "total": 1099511627776,
                "used": 22876
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

From here, we need to grab the ID so that we can create the subscription, i.e the ability to listen to this drive for changes.

Important: Your Azure Function will need to be running on a publicly accessible, https endpoint. Otherwise, your subscription will fail!

With my Azure Function running locally and proxied via NGrok, in Graph Explorer, I can run this to create the subscription:

Do a POST to this endpoint https://graph.microsoft.com/v1.0/subscriptions with the following payload

{
    "changeType": "updated",
    "notificationUrl": "https://9aaa11e2fe51.ngrok.io/api/GraphNotificationHook ",
    "resource": "/me/drives/b!X6KRZ3Gkzk00000000rsp8fG1qFJnyRF6ukM90-8i0ovGBNNSJ9LGV6NtuUR/root",
    "expirationDateTime": "2021-03-24T11:59:00.0000000Z",
    "clientState": "<some Secret Value>"
}
Enter fullscreen mode Exit fullscreen mode

Executing this should cause our Azure Function to fire. Since this is the first time running this, the Graph subscription will attempt to validate the webhook endpoint. Our Function is coded for this and should generate the following output:

Alt Text

And in our Graph Explorer, we should receive the following response:

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity",
    "id": "b7e95f87-01f1-443b-85e8-ef0c1430dd87",
    "resource": "/me/drives/b!X6KRZ3GkzkaMXrgy4zgVf3rsp8fG1qFJnyRF6ukM90-8i0ovGBNNSJ9LGV6NtuUR/root",
    "applicationId": "de8bc8b5-d9f9-48b1-a8ad-b748da725064",
    "changeType": "updated",
    "clientState": "SecretClientState",
    "notificationUrl": "https://9aaa11e2fe51.ngrok.io/api/GraphNotificationHook ",
    "notificationQueryOptions": null,
    "lifecycleNotificationUrl": null,
    "expirationDateTime": "2021-03-24T11:59:00Z",
    "creatorId": "449e6e6a-fd63-4705-8291-bc32ddb47b1d",
    "includeResourceData": null,
    "latestSupportedTlsVersion": "v1_2",
    "encryptionCertificate": null,
    "encryptionCertificateId": null
}
Enter fullscreen mode Exit fullscreen mode

This is all we need to create and process the Change Notification using Azure Functions. The next time that there is a change in OneDrive, our Azure Function should receive the notification and process it accordingly. The execution logs should look like this:

[2021-03-23T03:33:21.417Z] Executed 'GraphNotificationHook' (Succeeded, Id=e9bdb30b-2971-48bb-8b2f-13c3198f06d2, Duration=304ms)
[2021-03-23T03:34:12.211Z] Executing 'GraphNotificationHook' (Reason='This function was programmatically called via the host APIs.', Id=45426de3-e9ec-4906-a0c0-5965f0adc0a9)
[2021-03-23T03:34:12.215Z] C# HTTP trigger function processed a request.
[2021-03-23T03:34:12.218Z] [null]
[2021-03-23T03:34:12.327Z] Executed 'GraphNotificationHook' (Succeeded, Id=45426de3-e9ec-4906-a0c0-5965f0adc0a9, Duration=122ms)
Enter fullscreen mode Exit fullscreen mode

Working with the changed resources via Graph

Depending on the type of the resource we subscribe to, there is an option to receive rich notifications that include the data of the resource.

However, in the case of OneDrive, for security reasons, we only receive a notification that there is a change. No information on what this change is or what data has been affected. To find what triggered the change notification, we need to use the Delta API.

It is advisable that the Delta query is run on our resource before setting up the subscription to the change feed. Running the delta query against our OneDrive resource should look like this

https://graph.microsoft.com/v1.0/me/drives/<OneDriveID>/root/delta?token=latest

?token=latest is a clever hack to ensure that we don't get a torrent of information in the query by skipping all the pages

The output should look like this

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)",
    "@odata.deltaLink": "https://graph.microsoft.com/v1.0/me/drives/b!X6KR----9LGV6NtuUR/root/delta?token=MzslMjM0OyUyM------DM3MzI4MzslMjM7JTIzOyUyMzA",
    "value": [
        {
            "@odata.type": "#microsoft.graph.driveItem",
            "createdDateTime": "2020-03-01T06:10:34Z",
            "id": "01G4UX------------ZO354PWSELRRZ",
            "lastModifiedDateTime": "2021-03-23T03:33:38Z",
>>>>>>>>>>>>>>>>>  content omitted   <<<<<<<<<<<<<<<<<<<

        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

The most important information here is the @odata.deltalink value, which also contains the token needed to authenticate against the Graph Delta API. This first call establishes a snapshot of the state of our OneDrive (or any other resource).

During subsequent calls to our Azure Function, we can use this Delta link to retrieve the changes in our OneDrive.

Important things to note:

  1. Subscriptions to resources are short lived and need to be renewed frequently. Each change notification includes the Expiration date/time of the subscription so you can use this information to renew the subscription at a time that you deem necessary
  2. The Delta API token expires if it's not used regularly. You'll either need to have a Function running on a timer or ensure that your code is called regularly so that it doesn't expire.

I hope this blog is useful in showing you how to configure and set your Change Notification Feed for OneDrive. You can find more information in our docs

Top comments (3)

Collapse
 
wolarte profile image
Wilson Ricardo Olarte Martinez

this code work for me.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Graph;
using System.Collections.Generic;
using System.Linq;

namespace GraphNotificationHook
{
public class GraphNotification
{
public List value { get; set; }
}

public static class Function1
{
    [FunctionName("GraphNotificationHook")]
    public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");

        // parse query parameter
        var validationToken = req.Query["validationToken"];
        log.LogInformation(validationToken);
        if (!string.IsNullOrEmpty(validationToken))
        {
            log.LogInformation("validationToken: " + validationToken);
            return new ContentResult { Content = validationToken, ContentType = "text/plain" };
        }

        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        var data = JsonConvert.DeserializeObject<GraphNotification>(requestBody);

        if (!data.value.FirstOrDefault().ClientState.Equals("<YourClientStateValue>", StringComparison.OrdinalIgnoreCase))
        {
            //client state is not valid (doesn't much the one submitted with the subscription)
            return new BadRequestResult();
        }
        //do something with the notification data

        return new OkResult();
    }

}
Enter fullscreen mode Exit fullscreen mode

}

Collapse
 
wolarte profile image
Wilson Ricardo Olarte Martinez

Excelente work. Hey i have a question in your code in function app how response 200 ok in the validation with microsoft?
i try exetute step by step your code but MS respons: "message": "Subscription validation request failed. Notification endpoint must respond with 200 OK to validation request."
i don't understand where your code responde with 200 ok.

Collapse
 
devarshsanghvi profile image
devarsh-sanghvi

Thanks man, I was searching all day how to get actual change data and with your example I explored delta query and it worked for me.