Graph Change Notification Web Hook with Azure Functions
Tags: serverless, azure, security
Published: 2021-03-23 03:53:39

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
```  
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();
    }
}
```
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; }
}
```
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
            }
        }
    ]
}
```
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>"
}
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6kkvgt8cisnxl292wa14.png)

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
}
```
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)
```

## 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   <<<<<<<<<<<<<<<<<<<

        }
    ]
}
```
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
- [MS Graph Change Notifications](https://docs.microsoft.com/en-us/graph/webhooks)
- [DriveItem Resource](https://docs.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0)