Subscribe to Azure Active Directory changes

Use case: in our case GitHub has Teams plan, not enterprise one, which means we need manage users on our own, especially when someone leaves, so far we are requiring users to have corporate public email and checkinf if this account is enabled or not, and if not kick it off from github, but how can we react to Active Directory changes instead of having scheduled job

There is an concept of subscriptions in msgraph

https://learn.microsoft.com/en-us/graph/api/subscription-post-subscriptions?view=graph-rest-1.0&tabs=http

Prerequisites: we need an access token

token=$(az account get-access-token --query=accessToken --output=tsv --resource-type ms-graph)

Creating an subscription

curl -s -X POST "https://graph.microsoft.com/v1.0/subscriptions" -H "Authorization: Bearer $token" -H "Content-Type: application/json" -d '{
    "changeType": "created,updated,deleted",
    "notificationUrl": "https://40c8-178-150-44-191.ngrok-free.app",
    "resource": "/users",
    "expirationDateTime":"2023-08-20T17:37:51.0000000Z",
    "clientState": "optional, mac was here"
}' | jq
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#subscriptions/$entity",
  "id": "6b8d8da9-a1d7-4f1b-a333-6563521acc00",
  "resource": "/users",
  "applicationId": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
  "changeType": "created,updated,deleted",
  "clientState": null,
  "notificationUrl": "https://40c8-178-150-44-191.ngrok-free.app",
  "notificationQueryOptions": null,
  "lifecycleNotificationUrl": null,
  "expirationDateTime": "2023-08-20T17:37:51Z",
  "creatorId": "57434352-42b0-4090-9d36-9561ae89c607",
  "includeResourceData": null,
  "latestSupportedTlsVersion": "v1_2",
  "encryptionCertificate": null,
  "encryptionCertificateId": null,
  "notificationUrlAppId": null,
}

Notes:

  • clientState is an optional string, the good thing is that it will be passed everywhere, so can be used to identify subscription
  • expirationDateTime is not only required but has limitations, so do not expect it to be valid for years
  • to create subscription your listerener should be able to answer to initial request (see below)
  • notifications are arriving in ~1min, not immediatelly

List subscirptions:

curl -s "https://graph.microsoft.com/v1.0/subscriptions" -H "Authorization: Bearer $token" | jq ".value"

Delete subscription:

curl -s -X DELETE "https://graph.microsoft.com/v1.0/subscriptions/6b8d8da9-a1d7-4f1b-a333-6563521acc00" -H "Authorization: Bearer $token"

Here is an simples possible listener:

const express = require('express')

const app = express()

app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.all('/', (req, res) => {
  if (req.query.validationToken) { // required for subscription creation
    res.send(req.query.validationToken)
  } else {
    console.dir(req.body, {depth: null, colors: true})
    res.send('OK')
  }
})

app.listen(3000)

From what I have checked notifications are received with some delay, not immediatelly, also the payload is always the same with only change type changing between created, updated and deleted

Also you should note that deleting user from portal - will trigger update notification, and if you go to deleted users in portal and permanently delete it from there - only then you will receive deleted notification

Here is an example of payload

{
    "value": [
        {
            "changeType": "updated",
            "clientState": "optional, mac was here",
            "resource": "Users/e6421e53...",
            "resourceData": {
                "@odata.type": "#Microsoft.Graph.User",
                "@odata.id": "Users/e6421e53...",
                "id": "e6421e53...",
                "organizationId": "695e64b5..."
            },
            "subscriptionExpirationDateTime": "2023-08-20T10:37:51-07:00",
            "subscriptionId": "7b573f64-f3d7-4daf-9dd3-f35aa06d186e",
            "tenantId": "695e64b5..."
        }
    ]
}

Whenever we receive an notification we may want to receive user details, we can do it like so

curl -s -X GET "https://graph.microsoft.com/v1.0/users/e6421e53..." -H "Authorization: Bearer $token"
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
  "businessPhones": [],
  "displayName": "mactemp",
  "givenName": "mac",
  "jobTitle": null,
  "mail": null,
  "mobilePhone": null,
  "officeLocation": null,
  "preferredLanguage": null,
  "surname": "temp",
  "userPrincipalName": "[email protected]",
  "id": "e6421e53..."
}

And to see what other fields we have you may want to look at @odata.context

Here is and example:

curl -s -X GET "https://graph.microsoft.com/v1.0/users/57434352?\$select=displayName,mail,lastPasswordChangeDateTime,accountEnabled,signInActivity,usageLocation" -H "Authorization: Bearer $token" | jq
{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users(displayName,mail,lastPasswordChangeDateTime,accountEnabled,signInActivity,usageLocation)/$entity",
  "displayName": "Alexandr Marchenko",
  "mail": "[email protected]",
  "lastPasswordChangeDateTime": "2023-05-25T13:05:25Z",
  "accountEnabled": true,
  "usageLocation": "UA",
  "id": "57434352",
  "signInActivity": {
    "lastSignInDateTime": "2023-08-19T16:09:19Z",
    "lastNonInteractiveSignInDateTime": "2023-08-20T02:33:04Z",
  }
}

As you can guess this data may be used for many other things, also do not forget that you can store additional data for user, that also may be used