- Jul 24, 2025
Move Users Between Groups in Microsoft Entra ID
- Trainer
- 0 comments
Ever needed to move users from one group to another in Microsoft Entra ID, but didn't want to risk breaking everything by doing it all at once? I've been there. Whether you're cleaning up after a reorganization or rolling out new security policies, bulk group changes can be nerve-wracking.
Here's a PowerShell solution that lets you move users between groups in small, manageable batches – so you can sleep better at night.
Why This Matters
Most group management scripts are all-or-nothing affairs. You either move everyone at once (scary) or do it manually one by one (tedious). Neither approach gives you the control you need when dealing with hundreds of users.
This script solves that by letting you:
Choose exactly how many users to move at a time
Preview changes before making them
Continue or stop after each batch
See exactly what's happening in real-time
How It Works
The script is pretty straightforward. It connects to Microsoft Graph, finds your source and destination groups, figures out which users need to be moved, and then processes them in whatever batch size you choose.
Getting Started
First, make sure you're running this with admin privileges. You'll need one of these roles:
Global Administrator
Groups Administrator
Privileged Role Administrator
The script will ask for these permissions:
powershell
# Connect to Microsoft Graph with explicit scopes
Connect-MgGraph -Scopes "Group.Read.All", "GroupMember.ReadWrite.All", "User.Read.All" -NoWelcome
# Define group display names
$group1 = "Cyber_PAM_Roles"
$group2 = "Entra_OR_BG_1"
# Create arrays to track changes
$addedMembers = @()
$skippedMembers = @()
$errorMembers = @()
# Get group Object IDs using proper filter syntax
try {
$group1ObjectID = (Get-MgGroup -Filter "displayName eq '$group1'" -ErrorAction Stop).Id
Write-Host "Found group '$group1' with ID: $group1ObjectID" -ForegroundColor Cyan
$group2ObjectID = (Get-MgGroup -Filter "displayName eq '$group2'" -ErrorAction Stop).Id
Write-Host "Found group '$group2' with ID: $group2ObjectID" -ForegroundColor Cyan
} catch {
Write-Error "Error finding groups: $_"
Write-Host "Troubleshooting: Let's list some groups to verify connectivity..." -ForegroundColor Yellow
Get-MgGroup -Top 5 | Select-Object Id, DisplayName
exit
}
# Check if we successfully got the group IDs
if (-not $group1ObjectID -or -not $group2ObjectID) {
Write-Error "Could not find one or both groups. Please verify the group names."
exit
}
# Get members of both groups
try {
$membersGroup1 = Get-MgGroupMember -GroupId $group1ObjectID -All -ErrorAction Stop
Write-Host "Retrieved $($membersGroup1.Count) members from source group" -ForegroundColor Green
$existingMembersGroup2 = Get-MgGroupMember -GroupId $group2ObjectID -All -ErrorAction Stop
Write-Host "Retrieved $($existingMembersGroup2.Count) existing members from destination group" -ForegroundColor Green
$existingMemberIds = $existingMembersGroup2.Id
} catch {
Write-Error "Error retrieving group members: $_"
exit
}
Write-Host "`n=== Starting Synchronization Process ===" -ForegroundColor Magenta
# Copy members from Group1 to Group2, skipping duplicates
foreach ($member in $membersGroup1) {
if ($existingMemberIds -notcontains $member.Id) {
try {
New-MgGroupMember -GroupId $group2ObjectID -DirectoryObjectId $member.Id -ErrorAction Stop
# Get user details for display
$userInfo = Get-MgUser -UserId $member.Id -ErrorAction SilentlyContinue
if ($userInfo) {
$memberDetails = [PSCustomObject]@{
DisplayName = $userInfo.DisplayName
UserPrincipalName = $userInfo.UserPrincipalName
Department = $userInfo.Department
JobTitle = $userInfo.JobTitle
Id = $member.Id
}
$addedMembers += $memberDetails
Write-Host " Added: $($userInfo.DisplayName) ($($userInfo.UserPrincipalName))" -ForegroundColor Green
} else {
$memberDetails = [PSCustomObject]@{
DisplayName = "Unknown"
Id = $member.Id
Type = "Possibly a service principal or other object"
}
$addedMembers += $memberDetails
Write-Host "Added: $($member.Id) (Non-user object)" -ForegroundColor Green
}
} catch {
$errorMembers += [PSCustomObject]@{
Id = $member.Id
Error = $_.Exception.Message
}
Write-Warning "Failed to add $($member.Id): $_"
}
} else {
# Get user details for display
$userInfo = Get-MgUser -UserId $member.Id -ErrorAction SilentlyContinue
if ($userInfo) {
$skippedMembers += [PSCustomObject]@{
DisplayName = $userInfo.DisplayName
UserPrincipalName = $userInfo.UserPrincipalName
Id = $member.Id
}
Write-Host "Already in group: $($userInfo.DisplayName) ($($userInfo.UserPrincipalName))" -ForegroundColor DarkGray
} else {
$skippedMembers += [PSCustomObject]@{
Id = $member.Id
Type = "Non-user object"
}
Write-Host "Already in group: $($member.Id) (Non-user object)" -ForegroundColor DarkGray
}
}
}
# Summary section
Write-Host "`n=== Synchronization Summary ===" -ForegroundColor Magenta
Write-Host "Total processed: $($membersGroup1.Count)" -ForegroundColor Cyan
Write-Host "Added: $($addedMembers.Count)" -ForegroundColor Green
Write-Host "Skipped (already members): $($skippedMembers.Count)" -ForegroundColor DarkGray
Write-Host "Errors: $($errorMembers.Count)" -ForegroundColor Red
# Display detailed information about added members
if ($addedMembers.Count -gt 0) {
Write-Host "`n=== Newly Added Members ===" -ForegroundColor Green
$addedMembers | Format-Table -AutoSize -Property DisplayName, UserPrincipalName, Department, JobTitle
}
# If there were errors, show details
if ($errorMembers.Count -gt 0) {
Write-Host "`n=== Failed Additions ===" -ForegroundColor Red
$errorMembers | Format-Table -AutoSize
}
# Verify no duplicates exist in the final group
Write-Host "`n=== Verifying Final Group Integrity ===" -ForegroundColor Magenta
$finalMembers = Get-MgGroupMember -GroupId $group2ObjectID -All
$uniqueIds = @{}
$duplicateCount = 0
foreach ($member in $finalMembers) {
if ($uniqueIds.ContainsKey($member.Id)) {
Write-Warning " Duplicate member found: $($member.Id)"
$duplicateCount++
} else {
$uniqueIds[$member.Id] = $true
}
}
if ($duplicateCount -eq 0) {
Write-Host " Verification complete: No duplicate members found in the destination group" -ForegroundColor Green
} else {
Write-Host " Verification found $duplicateCount duplicate members in the destination group" -ForegroundColor Red
}
# Get current members of destination group with detailed information
Write-Host "`nCurrent Members of '$group2' ($($finalMembers.Count) total):" -ForegroundColor Cyan
# Create a more useful output table
$outputMembers = $finalMembers | ForEach-Object {
$user = Get-MgUser -UserId $_.Id -ErrorAction SilentlyContinue
if ($user) {
[PSCustomObject]@{
DisplayName = $user.DisplayName
UserPrincipalName = $user.UserPrincipalName
Department = $user.Department
JobTitle = $user.JobTitle
AddedToday = ($addedMembers.Id -contains $_.Id)
}
} else {
# Handle non-user objects (like service principals)
[PSCustomObject]@{
DisplayName = "Non-user object"
ObjectId = $_.Id
AddedToday = ($addedMembers.Id -contains $_.Id)
}
}
}
# Show the output table with sorting and highlighting
$outputMembers | Sort-Object -Property AddedToday -Descending | Format-Table -AutoSize
# Export results to CSV if needed (commented out by default)
# $timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
# $addedMembers | Export-Csv -Path "AddedMembers_$timestamp.csv" -NoTypeInformation
# Write-Host "Exported added members to: AddedMembers_$timestamp.csv" -ForegroundColor CyanUsing the Script
Enter your group names - The script asks for source and destination groups (with sensible defaults)
-
Choose your batch size - This is where you decide how many users to process:
Enter
0to just preview what would happenEnter
5for a small test batchEnter
50if you're feeling confident
-
Watch it work - You'll see real-time updates like:
Added (1/10): John Doe (john.doe@company.com) Added (2/10): Jane Smith (jane.smith@company.com) Continue or stop - After each batch, decide if you want to keep going
What I Like About This Approach
It's safe - You can start with just a few users to test everything works It's visible - You see exactly what's happening as it happens It's flexible - Change your mind halfway through? No problem It's smart - Skips users who are already in the destination group
When Things Go Wrong
If you hit permission errors, double-check you're using an admin account. The most common issue is "Authorization_RequestDenied" which usually means your account doesn't have the right roles.
The script also handles individual user failures gracefully - if one user can't be moved, it keeps going with the others and shows you a summary at the end.
Pro Tips
Start small - Always begin with a batch of 5-10 users to make sure everything works in your environment.
Use preview mode - Run with 0 users first to see what would change without actually changing anything.
Run during off-hours - Large migrations are best done when fewer people are working.
Keep records - The script can export results to CSV for your documentation.
Real-World Example
Last month, I used this to move 200 users from temporary COVID groups back to their department groups. Instead of moving everyone at once and potentially causing access issues, I:
Started with 5 users in preview mode
Moved 10 users as a test
Increased to batches of 25 once I was confident
Completed the migration over a few days during lunch breaks
No drama, no emergency calls, no stressed-out users wondering why they couldn't access files.
The Bottom Line
Group management doesn't have to be scary. With the right approach, you can make changes confidently, knowing you're in control every step of the way.
This script gives you that control. It's not fancy, but it works reliably and lets you sleep at night – which, let's be honest, is what we all want from our IT tools.