Automated M365
Offboarding Script
A single PowerShell script that handles the full M365 offboarding process. Block sign-in, revoke active sessions, remove licences, convert and delegate the mailbox, set an out-of-office, forward email, and retire the Intune device. One command, fully documented.
Why this exists
Manual offboarding is one of the most error-prone processes in IT. There are typically 8 to 12 steps, and missing any of them creates a security or compliance gap. A forgotten active session means a leaver could still access company email. An unconverted mailbox on a full licence wastes money every month. Offboarding that runs the same way every time, documented and auditable, is much safer.
This script runs all the steps in the right order. It takes a UPN (email address) as input and handles everything else automatically. All actions are logged to a dated text file for audit purposes.
Full offboarding script
The script authenticates using an app registration (same client credentials pattern as the compliance reporting project), then runs each offboarding step in sequence. The UPN and manager UPN are passed as parameters.
# ── M365 Offboarding Script ───────────────────────────────────────────────
# Usage: .\Invoke-M365Offboard.ps1 -LeaverUPN "jane@company.com" -ManagerUPN "bob@company.com"
# Requires: ExchangeOnlineManagement module, Graph API app registration with:
# User.ReadWrite.All, Directory.ReadWrite.All, Mail.ReadWrite,
# DeviceManagementManagedDevices.ReadWrite.All, Group.ReadWrite.All
param(
[Parameter(Mandatory)][string]$LeaverUPN,
[Parameter(Mandatory)][string]$ManagerUPN,
[string]$LogPath = "C:\Offboarding\$(Get-Date -f 'yyyyMMdd')_$($LeaverUPN.Split('@')[0]).log"
)
# Auth via app registration (credentials in env vars)
$tenantId = $env:GRAPH_TENANT_ID
$clientId = $env:GRAPH_CLIENT_ID
$clientSecret = $env:GRAPH_CLIENT_SECRET
$token = (Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" -Body @{
grant_type = 'client_credentials'; client_id = $clientId
client_secret = $clientSecret; scope = 'https://graph.microsoft.com/.default'
}).access_token
$h = @{ Authorization = "Bearer $token"; 'Content-Type' = 'application/json' }
function Log($msg) {
$entry = "[$(Get-Date -f 'HH:mm:ss')] $msg"
Write-Host $entry
Add-Content -Path $LogPath -Value $entry
}
# Get user object ID
$user = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$LeaverUPN" -Headers $h
$userId = $user.id
Log "Starting offboard for $($user.displayName) ($LeaverUPN)"
# 1. Block sign-in
Invoke-RestMethod -Method Patch -Uri "https://graph.microsoft.com/v1.0/users/$userId" -Headers $h -Body '{"accountEnabled":false}'
Log "[1] Sign-in blocked"
# 2. Revoke refresh tokens (kills all active sessions immediately)
Invoke-RestMethod -Method Post -Uri "https://graph.microsoft.com/v1.0/users/$userId/revokeSignInSessions" -Headers $h
Log "[2] Active sessions revoked"
# 3. Remove from all distribution groups (Exchange Online)
Connect-ExchangeOnline -AppId $clientId -CertificateThumbprint $env:GRAPH_CERT_THUMB -Organization "yourdomain.com" -ShowBanner:$false
Get-DistributionGroup -ResultSize Unlimited | Where-Object {
(Get-DistributionGroupMember $_.Identity -ResultSize Unlimited | Where-Object { $_.PrimarySmtpAddress -eq $LeaverUPN })
} | ForEach-Object {
Remove-DistributionGroupMember -Identity $_.Identity -Member $LeaverUPN -Confirm:$false
Log "[3] Removed from group: $($_.DisplayName)"
}
# 4. Convert mailbox to shared
Set-Mailbox -Identity $LeaverUPN -Type Shared
Log "[4] Mailbox converted to shared"
# 5. Remove M365 licence via Graph
$licences = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$userId/licenseDetails" -Headers $h).value
$skuIds = $licences.skuId
$body = @{ addLicenses = @(); removeLicenses = $skuIds } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "https://graph.microsoft.com/v1.0/users/$userId/assignLicense" -Headers $h -Body $body
Log "[5] Licences removed"
# 6. Set out-of-office reply
$oofMessage = "$($user.displayName) is no longer with the organisation. Please contact $ManagerUPN for assistance."
Set-MailboxAutoReplyConfiguration -Identity $LeaverUPN -AutoReplyState Enabled -InternalMessage $oofMessage -ExternalMessage $oofMessage
Log "[6] Out-of-office set"
# 7. Forward email to manager
Set-Mailbox -Identity $LeaverUPN -ForwardingSmtpAddress $ManagerUPN -DeliverToMailboxAndForward $false
Log "[7] Email forwarding set to $ManagerUPN"
# 8. Grant manager Full Access to mailbox
Add-MailboxPermission -Identity $LeaverUPN -User $ManagerUPN -AccessRights FullAccess -AutoMapping $true -Confirm:$false
Log "[8] Manager granted Full Access to mailbox"
# 9. Retire Intune-managed device(s)
$devices = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$userId/managedDevices" -Headers $h).value
$devices | ForEach-Object {
Invoke-RestMethod -Method Post -Uri "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$($_.id)/retire" -Headers $h
Log "[9] Intune device retired: $($_.deviceName)"
}
Log "Offboarding complete. Log saved to $LogPath"
Disconnect-ExchangeOnline -Confirm:$false
How to run it
Store the script on the management machine, set the Graph API credentials as system environment variables (same approach as the compliance reporting project), and run it whenever someone leaves:
.\Invoke-M365Offboard.ps1 -LeaverUPN "jane.smith@company.com" -ManagerUPN "bob.jones@company.com"