If you’ve ever needed to set up a new employee with the same access as an existing team member, or transition responsibilities between users, you know the pain of manually adding someone to dozens of groups. It’s tedious, error-prone, and frankly, there’s got to be a better way.
Well, there is, but it’s not quite as straightforward as you might hope. This blog walks you through how group membership copying actually works in Microsoft Entra ID, including the quirks you need to know about!
And, as an alternative, we’ll show you how to simply copy/paste AD, MS Graph, and Exchange Online group memberships between users!
Let’s break it down.
Not All Group Memberships in Microsoft 355 Can Be Copied With MS Graph
Before attempting to copy group memberships, it’s important to understand that not all group types in Microsoft 365 behave the same way. Each group type has its own management model and technical limitations.
| Group Type | Can Copy? |
| Microsoft 365 Groups | Yes |
| Security Groups | Yes |
| Distribution Lists | Limited* |
| Mail-Enabled Security | Limited* |
| Dynamic Groups | No |
| Synchronized Groups | No |
Important Points to Note:
-
- Distribution Lists and Mail-Enabled Security groups require Exchange Online PowerShell cmdlets rather than Microsoft Graph.
-
- Dynamic groups pose a unique challenge. Unlike static groups where administrators manually add or remove members, dynamic group membership is determined by Azure AD rules based on user attributes such as department, location, or job title. Since membership is rule-based, you cannot manually add users to dynamic groups.
-
- Synchronized groups (groups synced from on-premises Active Directory via Azure AD Connect/Entra Connect) cannot be managed through Microsoft Graph. These groups must be modified at the source using the ActiveDirectory PowerShell module in your on-premises environment. Changes made on-premises will then sync to Entra ID.
How To Copy Group Memberships Between Users in Microsoft Entra ID
Group memberships in Microsoft 365 can be copied either manually or through a scripted approach.
-
- While the manual method may seem straightforward, it is practical only for small, one-time scenarios. Manually reviewing each group and adding users through the admin portals quickly becomes time-consuming, increases the risk of human error, and offers no reliable way to validate the results.
-
- Because of these limitations, a script-based approach is strongly recommended. Using PowerShell scripts allows administrators to automatically identify supported group types, exclude unsupported ones, and copy memberships consistently.
PowerShell Script To Copy Group Memberships From One User To Another
The following PowerShell script copies static Microsoft 365 and security group memberships from one user to another. Dynamic groups are automatically detected and skipped.
Note: Distribution Lists and Mail-Enabled Security groups are not supported in this script. Synchronized groups from on-premises Active Directory are also not supported and must be managed using the Active Directory PowerShell module.
Prerequisites
Before running the script, ensure the Microsoft Graph PowerShell SDK is installed. The required permissions User.Read.All, Group.Read.All, and GroupMember.ReadWrite.All must be granted. You must also have an appropriate admin role, such as Global Administrator, Privileged Role Administrator, Groups Administrator, or Users Administrator.
Script
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory = $true, HelpMessage = "Enter the source user's email or object ID")]
[string]$SourceUserId,
[Parameter(Mandatory = $true, HelpMessage = "Enter the target user's email or object ID")]
[string]$TargetUserId,
[Parameter(Mandatory = $false, HelpMessage = "Path to export CSV report")]
[string]$ExportReport
)
# Function to connect to Microsoft Graph
function Connect-ToMicrosoftGraph {
Write-Host "`n[INFO] Connecting to Microsoft Graph..." -ForegroundColor Cyan
try {
Connect-MgGraph -Scopes "User.Read.All", "Group.Read.All", "GroupMember.ReadWrite.All" -NoWelcome -ErrorAction Stop
Write-Host "[SUCCESS] Connected to Microsoft Graph" -ForegroundColor Green
}
catch {
Write-Host "[ERROR] Failed to connect to Microsoft Graph: $($_.Exception.Message)" -ForegroundColor Red
exit
}
}
# Function to verify user exists
function Test-UserExists {
param([string]$UserId, [string]$UserType)
try {
$User = Get-MgUser -UserId $UserId -ErrorAction Stop
Write-Host "[SUCCESS] $UserType user found: $($User.DisplayName) ($($User.UserPrincipalName))" -ForegroundColor Green
return $User
}
catch {
Write-Host "[ERROR] $UserType user not found: $UserId" -ForegroundColor Red
Write-Host "[ERROR] $($_.Exception.Message)" -ForegroundColor Red
exit
}
}
# Main Script Execution
Write-Host "`n========================================" -ForegroundColor Yellow
Write-Host " M365 Group Membership Copy Tool" -ForegroundColor Yellow
Write-Host "========================================`n" -ForegroundColor Yellow
# Connect to Microsoft Graph
Connect-ToMicrosoftGraph
# Verify both users exist
Write-Host "`n[INFO] Verifying users..." -ForegroundColor Cyan
$SourceUser = Test-UserExists -UserId $SourceUserId -UserType "Source"
$TargetUser = Test-UserExists -UserId $TargetUserId -UserType "Target"
# Get source user's group memberships
Write-Host "`n[INFO] Retrieving group memberships for source user..." -ForegroundColor Cyan
try {
$SourceGroups = Get-MgUserMemberOf -UserId $SourceUser.Id -All -ErrorAction Stop
}
catch {
Write-Host "[ERROR] Failed to retrieve group memberships: $($_.Exception.Message)" -ForegroundColor Red
exit
}
# Filter out non-group objects and get full group details
$GroupsToProcess = @()
$SkippedDynamic = @()
$Report = [System.Collections.Generic.List[Object]]::new()
Write-Host "[INFO] Analyzing groups..." -ForegroundColor Cyan
foreach ($Membership in $SourceGroups) {
# Only process if it's a group
if ($Membership.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.group') {
try {
# Get full group details to check if it's dynamic
$GroupDetails = Get-MgGroup -GroupId $Membership.Id -ErrorAction Stop
# Check if group is dynamic
if ($GroupDetails.GroupTypes -contains "DynamicMembership") {
$SkippedDynamic += [PSCustomObject]@{
GroupName = $GroupDetails.DisplayName
GroupId = $GroupDetails.Id
Reason = "Dynamic group - membership is rule-based"
}
Write-Host " [SKIP] Dynamic group: $($GroupDetails.DisplayName)" -ForegroundColor Yellow
}
else {
$GroupsToProcess += $GroupDetails
}
}
catch {
Write-Host " [WARNING] Could not retrieve details for group $($Membership.Id)" -ForegroundColor Yellow
}
}
}
# Display summary
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host " Summary" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Total groups found: $($SourceGroups.Count)" -ForegroundColor White
Write-Host "Static groups to copy: $($GroupsToProcess.Count)" -ForegroundColor Green
Write-Host "Dynamic groups (skipped): $($SkippedDynamic.Count)" -ForegroundColor Yellow
Write-Host "========================================`n" -ForegroundColor Cyan
if ($GroupsToProcess.Count -eq 0) {
Write-Host "[INFO] No static groups to copy. Exiting." -ForegroundColor Cyan
exit
}
# Process each group
$SuccessCount = 0
$AlreadyMemberCount = 0
$FailedCount = 0
Write-Host "[INFO] Processing group memberships...`n" -ForegroundColor Cyan
foreach ($Group in $GroupsToProcess) {
$Status = ""
$Action = ""
try {
# Check if target user is already a member
$ExistingMembers = Get-MgGroupMember -GroupId $Group.Id -All -ErrorAction Stop
$IsAlreadyMember = $ExistingMembers | Where-Object { $_.Id -eq $TargetUser.Id }
if ($IsAlreadyMember) {
$Status = "Already Member"
$Action = "Skipped"
$AlreadyMemberCount++
Write-Host " [SKIP] Already member: $($Group.DisplayName)" -ForegroundColor Yellow
}
else {
if ($PSCmdlet.ShouldProcess("$($Group.DisplayName)", "Add $($TargetUser.DisplayName) to group")) {
New-MgGroupMember -GroupId $Group.Id -DirectoryObjectId $TargetUser.Id -ErrorAction Stop
$Status = "Success"
$Action = "Added"
$SuccessCount++
Write-Host " [SUCCESS] Added to: $($Group.DisplayName)" -ForegroundColor Green
}
else {
$Status = "WhatIf"
$Action = "Would Add"
Write-Host " [WHATIF] Would add to: $($Group.DisplayName)" -ForegroundColor Cyan
}
}
}
catch {
$Status = "Failed"
$Action = "Error"
$FailedCount++
Write-Host " [ERROR] Failed: $($Group.DisplayName) - $($_.Exception.Message)" -ForegroundColor Red
}
# Add to report
$Report.Add([PSCustomObject]@{
GroupName = $Group.DisplayName
GroupId = $Group.Id
GroupType = if ($Group.GroupTypes -contains "Unified") { "Microsoft 365" }
elseif ($Group.SecurityEnabled -and $Group.MailEnabled) { "Mail-Enabled Security" }
elseif ($Group.SecurityEnabled) { "Security" }
else { "Distribution" }
Status = $Status
Action = $Action
SecurityEnabled = $Group.SecurityEnabled
MailEnabled = $Group.MailEnabled
})
}
# Final summary
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " Operation Complete" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host "Successfully added: $SuccessCount" -ForegroundColor Green
Write-Host "Already a member: $AlreadyMemberCount" -ForegroundColor Yellow
Write-Host "Failed: $FailedCount" -ForegroundColor $(if ($FailedCount -gt 0) { "Red" } else { "Green" })
Write-Host "Dynamic groups skipped: $($SkippedDynamic.Count)" -ForegroundColor Yellow
Write-Host "========================================`n" -ForegroundColor Green
# Export report if requested
if ($ExportReport) {
try {
$Report | Export-Csv -Path $ExportReport -NoTypeInformation -Force
Write-Host "[SUCCESS] Report exported to: $ExportReport" -ForegroundColor Green
# Also export skipped dynamic groups if any
if ($SkippedDynamic.Count -gt 0) {
$DynamicReportPath = $ExportReport -replace '\.csv$', '_DynamicGroupsSkipped.csv'
$SkippedDynamic | Export-Csv -Path $DynamicReportPath -NoTypeInformation -Force
Write-Host "[INFO] Skipped dynamic groups exported to: $DynamicReportPath" -ForegroundColor Cyan
}
}
catch {
Write-Host "[ERROR] Failed to export report: $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host "`n[INFO] Script execution completed.`n" -ForegroundColor Cyan
Why Scripts Still Aren’t Ideal for Everyone
While PowerShell scripts are powerful, they’re not always practical:
-
- They require admin permissions and scripting knowledge.
- Every variation (DLs, mail-enabled security) often means another script.
The Easy Way: Using EasyEntra
While those scripts work great, there’s a much simpler approach if you’re managing this regularly.
EasyEntra provides a straightforward interface that lets you copy group memberships in just a few clicks using standard Ctrl+C and Ctrl+V keyboard shortcuts. The tool automatically handles both Entra ID and Exchange Online groups, checks for existing memberships, and skips the problematic group types we discussed, all without writing a single line of code!
For organizations managing hybrid environments, EasyEntra goes a step further by supporting on-premises Active Directory groups as well. This gives administrators a single, consistent way to manage user access across cloud and on-premises identities, without switching tools or maintaining multiple scripts.
Overall, EasyEntra is ideal for teams that want to save time, reduce manual effort, and safely delegate group management tasks, while still maintaining accuracy and control over user access!
For detailed instructions on copying group memberships from one user to another in EasyEntra, visit:
https://easyentra.com/knowledge-base/how-to/how-to-copy-group-membership-from-one-user-to-another/