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