Shore Up
Two matching record cards merging into one beside a magnifying glass
Windows

Find and Merge Duplicate Active Directory User Accounts

Ketan Aagja9 min read
No ratings yet

Before I start, one honesty note that shapes this whole guide: Active Directory has no merge operation. There is no Merge-ADUser cmdlet, and there never was. When people say "merge duplicate accounts," what they actually need is a repeatable process to find the duplicates, decide which one survives, copy the things that matter (mainly group memberships) onto the survivor, and then retire the other. That is what this guide automates. The detection half is safe and read-only. The consolidation half changes and can delete accounts, so it is gated hard.

Before you run this

What this does. The first script is read-only: it groups user objects by a business key (I use EmployeeID) and reports where two or more enabled/disabled accounts share the same value — your likely duplicates. The second script is a function that copies one account's group memberships onto the account you're keeping and then disables (or optionally deletes) the duplicate.

Privileges. Detection runs fine as any authenticated domain user with read access. The consolidation function needs write rights: Domain Admins, or an account with delegated permission to modify group membership and disable/delete users in the target OU. Run it from an elevated PowerShell 5.1 session on a workstation with the RSAT ActiveDirectory module installed (Import-Module ActiveDirectory).

Test first. Read both scripts before running. Create two throwaway test users that share an EmployeeID in a lab OU and run the whole process against them before you point this at real staff. Do not paste-and-run against a live OU.

What changes and what's irreversible. Adding group memberships to the surviving account is easily reversible. Disabling the duplicate is reversible (Enable-ADAccount). Deleting it is not, unless the AD Recycle Bin is enabled in your forest — and even then a restored object may not come back with everything intact. This process does not move the account's SID, its Exchange mailbox, file-share ACLs, home directory, or profile. If the duplicate owns a mailbox or is stamped on file permissions, disabling and deleting it breaks those. Treat every duplicate as a manual judgement call, not a bulk sweep.

Assumptions

  • Windows Server AD domain, functional level 2016 or later.
  • Admin workstation running PowerShell 5.1 with RSAT / ActiveDirectory module.
  • Duplicates are identified by a shared EmployeeID. If your environment keys on something else — mail, DisplayName, or a base UPN — change the $key variable. On RHEL/Linux there is no equivalent; this is Windows-only.

Step 1 — Detect the duplicates (read-only)

Import-Module ActiveDirectory

# The attribute that identifies a real person. EmployeeID is a good choice
# because it should be one-per-human. Change it to suit your directory.
$key = 'EmployeeID'

# Scope the search. Replace with your own OU distinguished name.
$searchBase = 'OU=Staff,DC=example,DC=com'

# Pull the accounts plus the attributes we want in the report.
$users = Get-ADUser -SearchBase $searchBase -Filter * `
    -Properties $key, DisplayName, Mail, Enabled, whenCreated, LastLogonDate

# Group by the key, ignore blanks, keep only keys used by more than one account.
$dupes = $users |
    Where-Object { $_.$key } |
    Group-Object -Property $key |
    Where-Object { $_.Count -gt 1 }

# Flatten to a readable report.
$report = foreach ($g in $dupes) {
    $g.Group | Select-Object `
        @{n='DupKey';e={$g.Name}}, SamAccountName, DisplayName, Mail,
        Enabled, whenCreated, LastLogonDate
}

$report | Sort-Object DupKey, whenCreated | Format-Table -AutoSize
$report | Export-Csv -Path 'C:\path\to\ad-duplicates.csv' -NoTypeInformation

Replace OU=Staff,DC=example,DC=com and C:\path\to\ad-duplicates.csv. The LastLogonDate and whenCreated columns are there to help you decide which account is the keeper — usually the one that's still logging in.

A note on LastLogonDate: it's derived from the lastLogonTimestamp attribute, which replicates lazily (up to ~14 days by default). It's fine for "is this account dormant?" but don't treat it as an exact last-logon time.

If you don't have a clean EmployeeID, grouping on DisplayName finds obvious name collisions, but expect false positives (two genuinely different "John Smith" accounts). Review the CSV by eye before acting on it — this is exactly the step you should not automate blindly.

Step 2 — Consolidate a single pair (gated, -WhatIf by default)

This function operates on one pair at a time, on purpose. It supports -WhatIf so you can see every change before it happens, and it defaults to disabling the duplicate rather than deleting it.

function Merge-DuplicateUser {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory)][string]$KeepIdentity,      # sAMAccountName to keep
        [Parameter(Mandatory)][string]$DuplicateIdentity, # sAMAccountName to retire
        [switch]$Delete                                    # delete instead of disable
    )

    $keep = Get-ADUser -Identity $KeepIdentity
    $dup  = Get-ADUser -Identity $DuplicateIdentity

    # Group memberships on the duplicate. Domain Users is a primary group and
    # can't be added with Add-ADGroupMember, so we skip it.
    $dupGroups = Get-ADPrincipalGroupMembership -Identity $dup |
        Where-Object { $_.Name -ne 'Domain Users' }

    foreach ($g in $dupGroups) {
        if ($PSCmdlet.ShouldProcess($keep.SamAccountName, "Add to group '$($g.Name)'")) {
            # SilentlyContinue skips groups the keeper is already in.
            Add-ADGroupMember -Identity $g -Members $keep -ErrorAction SilentlyContinue
        }
    }

    if ($Delete) {
        if ($PSCmdlet.ShouldProcess($dup.SamAccountName, 'DELETE account (irreversible)')) {
            Remove-ADUser -Identity $dup -Confirm:$false
        }
    }
    else {
        if ($PSCmdlet.ShouldProcess($dup.SamAccountName, 'Disable account')) {
            Disable-ADAccount -Identity $dup
        }
    }
}

Run it in dry-run mode first — nothing changes, you just see the plan:

Merge-DuplicateUser -KeepIdentity 'jsmith' -DuplicateIdentity 'jsmith2' -WhatIf

When the plan looks right, run it for real. Because ConfirmImpact is High, it will still prompt before each change:

Merge-DuplicateUser -KeepIdentity 'jsmith' -DuplicateIdentity 'jsmith2'

Only add -Delete once you're certain the duplicate owns no mailbox, no file ACLs, and no group ownership you care about, and you've confirmed the Recycle Bin is on:

Merge-DuplicateUser -KeepIdentity 'jsmith' -DuplicateIdentity 'jsmith2' -Delete

What this function deliberately does not do: it doesn't touch the primary group, doesn't migrate SID history (that's ADMT/specialist territory), and doesn't move Exchange or file resources. Those are per-object decisions, not something to bulk-script.

Step 3 — Verify

Confirm the surviving account picked up the memberships and the duplicate is retired:

# Keeper's group memberships after the merge:
Get-ADPrincipalGroupMembership -Identity 'jsmith' |
    Select-Object Name | Sort-Object Name

# State of the retired account:
Get-ADUser -Identity 'jsmith2' -Properties Enabled |
    Select-Object SamAccountName, Enabled

If you deleted it, confirm it's gone (and, if the Recycle Bin is on, that it's recoverable):

Get-ADObject -Filter { SamAccountName -eq 'jsmith2' } -IncludeDeletedObjects |
    Select-Object Name, Deleted, DistinguishedName

Undo

  • Re-enable a disabled account:

    Enable-ADAccount -Identity 'jsmith2'
    
  • Remove memberships you added by mistake with Remove-ADGroupMember (mirror of the add).

  • Restore a deleted account — only if the AD Recycle Bin is enabled:

    Get-ADObject -Filter { SamAccountName -eq 'jsmith2' } -IncludeDeletedObjects |
        Restore-ADObject
    

For exact parameters and edge cases, check Microsoft Learn for Get-ADUser, Get-ADPrincipalGroupMembership, Add-ADGroupMember, Disable-ADAccount, Remove-ADUser, and Restore-ADObject — the ActiveDirectory module reference pages are authoritative, and the "Enable Active Directory Recycle Bin" article covers turning that safety net on if you haven't already.

Do this one pair at a time, keep the CSV report as your record of what you changed, and never let the "merge" convenience talk you into deleting an account whose resources you haven't accounted for.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Clean Up Stale Computer Accounts in Active Directory with PowerShell

This guide finds Active Directory computer accounts that haven't logged in for a long time and retires them in three deliberate stages: report , disable and move to a holding OU , then delete . The purpose is to keep AD tidy and reduce the attack surface of forgotten machine accounts without accidentally killing a computer that's simply been powered off for a while.

8 min read

Automate Moving AD Users Between OUs From a CSV

This procedure reads a CSV of user accounts and their destination OUs, then moves each account to its target OU with PowerShell's Move-ADObject . Moving a user changes its distinguished name (DN). That matters because anything scoped by DN or OU — Group Policy links, delegated permissions, and OU-based filters — will start or stop applying to the account the moment it moves. The move itself does not delete the account or its group memberships, and it is reversible if you know where the account came from — which is why the script below records the original OU of every user before it touches anything.

8 min read

Bulk-Update AD User Attributes from a CSV

This guide reads a CSV of users and writes two attributes back to Active Directory for each one: Title and Department . Nothing is created or deleted — existing user objects have those two fields overwritten with the values in your file. That overwrite is a real change: if the CSV has a wrong value in a row, that user's title or department is now wrong until you fix it. There is no built-in "undo," so the safety step below is to export the current values first so you can put them back.

9 min read