Skip to content

Script to Backup and Restore Azure AD Group Members and Owners by ImmutableId

This comprehensive script facilitates the backup and restoration of Azure AD group members and owners based on their ImmutableId. It enables seamless migration between tenants by preserving group structures and membership relationships, with advanced features for domain replacement and XML-based data persistence.


FeatureCapabilityBusiness Value
Group BackupExports group info, members, and owners to XML filesPreserves complete group structure
ImmutableId MatchingUses ImmutableId for user identification across tenantsEnsures accurate user mapping
Cross-Tenant MigrationSupports migration between source and destination tenantsEnables tenant-to-tenant transfers
Domain ReplacementReplaces tenant-specific keywords in XML filesAdapts data for new environment

Ideal for: Migration specialists, Azure AD administrators, and enterprise architects

Prerequisites:

  • Azure AD PowerShell Module installed
  • Global Administrator role in both tenants
  • User synchronization with ImmutableId preservation
  • Sufficient permissions for group management

Terminal window
$backupFolder = "$home\Documents\GroupsBackup"
$sourceTenantDomain = ""
$sourceTenantDomainName = @("", "domain")
$destinationTenantDomain = ""
$destinationTenantDomainName = ""

Terminal window
# This script restore the members by ImmutableId!!!
$backupFolder = "$home\Documents\GroupsBackup"
# Source and destination tenant domain names
$sourceTenantDomain = ""
$sourceTenantDomainName =@("", "domain")
$destinationTenantDomain = ""
$destinationTenantDomainName = ""
function New-DirectoryIfNotExist {
param(
[string]$Path
)
if (Test-Path -Path $Path -PathType Container) {
$FolderProperty = Get-ItemProperty $Path
$date = ($FolderProperty.CreationTime.ToString()).Split('/')
$date += $date[2].Split(' ')
$date += $date[4].Replace(':', '')
$suffix = '-' + $date[3] + '-' + $date[1] + '-' + $date[5]
$NewName = 'GroupsBackup' + $suffix
Rename-Item -Path $Path -NewName $NewName
}
New-Item -ItemType Directory -Path $Path | Out-Null
Write-Output "Directory '$Path' created."
}
function Backup-Groups {
New-DirectoryIfNotExist -Path $backupFolder
try {
Connect-AzureAD -TenantId $sourceTenantDomain -ErrorAction Stop
}
catch {
Write-Output "Failed to connect to the source tenant. Error: $($_.Exception.Message)"
return
}
try {
$allGroups = Get-AzureADMSGroup -All $true |
Where-Object {
$null -eq $_.OnPremisesSecurityIdentifier -and
$_.GroupTypes -notcontains "Unified" -and
$_.MailEnabled -eq $true -and
$_.MembershipRule -ne "All Users"
}
}
catch {
Write-Output "Failed to retrieve groups. Error: $($_.Exception.Message)"
return
}
foreach ($sourceGroup in $allGroups) {
try {
$sourceGroupMembers = Get-AzureADGroupMember -ObjectId $sourceGroup.Id -All $true
$sourceGroupOwner = Get-AzureADGroupOwner -ObjectId $sourceGroup.Id -All $true
$exportPath = Join-Path -Path $backupFolder -ChildPath "$($sourceGroup.Id)_$($sourceGroup.DisplayName)"
$xmlPath = $exportPath + '_Info.xml'
$sourceGroup | Export-Clixml -Path $xmlPath
$xmlPath = $exportPath + '_members.xml'
$sourceGroupMembers | Export-Clixml -Path $xmlPath
$xmlPath = $exportPath + '_Owner.xml'
$sourceGroupOwner | Export-Clixml -Path $xmlPath
}
catch {
Write-Output "Failed to backup group $($sourceGroup.DisplayName). Error: $($_.Exception.Message)"
}
}
}
function Restore-Groups {
if (Test-Path -Path $backupFolder -PathType Container) {
$AllGroups = @()
$xmlFiles = Get-ChildItem -Path $backupFolder
$xmlFiles | Where-Object Name -Like "*_Info.xml" | ForEach-Object {
$AllGroups += Import-Clixml -Path $_.FullName
}
try {
Connect-AzureAD -TenantId $destinationTenantDomain -ErrorAction Stop
}
catch {
Write-Output "Failed to connect to the destination tenant. Error: $($_.Exception.Message)"
return
}
$GroupsInfo = @()
foreach ($sourceGroup in $AllGroups) {
try {
$destinationGroup = Get-AzureADMSGroup -Filter "DisplayName eq '$($sourceGroup.DisplayName)'" -ErrorAction SilentlyContinue
if ($null -eq $destinationGroup) {
$destinationGroup = New-AzureADMSGroup -DisplayName $($sourceGroup.DisplayName) -MailEnabled $($sourceGroup.MailEnabled) -MailNickname $($sourceGroup.MailNickname) -SecurityEnabled $($sourceGroup.SecurityEnabled) -Description $($sourceGroup.Description) -GroupTypes $($sourceGroup.GroupTypes) -IsAssignableToRole $($sourceGroup.IsAssignableToRole) -MembershipRule $($sourceGroup.MembershipRule) -MembershipRuleProcessingState $($sourceGroup.MembershipRuleProcessingState) -Visibility $($sourceGroup.Visibility)
}
$GroupsInfo += @{
sourceGroupID = $($sourceGroup.Id);
GroupDisplayName = $($sourceGroup.DisplayName);
destinationGroupID = $($destinationGroup.Id)
}
if ($sourceGroup.GroupTypes -notcontains 'DynamicMembership') {
$exportPath = Join-Path -Path $backupFolder -ChildPath "$($sourceGroup.Id)_$($sourceGroup.DisplayName)"
$xmlPath = $exportPath + '_members.xml'
$sourceGroupMembers = Import-Clixml -Path $xmlPath
$sourceGroupMembers = $sourceGroupMembers | Where-Object { $_.ObjectType -eq "User" }
$destinationGroupMembers = Get-AzureADGroupMember -ObjectId $destinationGroup.Id | Where-Object { $_.ObjectType -eq "User" } -ErrorAction SilentlyContinue
foreach ($sourceMember in $sourceGroupMembers) {
if ($sourceMember.ImmutableId -notin $destinationGroupMembers.ImmutableId) {
$userToAdd = Get-AzureADUser -Filter "ImmutableId eq '$($sourceMember.ImmutableId)'"
if ($userToAdd.Count -gt 1) {
Write-Output "Multiple users found with the same ImmutableId: $($userToAdd.UserPrincipalName)"
continue
}
if ($userToAdd) {
Add-AzureADGroupMember -ObjectId $destinationGroup.Id -RefObjectId $userToAdd.ObjectId
}
}
}
foreach ($destinationMember in $destinationGroupMembers) {
if ($destinationMember.ImmutableId -notin $sourceGroupMembers.ImmutableId) {
Remove-AzureADGroupMember -ObjectId $destinationGroup.Id -MemberId $destinationMember.ObjectId
}
}
}
$xmlPath = $exportPath + '_Owner.xml'
$sourceGroupOwner = Import-Clixml -Path $xmlPath
$sourceGroupOwner = $sourceGroupOwner | Where-Object { $_.ObjectType -eq "User" }
$destinationGroupOwner = Get-AzureADGroupOwner -ObjectId $destinationGroup.Id | Where-Object { $_.ObjectType -eq "User" } -ErrorAction SilentlyContinue
foreach ($sourceOwner in $sourceGroupOwner) {
if ($sourceOwner.ImmutableId -notin $destinationGroupOwner.ImmutableId) {
$userToAdd = Get-AzureADUser -Filter "ImmutableId eq '$($sourceOwner.ImmutableId)'"
if ($userToAdd.Count -gt 1) {
Write-Output "Multiple users found with the same ImmutableId: $($userToAdd.UserPrincipalName)"
continue
}
if ($userToAdd) {
Add-AzureADGroupOwner -ObjectId $destinationGroup.Id -RefObjectId $userToAdd.ObjectId
}
}
}
foreach ($destinationOwner in $destinationGroupOwner) {
if ($destinationOwner.ImmutableId -notin $sourceGroupOwner.ImmutableId) {
Remove-AzureADGroupOwner -ObjectId $destinationGroup.Id -OwnerId $destinationOwner.ObjectId
}
}
}
catch {
Write-Output "Failed to restore group $($sourceGroup.DisplayName). Error: $($_.Exception.Message)"
}
}
$path = Join-Path -Path $backupFolder -ChildPath ".GroupsInfo.csv"
$GroupsInfo | Export-Clixml -Path $path
}
}
# Function to find and replace keywords in XML file
function FindAndReplaceKeywordsInXML {
param (
[string]$FilePath,
$Keywords
)
# Read the content of the XML file
$content = Get-Content -Path $FilePath -Raw
# Check if the file contains any of the specified keywords
$foundKeywords = $Keywords | Where-Object { $content -match $_ }
if ($foundKeywords.Count -gt 0) {
# Replace the keywords with "I_Find_U"
$content = $content -replace ($Keywords -join '|'), "$destinationTenantDomainName"
# Save the modified content back to the same file
$content | Set-Content -Path $FilePath
Write-Output "Keywords replaced in $FilePath"
} else {
Write-Output "No keywords found in $FilePath"
}
}
function Convert-Domains {
# Set the folder path where XML files are located
$FolderPath = $backupFolder
# Set the keywords to search for
$Keywords = $sourceTenantDomainName
# Get a list of all XML files in the folder
$xmlFiles = Get-ChildItem -Path $FolderPath -Filter "*.xml" -File
# Process each XML file
foreach ($file in $xmlFiles) {
FindAndReplaceKeywordsInXML -FilePath $file.FullName -Keywords $Keywords
}
}
# Uncomment the following lines to execute the backup or restore functions
# Backup-Groups
# Convert-Domains
Restore-Groups
---
## Migration Process
### Phase 1: Backup
1. **Execute Backup-Groups** function
2. **XML files** created with group information
3. **Members and owners** exported separately
4. **Backup folder** automatically timestamped
### Phase 2: Domain Conversion
1. **Run Convert-Domains** function
2. **Tenant-specific keywords** replaced
3. **Domain references** updated for destination
4. **XML files** modified in place
### Phase 3: Restore
1. **Execute Restore-Groups** function
2. **Groups created** if not existing
3. **Members matched** by ImmutableId
4. **Ownership transferred** appropriately
---
## File Structure
### Backup Files Generated
| File Type | Naming Convention | Content |
|-----------|-------------------|---------|
| **Group Info** | `{GroupId}_{DisplayName}_Info.xml` | Group properties and settings |
| **Members** | `{GroupId}_{DisplayName}_members.xml` | Member list with ImmutableId |
| **Owners** | `{GroupId}_{DisplayName}_Owner.xml` | Owner list with ImmutableId |
---
## Usage Scenarios
### Tenant-to-Tenant Migrations
- Preserve group structures during mergers
- Maintain access control during divestitures
- Transfer business unit configurations
### Disaster Recovery
- Backup group configurations
- Restore after accidental deletions
- Maintain business continuity
---
> **Key Takeaway:** This script provides a robust solution for Azure AD group migration, ensuring seamless transfer of group structures and membership relationships while maintaining data integrity through ImmutableId-based matching.