Understanding User Hard Matching and Soft Matching in Azure AD Connect

Azure AD Connect

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:

Azure AD Connect's Uniquely identifying your users page (click for original screenshot)

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:

  1. Does not exceed 60 characters in length
  2. Does not contain special characters like \ ! # $ % & * + / = ?  and ^
  3. Is globally unique throughout your organization
  4. 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:

Azure AD Connect Hard Matching Table

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:

Azure AD Connect Soft Matching Table

Through soft matching, an on-premises Active Directory user object is matched to an Azure AD user object, when:

  1. The userPrincipalName attributes match
  2. 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
  3. 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.