Offboarding is where ticket-driven shops leak access. Someone disables the AD account, forgets the refresh tokens are still valid for another hour, and the leaver keeps reading mail from a phone. This is the single pass I run — hybrid AD plus Entra plus Exchange — written to be idempotent so a half-finished run is safe to repeat.
# Disable-ADAccount needs RSAT-AD; Graph: User.ReadWrite.All, Group.ReadWrite.All
param([Parameter(Mandatory)][string]$Upn)
$log = { param($m) "{0} {1}" -f (Get-Date -f s), $m |
Tee-Object "./leaver-$($Upn.Split('@')[0]).log" -Append }
# 1. on-prem AD
$sam = (Get-ADUser -Filter "UserPrincipalName -eq '$Upn'").SamAccountName
if ($sam) { Disable-ADAccount $sam; & $log "AD disabled: $sam" }
# 2. Entra: block sign-in + invalidate refresh tokens (safe to re-run)
$u = Get-MgUser -UserId $Upn
Update-MgUser -UserId $u.Id -AccountEnabled:$false
Revoke-MgUserSignInSession -UserId $u.Id | Out-Null
& $log "Entra blocked + tokens revoked"
# 3. mailbox -> shared (no-op if already shared)
if ((Get-Mailbox $Upn).RecipientTypeDetails -ne 'SharedMailbox') {
Set-Mailbox $Upn -Type Shared; & $log "mailbox -> shared"
}
# 4. strip licenses + group memberships
$skus = (Get-MgUserLicenseDetail -UserId $u.Id).SkuId
if ($skus) { Set-MgUserLicense -UserId $u.Id -AddLicenses @() -RemoveLicenses $skus | Out-Null }
Get-MgUserMemberOf -UserId $u.Id | Where-Object { $_.AdditionalProperties['@odata.type'] -eq '#microsoft.graph.group' } |
ForEach-Object { Remove-MgGroupMemberByRef -GroupId $_.Id -DirectoryObjectId $u.Id -ErrorAction SilentlyContinue }
& $log "licenses + groups stripped"
The token revocation is the step everyone skips — disabling the account alone
leaves issued refresh tokens live until expiry. Run AD disable first so dirsync
doesn’t re-enable the cloud object mid-script, and note dynamic groups ignore
Remove-MgGroupMemberByRef; fix the membership rule instead.