Home About Tools Projects Guides & Blog ⚡ Hire Me ✦ Websites Contact →
Home Projects Automated M365 Offboarding Script with PowerShell and Graph API
PowerShell · Graph API · Automation

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.

Status Complete
Language PowerShell 7
API Microsoft Graph v1.0
Auth Client credentials
Time to run ~45 seconds
Category Automation · Scripting
// overview

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.

ℹ️
What the script does, in order: (1) Block Entra ID sign-in, (2) Revoke all active refresh tokens, (3) Remove from all distribution groups, (4) Convert mailbox to shared, (5) Remove the M365 licence, (6) Set out-of-office reply, (7) Forward email to manager, (8) Grant manager Full Access to mailbox, (9) Retire the Intune-managed device, (10) Log all actions with timestamp.
// the script

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.

PowerShell - Invoke-M365Offboard.ps1
# ── 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
// deployment

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:

PowerShell
.\Invoke-M365Offboard.ps1 -LeaverUPN "jane.smith@company.com" -ManagerUPN "bob.jones@company.com"
The log file is your audit trail. Every action is timestamped and written to a dated log file. Keep these for 12 months minimum. If an ex-employee disputes access or data handling, the log shows exactly what was done and when.
// outcomes

What this replaces

~45s
Full offboard run time for a typical user
0
Steps missed compared to manual process
10+
Manual admin steps replaced by one command
Full audit log generated automatically