This is the final post in a series detailing using PowerShell to leverage the Azure AD Graph API. For those catching up it started here introducing using PowerShell to access the Azure AD via the Graph API, licensing users in Azure AD via Powershell and the Graph API, and returning all objects using paging via Powershell and the Graph API.

In this post I show how to;

  • enumerate objects from Azure AD via Powershell and the Graph API, and set a delta change cookie
  • enumerate changes in Azure AD since the last query
  • return objects that have changed since the last query
  • return just the changed attributes on objects that have changed since the last query
  • get a differential sync from now delta change link

Searching through MSDN and other resources working this out I somehow stumbled upon a reference to changes in the API that detail the search filters. v1.5 and later of the API requires filters using the context ‘Microsoft.DirectoryServices.User|Group|Contact’ etc instead of ‘Microsoft.WindowsAzure.ActiveDirectory.User|Group|Contact’ which you’ll find in the few examples around. If you don’t want to return all these object types update the filter on line 21 in the script below.

Here is the script to return all Users, Groups and Contacts from the tenant along with all the other options I detail in this post. Update the following for your tenant;

  • line 6 for your tenant URI
  • line 10 for your account in the tenant
  • line 11 for the password associated with your account from line 10
# Adding the AD AuthN library to your PowerShell Session.
# the default path to where the ADAL GraphAPI PS Module puts the Libs
Add-Type -Path 'C:\Program Files\WindowsPowerShell\Modules\AzureADPreview\1.1.143.0\Microsoft.IdentityModel.Clients.ActiveDirectory.dll'
# Your Azure tenant name
$tenantID = "mytenant.com.au"
$authString = "https://login.microsoftonline.com/$tenantID"
# username and password. The username must be MFA disabled user Admin at least, and must not be a live id.
$username = "doc@mytenant.com.au"
$password = "Sup3rS3cr3t1"
# The resource URI for your token.
$resource = "https://graph.windows.net/"
# Object Type (eg. Users, Groups, Contacts, DirectoryObjects)
$object = "directoryObjects"
# What Objects are we interested in. I'm expliciting calling User, Group and Contact even though they are meant to be implied (default)
# as I've read about mixed results with differential sync across different object types
$Searchfilter ="`$filter=isof('Microsoft.DirectoryServices.User') or isof('Microsoft.DirectoryServices.Group') or isof('Microsoft.DirectoryServices.Contact')"
# Output Directory and file for Differential Cookie
$downloadDirectory = "C:\Users\Darren\Dropbox\Kloud\Powershell\O365\DeltaSync"
$cookieFile = "\AADDeltaCookie.txt"
$filepath = $downloadDirectory +$cookieFile
# Reset results var
$query = $null
# Read in Delta Cookie if it exists, if not create the file for storing the cookie
if(!(Test-Path $filepath))
{
$cookie = New-Item -Path $filepath -ItemType File
}
else
{
$cookie = Get-Item -Path $filepath
}
# This is the powershell common client id.
$client_id = "1950a258-227b-4e31-a9cf-717495945fc2"
# Create a client credential with the above common client id, username and password.
$creds = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserCredential" `
-ArgumentList $username,$password
# Create a authentication context with the above authentication string.
$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" `
-ArgumentList $authString
# Acquire access token from server.
$authenticationResult = $authContext.AcquireToken($resource,$client_id,$creds)
# Use the access token to setup headers for your http request.
$authHeader = $authenticationResult.AccessTokenType + " " + $authenticationResult.AccessToken
$headers = @{"Authorization"=$authHeader; "Content-Type"="application/json"}
# URI to get first set of objects
if((Get-Item $cookie).length -gt 0kb){
# Delta cookie value exists. Get it
$url = Get-Content $cookie.FullName
# omit &ocp-aad-dq-include-only-changed-properties=true from the URI if you want the full object
$url += '&ocp-aad-dq-include-only-changed-properties=true&api-version=1.6' -f $authenticationResult.TenantId
}
else
{
# no Delta Cookie, so first run, so return everything
$url = "https://graph.windows.net/{0}/$($object)?&$($Searchfilter)&api-version=1.6&deltaLink="
}
# Get first set of results
$query = Invoke-RestMethod -Method Get -Headers @{
Authorization = $authenticationResult.CreateAuthorizationHeader()
'Content-Type' = "application/json"
# unremark if you just want the DeltaLink from now
# 'ocp-aad-dq-include-only-delta-token' = "true"
} -Uri ($url -f $authenticationResult.TenantId)
$query.value.Count
# An Array for the retuned objects to go into
$tenantObjects = @()
# Add in our first objects
$tenantObjects += $query.value
$moreObjects = $query
# Get all the remaining objects in batches if we didn't return them all already
if ($query.'aad.nextLink'){
$moreObjects.'aad.nextLink' = $query.'aad.nextLink'
do
{
$moreObjects = Invoke-RestMethod -Method Get -Headers @{
Authorization = $authenticationResult.CreateAuthorizationHeader()
'Content-Type' = "application/json"
} -Uri ($moreObjects.'aad.nextLink'+'&api-version=1.6' -f $authenticationResult.TenantId)
$moreObjects.value.count
$tenantObjects += $moreObjects.value
$tenantObjects.Count
} while ($moreObjects.'aad.nextLink')
}
$moreObjects.value | out-gridview
# store the DeltaLink in a file for next time we run the script
$moreObjects.'aad.deltaLink' | Out-File $cookie

Here is a sample output showing the Users, Groups, Contacts and DirectoryLinkChange objects. Note: if you have a large tenant that has been in place for a period of time it may take a while to enumerate.  In this instance you can use the Differential Sync from now option. More on that later.

https://dl.dropboxusercontent.com/u/76015/BlogImages/DifferentialQuery/FirstDQRun.png

Running the query again using the Differential DeltaLink from the first run now returns no results. This is as expected as no changes have been made in the tenant on the objects in our query.

https://dl.dropboxusercontent.com/u/76015/BlogImages/DifferentialQuery/SecondDQRun.png

Now if I make a change in the tenant and run the query again using the Differential DeltaLink I get 1 result. And I get the full object.

https://dl.dropboxusercontent.com/u/76015/BlogImages/DifferentialQuery/ThirdDQRun.png

What if I just wanted to know the change that was made?

If we add ‘&ocp-aad-dq-include-only-changed-properties=true’ to the URI that’s exactly what we get. The object and what changed. In my case the Department attribute.

https://dl.dropboxusercontent.com/u/76015/BlogImages/DifferentialQuery/ForthDQRun.png

Finally as alluded to earlier there is the Differential Sync from now option. Very useful on large tenants where you can query and get all users, contacts, groups etc without using differential sync, then get the Differential Delta token for future sync queries. So I’ve used the same URI that I used as the beginning of this blog post but in the header specified ‘ocp-aad-dq-include-only-delta-token’ = “true” and as you can see I returned no results but I got the important Differential Query DeltaLink.

https://dl.dropboxusercontent.com/u/76015/BlogImages/DifferentialQuery/FifthDQRun.png

Summary

Using Powershell we can leverage the Azure AD Graph RestAPI and use the Differential Sync functions to efficiently query Azure AD for changes rather than needing to enumerate an entire tenant each time. Brilliant.

Follow Darren on Twitter @darrenjrobinson

Category:
Azure Platform, PowerShell
Tags:

Join the conversation! 1 Comment

  1. Hello Darren,

    I tried this script but I am receiving below error message.

    Exception calling “AcquireToken” with “3” argument(s): “AADSTS7000218: The request body must contain the following parameter: ‘client_assertion’ or ‘client_secret’.
    Trace ID: 661f5dd1-d985-48ce-8128-dc3918f34500
    Correlation ID: bee9612a-1532-4daf-8307-34cab68a6201
    Timestamp: 2019-08-12 06:58:43Z”
    Please guide me if I am missing anything.

    Regards,
    Sachin

Comments are closed.