EASYENTRA Blog

News & Updates

How To Decommission Users in Microsoft 365

Microsoft 365 user decommissioning

When an employee leaves, several actions need to happen quickly and in the right order. Their account must be locked, active sessions revoked, licenses removed, and their mailbox secured or preserved based on company policy. Individually, these steps are simple. Together, they form a process that needs to be consistent every single time.

And if this isn’t done properly, it can quickly turn into a security or compliance risk. Let’s walk through a cleaner, more reliable way to handle this.

Where the Native Approach Falls Short

There’s no built-in Microsoft cmdlet that handles the full offboarding checklist. If you’ve ever tried, you know the drill: you’re reaching into at least two separate PowerShell modules (Microsoft.Graph and ExchangeOnlineManagement).

Because of this split, there isn’t a single command that handles everything. Admins usually end up chaining multiple cmdlets together, which works, but also means the process is something you have to build, test, and maintain yourself.

Decommission Microsoft 365 Users Using PowerShell

To make offboarding simpler for administrators, here is a PowerShell script that takes care of the key steps in decommissioning users.

# Decommission-M365User.ps1
# Offboard a departing user from Microsoft 365
# Requires: Microsoft.Graph + ExchangeOnlineManagement
param(
    [Parameter(Mandatory = $true)]
    [string]$UserUPN,
    [string]$LogPath = ".\DecommissionLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
)
function Write-Log {
    param([string]$Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $entry = "[$timestamp] $Message"
    Write-Host $entry
    Add-Content -Path $LogPath -Value $entry
}
Write-Log "=== Starting decommission for: $UserUPN ==="
# Connect
try {
    Connect-MgGraph -Scopes `
        "User.ReadWrite.All", `
        "Directory.ReadWrite.All", `
        "GroupMember.ReadWrite.All" `
        -ErrorAction Stop
    Write-Log "Connected to Microsoft Graph."
} catch {
    Write-Log "ERROR connecting to Microsoft Graph: $_"
    exit 1
}
try {
    Connect-ExchangeOnline -ErrorAction Stop
    Write-Log "Connected to Exchange Online."
} catch {
    Write-Log "ERROR connecting to Exchange Online: $_"
    exit 1
}
# Get User
try {
    $user = Get-MgUser -UserId $UserUPN -ErrorAction Stop
    Write-Log "User found: $($user.DisplayName) [$($user.Id)]"
} catch {
    Write-Log "ERROR: User '$UserUPN' not found. Aborting."
    exit 1
}
# 1. Disable Account
try {
    Update-MgUser -UserId $user.Id -AccountEnabled:$false
    Write-Log "Account disabled."
} catch {
    Write-Log "ERROR disabling account: $_"
}
# 2. Reset Password
try {
    Add-Type -AssemblyName System.Web
    $newPwd = [System.Web.Security.Membership]::GeneratePassword(20, 4)
    Update-MgUser -UserId $user.Id -PasswordProfile @{
        Password                      = $newPwd
        ForceChangePasswordNextSignIn = $false
    }
    Write-Log "Password reset to a random value."
} catch {
    Write-Log "ERROR resetting password: $_"
}
# 3. Revoke Active Sessions
try {
    Revoke-MgUserSignInSession -UserId $user.Id
    Write-Log "All sign-in sessions revoked."
} catch {
    Write-Log "ERROR revoking sessions: $_"
}
# 4. Remove Group Memberships
try {
    $groups = Get-MgUserMemberOf -UserId $user.Id -All
    $removed = 0
    $skipped = 0
    # Log all group memberships before removal
    if ($groups.Count -eq 0) {
        Write-Log "No group memberships found."
    } else {
        Write-Log "--- Group membership snapshot ($($groups.Count) groups) ---"
        foreach ($g in $groups) {
            $groupDetail = Get-MgGroup -GroupId $g.Id -ErrorAction SilentlyContinue
            $displayName = if ($groupDetail.DisplayName) { $groupDetail.DisplayName } else { "(unknown)" }
            $groupType   = if ($groupDetail.GroupTypes -contains "Unified") { "M365 Group" }
                           elseif ($groupDetail.MailEnabled -and -not $groupDetail.SecurityEnabled) { "Distribution List" }
                           elseif ($groupDetail.SecurityEnabled -and $groupDetail.MailEnabled) { "Mail-Enabled Security" }
                           elseif ($groupDetail.SecurityEnabled) { "Security Group" }
                           else { "Unknown" }
            Write-Log "  [MEMBER OF] $displayName | Type: $groupType | ID: $($g.Id)"
        }
        Write-Log "--- End of group snapshot ---"
    }
    foreach ($g in $groups) {
        try {
            Remove-MgGroupMemberByRef -GroupId $g.Id -DirectoryObjectId $user.Id -ErrorAction Stop
            $removed++
            Write-Log "  Removed from group: $($g.Id)"
        } catch {
            $skipped++
            Write-Log "  SKIPPED group $($g.Id): $($_.Exception.Message)"
        }
    }
    Write-Log "Group removal complete - removed: $removed, skipped: $skipped."
} catch {
    Write-Log "ERROR retrieving group memberships: $_"
}
# 5. Mailbox: Convert to Shared
try {
    Set-Mailbox -Identity $UserUPN -Type Shared -HiddenFromAddressListsEnabled $true
    Write-Log "Mailbox converted to Shared and hidden from GAL."
} catch {
    Write-Log "ERROR converting mailbox: $_"
}
# 6. Set Out-of-Office Reply
try {
    Set-MailboxAutoReplyConfiguration -Identity $UserUPN `
        -AutoReplyState Enabled `
        -InternalMessage "This user has left the organisation. Please contact your manager for assistance." `
        -ExternalMessage "This user has left the organisation. Please contact us at info@yourcompany.com."
    Write-Log "Out-of-office auto-reply configured."
} catch {
    Write-Log "ERROR setting auto-reply: $_"
}
# 7. Enable Litigation Hold
# Requires Exchange Online Plan 2 (E3, E5, or add-on). Will be skipped with a warning for F3 or lower licences.
try {
    $ErrorActionPreference = 'Stop'
    Set-Mailbox -Identity $UserUPN -LitigationHoldEnabled $true -ErrorAction Stop
    # Verify the hold was actually applied
    $holdCheck = Get-Mailbox -Identity $UserUPN | Select-Object -ExpandProperty LitigationHoldEnabled
    if ($holdCheck -eq $true) {
        Write-Log "Litigation hold enabled and confirmed."
    } else {
        Write-Log "WARNING: Set-Mailbox completed without error but LitigationHoldEnabled is still false. Manual verification required."
    }
} catch {
    $errMsg = $_.Exception.Message
    if ($errMsg -like "*license doesn't permit*" -or $errMsg -like "*LitigationHoldEnabled*") {
        Write-Log "WARNING: Litigation hold NOT enabled - the assigned licence does not include Exchange Online Plan 2. Upgrade to E3/E5 or add an Exchange Online Plan 2 licence before enabling hold."
    } else {
        Write-Log "ERROR enabling litigation hold: $errMsg"
    }
} finally {
    $ErrorActionPreference = 'Continue'
}
# 8. Remove Licences
try {
    $licenceDetails = Get-MgUserLicenseDetail -UserId $user.Id
    $skus = $licenceDetails.SkuId
    if (-not $skus) {
        Write-Log "No licences assigned - nothing to remove."
    } else {
        Write-Log "Found $($skus.Count) licence(s). Attempting direct removal..."
        try {
            Set-MgUserLicense -UserId $user.Id -AddLicenses @() -RemoveLicenses $skus -ErrorAction Stop
            Write-Log "Licences removed directly."
        } catch {
            if ($_.Exception.Message -like "*inherited from a group*" -or
                $_.FullyQualifiedErrorId -like "*Request_BadRequest*") {
                Write-Log "Licences are group-assigned. Locating licensing groups..."
                $allGroups = Get-MgUserMemberOf -UserId $user.Id -All
                $licensingGroups = @()

                foreach ($g in $allGroups) {
                    try {
                        $grpLicences = Get-MgGroupLicenseDetail -GroupId $g.Id -ErrorAction SilentlyContinue
                        if ($grpLicences) { $licensingGroups += $g }
                    } catch { }
                }
                if ($licensingGroups.Count -eq 0) {
                    Write-Log "  Could not identify licensing groups. Manual licence removal required."
                } else {
                    foreach ($lg in $licensingGroups) {
                        try {
                            Remove-MgGroupMemberByRef -GroupId $lg.Id -DirectoryObjectId $user.Id -ErrorAction Stop
                            Write-Log "  Removed from licensing group: $($lg.Id)"
                        } catch {
                            Write-Log "  Could not remove from licensing group $($lg.Id): $_"
                        }
                    }
                    Write-Log "Licence removal via group complete. Propagation may take a few minutes."
                }
            } else {
                Write-Log "ERROR removing licences (unexpected): $_"
            }
        }
    }
} catch {
    Write-Log "ERROR retrieving licence details: $_"
}
Write-Log "=== Decommission COMPLETE for: $UserUPN ==="
Write-Log "Log saved to: $LogPath"
Write-Host ""
Write-Host "Done. Full log written to: $LogPath" -ForegroundColor Green

The script does its job, but the challenges don’t show up immediately. They show up when the process repeats. You’re asked for logs, or you need to decommission multiple users at once, or something fails midway, and no one notices. Over time, small gaps like these starting adding operational overhead.

What began as a quick solution slowly becomes something you have to monitor, adjust, and explain.

Easy Entra Cuts Through the Administrative Overhead

At some point, the question shifts from “How do I do this?” To “How do I make sure this runs the same way every time?” That’s where tools like EasyEntra change the approach.

Decommissioning a Hybrid User Using PowerShell

Instead of writing and maintaining a long PowerShell script for every offboarding step, EasyEntra lets you define the entire decommission process once using its GUI. That includes actions like disabling the account, removing licenses, handling the mailbox, and any other steps you configure.

The Invoke-EEDecommissionHybridUser cmdlet simply runs those saved steps in a single command, so every offboarding follows the same process without needing to chain multiple cmdlets.

The cmdlet also returns a structured result object, which includes the overall status and a detailed task log. This means you automatically get visibility into what was executed, without having to build your own logging. You can capture and save this output easily for auditing purposes by exporting it to a file.

Since it’s a standard PowerShell command, it can be used in bulk operations or scheduled tasks. For example, you can loop through a CSV file of users and run the cmdlet for each one, ensuring consistent and repeatable decommissioning across multiple accounts.

Decommissioning Users via EasyEntra GUI

For a GUI-based approach, EasyEntra also provides a simple way to decommission users directly within the application. As outlined in the EasyEntra Knowledge Base, this can be done by selecting the user, choosing the decommission option, and then stepping through account and mailbox actions. You can review and adjust the configuration before executing the process, and those selections are saved for future use. This makes it easier to apply the same offboarding logic consistently, whether you are handling a single user or repeating the process over time.

Free 30-minute demo

try 30 days for free

GET EASYENTRA NEWS

Opt out at any time

“One of the best products I've used.”
Gary Shurland
Chief Information Officer, Mirick, United States
“This tool has been invaluable in streamlining our IT processes.”
Tyson Mckay
Chief Information Officer, Southwest Network, United States
“This product has been a miracle for our Help Desk. EasyEntra has completely transformed how we handle Microsoft 365 administration.”
Doug Sanders
Manager of Technical Customer Support, Junior Achievement USA, United States
“Your product is such a time saver. I love it!”
Scott Fehr
IT Infrastructure, MEC Aerial Work Platforms, United States
“It's a good product and saves us lots of time for these ongoing quick admin tasks.” 
Chris McFerran
Managing Director, CTech IT Solutions Ltd, United Kingdom
“EasyEntra has significantly streamlined our workflow, simplifying everything. It feels almost like a revolution.”
Johan Sadelius
IT-chef, Arjeplog Kommun, Sweden
I greatly appreciate your assistance and willingness to enhance the already outstanding product.”
Michael I. Wilson
Executive Director of Information Technology, Archdiocese Of Washington, United States
“It's great not having to switch back and forth between the O365 admin center and the Teams admin center to assign groups. I am sold!”
Thomas Madden
Director Information Technology, AutoPayPlus, United States
“I would highly recommend organizations use the solution as it greatly simplifies various tasks.”
S. Roger Singh
Chief Technology Officer, Prasad & Company LLP, Canada
“EasyEntra is time-saving. Love the copy/paste for user/computer groups and the copy to new user.”
Damian Nita
Associate Network Administrator, Shenandoah Valley Westminster-Canterbury, United States
“EasyEntra has transformed our daily IT operations by simplifying user management, reducing errors, and enhancing overall efficiency.”
Henrik Nefling
IT- and Digitalization Manager, Animal Protection Denmark, Denmark