JonasOhmsen
14 TopicsHow-To Use Graph Object Change Notifications
Hi, Jonas and Roland here! Or as we say in the north of Germany: "Moin Moin!" We recently worked on a mechanism to sync Entra ID group memberships into an Azure SQL table and would like to share our learnings. This article is part of a series of articles about Entra ID group membership synchronization. Check them out here: https://aka.ms/JonasOhmsenBlogs Introduction In this article we will dive deeper into “Option #4: Delta Notification” as described in our previous article: How-To Sync EntraID Group Memberships Into Any System TL/DR In short, we’ve built an Azure Function app to synchronize Entra ID group membership changes into an Azure SQL database using Graph object change notification functionality. The solution can be found here: LINK A quick recap: Microsoft Graph offers the ability to subscribe to change notifications. In our case, we need to track and act on membership changes made to Entra ID groups. A change notification subscription will result in two actions. The change notification in the form of a web request Will be send every time a change is made to an Entra ID object and will contain a list of those changes. Service notifications also in the form of a web request Will contain a list of information about the subscription or service states. A notification about a soon to expire subscription for example. Those can happen at any time. For this to work we need two endpoints to be able to receive those two types of notifications. A typical communication sequence between Microsoft Graph and our custom endpoints would look like this: Overview We will use an Azure Functions App with two endpoints. One for the change notifications and one for the service notifications. We already have a SQL database we use for an application, and we need to update Entra ID group memberships in a table called “GroupMembers”. That way our application always has the correct membership information available. (Please read our previous article about the application setup and why we required group memberships to be stored outside of Entra ID: How-To Sync EntraID Group Memberships Into Any System) For the function app to write to Azure SQL, we enabled a system managed identity and granted access to SQL. That managed identity does also need the “Groups.Read.All” permissions for Microsoft Graph to be able to subscribe to group changes. We will use two stored procedures in SQL to help keep the data consistent. Those stored procedures will give us the flexibility to change any SQL schema or logic without needing to change our synchronization process. NOTE: To keep the scenario simple, we will start with empty Entra ID groups. Since existing memberships will not result in a change notification and we would need a mechanism for an initial membership import. NOTE: Our function app will be written in PowerShell, but you can use any other available language to achieve the same. Initial function setup We will use some variables configured centrally for all our functions within our Functions app. That way we can keep the code clean and we are able to later adjust the settings in the Azure portal. Name: varClientState Value: Will contain a random password like string. We will use that to verify that each change notification come from a subscription we created. Name: varAzureSQLInstance Value: Will contain the azure SQL instance name where we store our database with table “GroupMembers”. Example: sampledbserver10235x.database.windows.net Name: varAzureSQLDatabase Value: The name of the database with table “GroupMembers”. Name: varMaximumSubscriptionExpirationDays Value: A subscription has a lifespan of 1 to 29 days. We will use the variable to set an initial value when we create new subscription and we will use the variable if we need to renew a subscription. Name: varNotificationUrl Value: Will contain the URL of our main function to act on member changes. We’ll come to that later. Name: varLifecycleNotificationUrl Value: Will contain the URL of our subscription lifecycle function. We’ll come to that later. NOTE: We will not use SQL bindings in our function app to be able to call SQL multiple times. That’s the reason for the SQL related variables instead of a SQL connections string. Read more about function bindings HERE. SQL table “GroupMembers” Let’s start with the SQL table we will use to store all the members of all the groups we are interested in. The table will contain the following information SQL stored procedures We will use the following stored procedures to delete or add members of groups to or from our table. The stored procedures help keep the data consistent and make changes to the database schema possible without the need to change the Function App. sproc: NewGroupMember The stored procedure has four parameters. @group_id: The id of the group @member_id: In case of a user the Entra ID object ID. In case of a device the device ID @object_id: The Entra ID object ID @object_type: Can be 'user' or 'device'. NOTE: We only work with users or devices and not with groups in groups. NOTE: To keep things simple, the stored procedure will not return any error in case the object is already there. sproc: RemoveGroupMember The stored procedure has only two parameters. One for the group ID and one for the Entra ID object ID. NOTE: It will not return an error in case the member does not exist anymore. Function App managed identity We will activate the system assigned managed identity for our function app. The identity gives us the ability to grant access to SQL and Microsoft Graph easily. Function App managed identity SQL permissions The following statements will add the Function App managed identity to our SQL database and grant just enough permissions to execute the required stored procedures: Function App managed identity Graph permissions The Function app managed identity also needs the Groups.Read.All, Users.Read.All and Devices.Read.All Microsoft Graph permission. Groups.Read.All is required to be able to create group object change subscriptions. That is the most important one. (Users.Read.All and Devices.Read.All will be explained in the “NotifyGroupChange function” section below) We will use the following PowerShell script to add those application permissions to the managed identity. We use a script because we cannot set those permissions via the Azure Portal. Function App in more detail As mentioned before, we need at least two endpoints aka functions in our Azure Function App. One for the change notifications and one for the service notifications. Both functions need to be able to reply to the initial subscription setup call. In that call Microsoft Graph will send a “ValidationToken” and expects that token to be in the reply. If the reply does not contain the validation token or if there is no reply at all, the subscription will not be activated. If one of the functions detects a validation token, we will simply reply with it in text/plain form and with http status code 200 OK. Those are our first lines of code in both functions. “NotifyGroupChange” function The NotifyGroupChange or change notification function will be the main function keeping table “GroupMembers” consistent. We will use the following code to get an access token for SQL server to be able to write to the database. And Connect-MgGraph to get an access token for Microsoft Graph to be able to manage subscriptions and get information about Entra ID objects. Both methods will use the Function app managed identity. NOTE: MSI_ENDPOINT and MSI_SECRET are built-in variables we can use to authenticate and to request an access token. NOTE: The first step is only required because we will use the SQL cmdlet “Invoke-Sqlcmd” instead of a SQL binding. Read more about function bindings HERE. That way we have more control and less limitations over the SQL commands. The following is an example of a group membership change notification sent by Microsoft Graph. The yellow arrow shows us the client secret we used to create the subscription. The purple arrow the group we subscribed to. The green arrow an added member and the red arrow a removed member. Before doing anything with that information we need to check if that message was sent from a subscription we created. We will do that by checking the “ClientState” value. If it doesn’t match, we will stop all actions and return with bad request (error 400) If the “ClientState” value matches, we can proceed. There is one important thing to note here. Every object in Entra ID has an object ID. Like the user shown in the screenshot below: But our application works with user and devices and devices also have a device ID as shown in the screenshot below: The change notification will only contain the object ID and never the device ID or any other IDs. However our application needs the device ID. One way of getting the device ID is to use a Microsoft Graph function to get an object by its object ID. We simply take all those object IDs from the change notification and send them to: https://graph.microsoft.com/v1.0/directoryObjects/microsoft.graph.getByIds as shown in the below example. That method also gives us the object type. So, we know if the member change happened for a device or a user. Or even a group within a group if we want to. NOTE: The “microsoft.graph.getByIds” functions requires at least Users.Read.All and Devices.Read.All permissions. We can then use the returned info to enrich the exiting object ID with a device ID and the type of that object. The below screenshot shows a code example to use Invoke-Sqlcmd to either add or remove members from the SQL table “GroupMembers”. (Simplified to fit in this blog) At the end of the script we will return with http status code 200 OK. If we encounter a problem, we return with http status code 400 bad request. Bad request will also mean that Microsoft Graph will re-send the change notification. “SubscriptionLifecycle” function As mentioned before, we also need an endpoint to receive service notifications from Microsoft Graph. That endpoint is called by the subscription for lifecycle events, which are different from the actual change notification. This function also needs to respond with the validation token and therefore contains the same “ValidationToken” code mentioned earlier. At the moment the function will either use the “PATCH” method to set a new expiration date to extend the lifetime of a subscription or write the event to the log. The code might look like this. We first need the new expiration date in a specific format. Then construct a small JSON file containing the new date and at the end send the JSON to the correct endpoint for that subscription. “NewSubscription” function While the other functions are triggered by Microsoft Graph, this one is meant for the admin to create a new subscription. Therefore we don’t need the ValidationToken code for this function. All we have to do is to POST a JSON file to the subscription endpoint. We will construct the file based on the Function App variables for the “notificationUrl”, “lifecycleNotificationUrl”, “expirationDateTime” and “clientState”. NOTE: Now is a good time to set those URL variables for the required functions. The following web request will then create a new subscription without the need for any other data. Because all other values are coming from the Function App variables mentioned earlier. https://<FunctionAppName>.azurewebsites.net/api/NewSubscription?GroupID=0686a5ee-3e46-49e5-82e8-8afe07ca4fa8 “ListSubscriptions” function The ListSubscriptions function is also meant for admins and does also not require the ValidationToken code. IMPORTANT: Subscriptions are only visible to the creator of the subscription. So, this function is vital for us, because it will show us all active subscriptions created by the managed identity of the Function App. The code will simply use the GET method for endpoint: GET https://graph.microsoft.com/v1.0/subscriptions and runs without any additional parameters. “DeleteSubscription” function The DeleteSubscription function is also meant for admins and does also not require the ValidationToken code. It has just one parameter called “SubscriptionID”. It will use the DELETE method to delete a subscription by ID. DELETE https://graph.microsoft.com/v1.0/subscriptions/<SubscriptionID> Putting it all together The following is an example of the features we get from those different functions. We are now able to create a “NewSubscription”. Then add or remove any objects from the subscribed group. Microsoft Graph will then call the “NotifyGroupChange” function. The “NotifyGroupChange” function will keep the table “GroupMembers” in sync. Microsoft Graph is able to notify us about service or subscription states. We can list all active subscriptions or delete them. This is it for now. But there is more and we will keep posting articles about that topic until we run out of content 😉 We hope you enjoyed our article. Let us know in the comments. Stay safe! Roland Spindeler and Jonas Ohmsen Code disclaimer This sample script is not supported under any Microsoft standard support program or service. This sample script is provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of this sample script and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of this script be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use this sample script or documentation, even if Microsoft has been advised of the possibility of such damages.