Use Microsoft Graph to Check Synchronised On-Premises AD extensionAttributes 1-15

Hello, again!

It's been some time,

It's been 271 days,

This feels GOOD.

This feels like...

A reacquaintance,

Hello, old friend!

 

Anyway, enough of that! Earlier this year we added the onPremisesExtensionAttributes complex type to the user entity, under directory APIs, in Microsoft Graph. This contains the synchronised on-premises AD / Exchange extension attributes 1-15 that many of us have known and loved for many, many years... *sigh*

I was asked a question: "How can we use Microsoft Graph to check for users that have extensionAttributes sync'd up to Azure AD?"

When clarifying the request, the customer wasn't able to tell me which attributes were likely to be sync'd... I had to check them all.

 

Here's what I did:

  1. Fetched every user from MS Graph and selected the ‘UserPrincipalName’ and ‘onPremisesExtensionAttributes’ properties.
  2. For each user, looped through the properties of the ‘onPremisesExtensionAttributes’ object and checked each ‘extensionAttribute’, i.e. 1 to 15.
  3. When an ‘extensionAttribute’ was found to be populated, wrote the output to screen, showing the ‘UserPrincipalName’ with the ‘extensionAttribute’ and its value.

 

Here's some sample output:

 

The script will also dump out where a user has more than one 'extensionAttribute' set:

 

You'll need to set up a web app / API in your Azure AD tenant to query Microsoft Graph on your behalf. Below is a good article that contains a section on setting up a web app / API for a similar purpose. Start at the section 'Click on the Azure Active Directory' and give the application your own name and the necessary permissions to read user data.

Leverage the Microsoft Graph with Azure Active Directory Identity Protection to Identify Network Threats

 

Now for the 'Shell:

 
function Get-AzureADGraphAppToken {

    [CmdletBinding()]

    Param (
        #Target domain, e.g. contoso.com
        [Parameter(Mandatory=$false,
                   Position=0)]
        [string]$TenantDomain,

        #Client ID for app used to access graph
        [Parameter(Mandatory=$false,
                   Position=1)]
        [string]$ClientId,

        #Client secret used by app to access graph
        [Parameter(Mandatory=$false,
                   Position=2)]
        [string]$ClientSecret,

        #Switch for Azure AD graph token
        [switch]$AzureADGraph

    )
    

    #Nullify existing header
    $Header = $null
    
    if ($AzureADGraph) {

        #Azure AD Graph URL
        $Resource = "https://graph.windows.net/"

    }
    else {

        #MS Graph URL
        $Resource = "https://graph.microsoft.com"

    }

    
    #Login URL
    $LoginUrl = "https://login.microsoft.com"
    
    #Get a token
    $Body = @{grant_type="client_credentials";resource=$Resource;client_id=$ClientID;client_secret=$ClientSecret}
    $Oauth = Invoke-RestMethod -Method Post -Uri $LoginURL/$TenantDomain/oauth2/token?api-version=1.0 -Body $Body

    return $OAuth


}   #end of function



#Get a token
$OAuth = Get-AzureADGraphAppToken -TenantDomain "add_your_tenant_domain_here" `
                                  -ClientId "add_your_web_app_client_id_here" `
                                  -ClientSecret "add_your_client_secret_here_but_dont_leave_it_permanently_saved_here_at_rest"


    
#Header variable for REST call
$Header = @{'Authorization'="$($OAuth.token_type) $($OAuth.access_token)"}


#MS Graph Query
$Query = "https://graph.microsoft.com/beta/users?`$select=UserPrincipalName,onPremisesExtensionAttributes"


#Use do / while to repeat the query to fetch next 100 records
do {
    #Call REST method
    $Result = (Invoke-Restmethod -UseBasicParsing -Headers $Header -Uri $Query -Method Get)

    #For every user returned check each extension attribute for a value
    $Result.Value | ForEach-Object {

        #Variables for the current user and their attribute set
        $User = $_
        $Extensions = $User.onPremisesExtensionAttributes

        #Loop through each extension attribute and check for a value
        1..15 | ForEach-Object  {
    
            if ($Extensions."extensionAttribute$_") {
    
                #If an attribute is populated return it
                Write-Output "$($User.UserPrincipalName),extensionAttribute$_,$($Extensions."extensionAttribute$_")"

            }  

        }   

    }   
    
    $Query = $Result.'@odata.nextLink'
}
while($Query -ne $null)