
Dealing with hybrid identity can be scary and complex but in this post I will assist you in understanding how objects and their attributes are synchronized between on-premises Active Directory and Azure AD.
So lets look at provisioning an object from on-premises Active Directory to Azure AD, Azure AD performs additional actions one time, when compared to updating a user object. These two actions are named:
- Hard Matching
- Soft Matching
The actions are performed in the above sequence; Hard matching is attempted, before soft matching is attempted. If there’s no match, a new user object is created in Azure AD to correspond to the user object in the on-premises Active Directory environment.
Hard Matching
To definitively match an on-premises Active Directory user object to an Azure AD user object, Azure AD Connect looks at the sourceanchor attribute.
During normal synchronization cycles, this attribute is already used to provide the end-to-end connection between the on-premises Active Directory user object and the Azure AD user object through Azure AD Connect’s connector spaces and metaverse, so it’s an ideal way to match.
The Azure Active Directory Connect wizard, used to configure Azure AD Connect installations provides options to choose the sourceanchor attribute:
The above screenshot is a screenshot of a recent versions of Azure AD Connect. It provides the default option to Let Azure AD manage the source anchor and a list of available attributes to use as an alternative through the Choose a specific attribute option.
Let Azure AD manage the source anchor
When the Let Azure AD manage the source anchor option is selected, Azure AD Connect checks if there is a previous (older) Azure AD Connect installation connected to the Azure AD tenant. If there is, it will default to the objectGUID attribute . If this is the first Azure AD Connect installations, or all other Azure AD Connect installations have already been migrated to use mS-DS-ConsistencyGUID as the sourceanchor attribute for user objects, the mS-DS-ConsistencyGUID attribute is automatically selected as the sourceanchor attribute.
Choose a specific attribute
When you Choose a specific attribute it is important to choose an attribute that:
- Does not exceed 60 characters in length
- Does not contain special characters like \ ! # $ % & * + / = ? and ^
- Is globally unique throughout your organization
- Is either a string, integer or binary
A good sourceanchor attribute is not based on a person’s name, because this may change, however some organizations still choose to do so, based on the mail attribute…
Note:
The sourceanchor attribute chosen is stored in the configuration of the Azure AD tenant.
Note:
When upgrading or changing settings, Azure AD Connect reports that the Azure AD Connect installation for this tenant still uses objectGUID to synchronize user objects.
Hard matching occurs, based on the following data:
Here is a script I created for generating the ImmutableID from on-premises AD and Azure AD:
<#Information
Author: thewatchernode
Contact: author@blogabout.cloud
Published: 4th October 2020
.DESCRIPTION
Tool to assist with application delivery
Version Changes
: 0.1 Initial Script Build
: 1.0 Initial Build Release
: 1.1 Additional fields in AD Output
.EXAMPLE
.\set-immutableid.ps1
Description
-----------
Runs script with default values. It
.INPUTS
None. You cannot pipe objects to this script.
#>
#region Shortname
$DarkCyan = 'DarkCyan'
$DarkRed = 'DarkRed'
$Green = 'Green'
$Red = 'Red'
$Yellow = 'Yellow'
$White = 'White'
$AzureAD = 'AzureAD'
$ImportExcel = 'ImportExcel'
$Quit = 'Q'
#endregion
#region Banner
[string] $Root = @'
┌─────────────────────────────────────────────────────────────┐
Gather ImmutableID in Bulk using PowerShell
Follow me @thewatchernode on Twitter
This script gathers the ImmutableID from Active Directory
and Azure Active Directory.
└─────────────────────────────────────────────────────────────┘
1) Connect to Azure AD -->
2) Get ImmutableID for AD -->
3) Get ImmutableID for AAD
5) Merge AD and AAD outputs (Coming Soon) -->
6) Set ImmutableID using Option 4 (Coming Soon) -->
Q) Quit
Select an option.. [1-99]?
'@
[string] $ADRoot = @'
┌─────────────────────────────────────────────────────────────┐
Gather ImmutableID in Bulk using PowerShell
Follow me @thewatchernode on Twitter
This section will gather the UserPrincipleName,SamAccountName,
DisplayName,CanonicalName, EmailAddress and ImmutableID for
all AD Users..
└─────────────────────────────────────────────────────────────┘
'@
[string] $AADRoot = @'
┌─────────────────────────────────────────────────────────────┐
Gather ImmutableID in Bulk using PowerShell
Follow me @thewatchernode on Twitter
This section will gather the UserPrincipleName,ObjectID and
ImmutableID for all Azure AD Users.
└─────────────────────────────────────────────────────────────┘
'@
[string] $ImportExcel1 = @'
┌─────────────────────────────────────────────────────────────┐
Gather ImmutableID in Bulk using PowerShell
Follow me @thewatchernode on Twitter
Please Note: The worksheet in both files MUST be called
Sheet1
└─────────────────────────────────────────────────────────────┘
'@
#endregion Banner
#region Menu Prompt
function Get-Root {
# Menu Prompt
Do {
$MenuOption = Read-Host -Prompt $Root
Clear-Host
switch ($MenuOption){
1 { # Connect to AzureAD
Get-AADConnect
}
2 { # Get ImmutableID for AD
Get-ADImmutableID
}
3 { # Get ImmutableID for AAD
Get-ADDImmutableID
}
4 { # Rename exports
}
5 { # Merge XLSX Sheets and convert to CSV file
Get-ImportExcel
Get-MergeFiles
}
6 { # Set ImmutableID in Azure
Set-ImmutableID
}
$Quit {return}
}
} until ($Root -eq {$Quit})
}
#endregion Menu Prompt
#region Functions
Function Test-IsAdmin {
<#
.SYNOPSIS
Describe purpose of "Test-IsAdmin" in 1-2 sentences.
.DESCRIPTION
Add a more complete description of what the function does.
.EXAMPLE
Test-IsAdmin
Describe what this call does
.NOTES
Place additional notes here.
.LINK
URLs to related sites
The first link is opened by Get-Help -Online Test-IsAdmin
.INPUTS
List of input types that are accepted by this function.
.OUTPUTS
List of output types produced by this function.
#>
([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')
}
if (!(Test-IsAdmin)){
throw 'Please Note: You are trying to run this script without administative priviliges. In order to run this script you will required PowerShell running in Administrator Mode'
}
else {
Write-Verbose -Message 'Are you running as an Administator' -verbose
}
Function Get-AzureADPSVersion {
# Azure PowerShell Version
$ModuleVersion = Get-InstalledModule -Name $AzureAD -ErrorAction SilentlyContinue | Select-Object -Property name,version
Write-Host 'Your client machine is running the following version of AzureAD Module' -ForegroundColor $White -BackgroundColor $DarkCyan
$moduleversion
}
Function Get-AzureAD {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls1
$ModuleCheck = Get-InstalledModule -name $AzureAD -ErrorAction SilentlyContinue
if ($ModuleCheck) {
Write-Host 'Info: Detected an installation of the AzureAD Module' -ForegroundColor $White -BackgroundColor $DarkCyan
$Module = Get-Module -Name $AzureAD -ListAvailable
# Identify modules with multiple versions installed
$g = $module | Group-Object -Property name -NoElement | Where-Object count -gt 1
# Check Module from PSGallery
Write-Host 'Checking AzureAD module from the PSGallery' -ForegroundColor $White -BackgroundColor $DarkCyan
$gallery = $module | Where-Object {$_.repositorysourcelocation}
Write-Host 'Comparing installed version against online version of AzureAD module' -ForegroundColor $White -BackgroundColor $DarkCyan
foreach ($module in $gallery) {
#find the current version in the gallery
Try {
$online = Find-Module -Name $module.name -Repository PSGallery -ErrorAction Stop
}
Catch {
Write-Warning -Message ('Module {0} was not found in the PSGallery' -f $module.name)
}
#compare versions
if ($online.version -gt $module.version) {
$UpdateAvailable = 'Version removed'
Write-Host -BackgroundColor $DarkRed -ForegroundColor $White "Warning: Legacy Version of AzureAD Module detected. Starting removing process"
Uninstall-Module -Name $AzureAD -RequiredVersion $module.version
Write-Host -BackgroundColor $DarkRed -ForegroundColor $White "Info: Legacy Version of AzureAD Module now removed"
Install-Module -Name $AzureAD -RequiredVersion $online.Version -Force
}
else {
$UpdateAvailable = 'No update required'
}
#write a custom object to the pipeline
[pscustomobject]@{
Name = $module.name
MultipleVersions = ($g.name -contains $module.name)
InstalledVersion = $module.version
OnlineVersion = $online.version
Update = $UpdateAvailable
Path = $module.modulebase
}
}
# Microsoft Azure PowerShell Version
Get-AzureADPSVersion
}
else
{
Write-Host 'Error: Failed to detect an installation of the AzureAD Module' -ForegroundColor $Red
Write-Host 'Info: Attempting an installation of the AzureAD Module' -ForegroundColor $Green
Install-Module -Name $AzureAD
# Microsoft Azure PowerShell Check
Get-AzureADPSVersion
}
}
Function Get-AADConnect {
Get-AzureADPSVersion
Get-AzureAD
Write-Host "INFO: Connecting to Azure Active Directory, prompting for relevant administrative credentials" -ForegroundColor $White -BackgroundColor $DarkCyan
Connect-AzureAD
}
Function Get-ADImmutableID {
$ADRoot
$reportoutput=@()
$users = Get-ADUser -Filter * -Properties *
$users | Foreach-Object {
$user = $_
$immutableid = [System.Convert]::ToBase64String($user.ObjectGUID.tobytearray())
$userid = $user | select @{Name='Access Rights';Expression={[string]::join(', ', $immutableid)}}
$report = New-Object -TypeName PSObject
$report | Add-Member -MemberType NoteProperty -Name 'UserPrincipalName' -Value $user.UserPrincipalName
$report | Add-Member -MemberType NoteProperty -Name 'SamAccountName' -Value $user.samaccountname
$report | Add-Member -MemberType NoteProperty -Name 'DisplayName' -Value $user.displayname
$report | Add-Member -MemberType NoteProperty -Name 'CanonicalName' -Value $user.canonicalname
$report | Add-Member -MemberType NoteProperty -Name 'EmailAddress' -Value $user.emailaddress
$report | Add-Member -MemberType NoteProperty -Name 'ImmutableID' -Value $immutableid
$reportoutput += $report
}
# Report
$reportoutput | Export-Csv -Path $env:USERPROFILE\desktop\ImmutableID4AD.csv -NoTypeInformation -Encoding UTF8
Write-Host "INFO: File exported to $env:USERPROFILE\desktop\ImmutableID4AD.csv" -ForegroundColor $Green
}
Function Get-ADDImmutableID {
$AADRoot
$reportoutput=@()
$users = Get-AzureADUser | Select UserPrincipalName, SamAccountName, DisplayName, EmailAddress,ImmutableID
$users | Foreach-Object {
$user = $_
#$immutableid = "[System.Convert]::ToBase64String($user.ObjectGUID.tobytearray())"
$userid = $user | select @{Name='Access Rights';Expression={[string]::join(', ', $immutableid)}}
$report = New-Object -TypeName PSObject
$report | Add-Member -MemberType NoteProperty -Name 'UserPrincipalName' -Value $user.UserPrincipalName
$report | Add-Member -MemberType NoteProperty -Name 'SamAccountName' -Value $user.samaccountname
$report | Add-Member -MemberType NoteProperty -Name 'DisplayName' -Value $user.displayname
$report | Add-Member -MemberType NoteProperty -Name 'EmailAddress' -Value $user.emailaddress
$report | Add-Member -MemberType NoteProperty -Name 'ImmutableID' -Value $user.immutableid
$reportoutput += $report
}
# Report
$reportoutput | Export-Csv -Path $env:USERPROFILE\desktop\ImmutableID4AAD.csv -NoTypeInformation -Encoding UTF8
Write-Host "INFO: File exported to $env:USERPROFILE\desktop\ImmutableID4AAD.csv" -ForegroundColor $Green
}
Function Get-RenameCSVtoXLSX {
$proj_files = get-childitem -path "$env:userprofile\Desktop\*.csv"
ForEach ($file in $proj_files) {
$file | Rename-Item -NewName { $_.name -Replace '\.csv$','.xls' }
}
Write-Host "INFO: Files named to .xls for investigation using Excel" -ForegroundColor $Green
}
Function Get-RenameXLSXtoCSV {
$proj_files = get-childitem -path "$env:userprofile\Desktop\*.xlsx"
ForEach ($file in $proj_files) {
$filenew = $file.Name + ".csv"
Rename-Item $file $filenew
}
}
Function Get-ImportExcelPSVersion {
# MSOL PowerShell Version
$ModuleVersion = Get-InstalledModule -Name $ImportExcel | Select-Object -Property name,version
Write-Host 'Your client machine is running the following version of ImportExcel Module' -ForegroundColor $White -BackgroundColor $DarkCyan
$moduleversion
}
Function Get-ImportExcel {
$ModuleCheck = Get-InstalledModule -name $ImportExcel -ErrorAction SilentlyContinue
if ($ModuleCheck) {
Write-Host 'Info: Detected an installation of the ImportExcel Module' -ForegroundColor $Green
$Module = Get-Module -Name $ImportExcel -ListAvailable
# Identify modules with multiple versions installed
$g = $module | Group-Object -Property name -NoElement | Where-Object count -gt 1
# Check Module from PSGallery
Write-Host 'Checking ImportExcel module from the PSGallery' -ForegroundColor $White -BackgroundColor $DarkCyan
$gallery = $module | Where-Object {$_.repositorysourcelocation}
Write-Host 'Comparing installed version against online version of ImportExcel module' -ForegroundColor $White -BackgroundColor $DarkCyan
foreach ($module in $gallery) {
#find the current version in the gallery
Try {
$online = Find-Module -Name $module.name -Repository PSGallery -ErrorAction Stop
}
Catch {
Write-Warning -Message ('Module {0} was not found in the PSGallery' -f $module.name)
}
#compare versions
if ($online.version -gt $module.version) {
$UpdateAvailable = 'Version removed'
Write-Host -BackgroundColor $DarkRed -ForegroundColor $White "Warning: Legacy Version of ImportExcel Module detected. Starting removing process"
Uninstall-Module -Name $ImportExcel -RequiredVersion $module.version
Write-Host -BackgroundColor $DarkRed -ForegroundColor $White "Info: Legacy Version of ImportExcel Analyzer Module now removed"
Install-Module -Name $ImportExcel -RequiredVersion $online.Version -Force
}
else {
$UpdateAvailable = 'No update required'
}
#write a custom object to the pipeline
[pscustomobject]@{
Name = $module.name
MultipleVersions = ($g.name -contains $module.name)
InstalledVersion = $module.version
OnlineVersion = $online.version
Update = $UpdateAvailable
Path = $module.modulebase
}
}
# ImportExcel
Get-ImportExcelPSVersion
}
else
{
Write-Host 'Error: Failed to detect an installation of the ImportExcel Module' -ForegroundColor $Red
Write-Host 'Info: Attempting an installation of the ImportExcel Module' -ForegroundColor $Green
Install-Module -Name $ImportExcel
# ImportExcel
Get-ImportExcelPSVersion
}
}
Function Get-MergeFiles {
#$ref = Read-Host -Prompt 'Specify your Reference File for example (c:\ref.xlsx)'
#Write-Host -Foreground $Cyan ('You have specified {0}' -f $ref)
#$dif = Read-Host -Prompt 'Specify your Difference File for example (c:\dif.xlsx)'
#Write-Host -Foreground $Cyan ('You have specified {0}' -f $dif)
Write-Host 'Specify your 1st source reference file ' -ForegroundColor $White -BackgroundColor $DarkCyan
$Ref = Get-Filename
Write-Host 'Specify your 2nd source reference file ' -ForegroundColor $White -BackgroundColor $DarkCyan
$Dif = Get-FileName
$out = Read-Host -Prompt 'Specify your output file for example (c:\output.xlsx)'
Write-Host ('You have specified {0}' -f $out) -ForegroundColor $White -BackgroundColor $DarkCyan
Merge-Worksheet -Referencefile $ref -Differencefile $dif -OutputFile $out -WorksheetName Sheet1 -Startrow 1 -OutputSheetName Sheet1 -NoHeader
}
Function Set-ImmutableID {
#endregion
Start-Transcript $env:USERPROFILE\desktop\PilotUser.csv
foreach($user in $csv1){
Set-AzureADUser -ObjectID $user.ObjectId -ImmutableID $user.ImmutableID
Write-Host $user.PrimarySMTPAddress,"with ObjectID"$user.ObjectId," has been set with ImmutableID",$user.ImmutableID
}
Stop-Transcript
Write-Host "Starting AD Sync Cycle Delta"
Start-ADSyncSyncCycle -PolicyType Delta
}
Function Get-FileName
{
[CmdletBinding()]
param
(
[Object]$initialDirectory
)
Add-Type -AssemblyName System.windows.forms | Out-Null
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $initialDirectory
$OpenFileDialog.filter = 'XLS (*.xls*)| *.xls*'
$OpenFileDialog.ShowDialog() | Out-Null
$OpenFileDialog.filename
}
#endregion
#endregion
#region Code Launch
clear-host
#Requires -Version 5.0
Write-Output "I'm version 5.0 or above"
$PSVersionTable
Test-IsAdmin
Write-host 'Version information - You are running script version 1.2' -ForegroundColor White -BackgroundColor DarkGray
Get-Root
#endregion Code Launch
When hard matching provides a match, soft matching is not attempted. However, the non-matching rules still apply.
Soft Matching
When hard matching doesn’t provide a match, soft matching is attempted. Soft matching is little more straight-forward than hard matching as it’s based on the following data:

Through soft matching, an on-premises Active Directory user object is matched to an Azure AD user object, when:
- The userPrincipalName attributes match
- The userPrincipalName attribute for the on-premises user object matches with the e-mail address denoted with SMTP: in the proxyAddresses attribute of the Azure AD user object
- The primary SMTP address (denoted with SMTP: in the proxyAddresses attribute) matches the userPrincipalName of the Azure AD user object
When soft matching provides a match, hard matching is established at the first synchronization cycle by setting the immutableID attribute for the Azure AD user object, based on the sourceanchor configuration. This is done for disaster recovery purposes: When the (only) Azure AD Connect installation fails, a replacement Azure AD Connect installation can pick up synchronization for end users by accurate hard matching.