Public/offboard-user.ps1
|
# offboard-user.ps1 # Full M365 user offboarding. Performs all steps in sequence with confirmation # and logs every action to a timestamped CSV on the Desktop. # # Steps performed: # 1. Block sign-in # 2. Reset password to random string # 3. Revoke all active sessions # 4. Remove all group memberships # 5. Remove all admin roles # 6. Remove MFA methods # 7. Cancel future calendar events # 8. Convert mailbox to shared (preserves data, removes licence requirement) # 9. Set Out of Office reply # 10. Hide from Global Address List # 11. Remove licence assignments # 12. Export summary CSV # # Requires: Graph (User.ReadWrite.All, Directory.ReadWrite.All, # UserAuthenticationMethod.ReadWrite.All, RoleManagement.ReadWrite.Directory) # Exchange Online if (-not (Get-ConnectionInformation)) { Connect-ExchangeOnline } if (-not (Get-MgContext)) { Connect-MgGraph -Scopes "User.ReadWrite.All","Directory.ReadWrite.All","UserAuthenticationMethod.ReadWrite.All","RoleManagement.ReadWrite.Directory" -ContextScope Process } $upn = Read-Host "Enter UPN of user to offboard" # Verify user exists $user = Get-MgUser -Filter "userPrincipalName eq '$upn'" -Property "Id,DisplayName,UserPrincipalName,AssignedLicenses" if (-not $user) { Write-Host "User not found: $upn" -ForegroundColor Red exit } Write-Host "`nOffboarding: $($user.DisplayName) ($upn)" -ForegroundColor Yellow Write-Host "This will perform all offboarding steps. Continue? (y/n)" -ForegroundColor Yellow if ((Read-Host) -ne "y") { Write-Host "Aborted."; exit } $log = [System.Collections.Generic.List[PSCustomObject]]::new() function Log-Action { param($Step, $Status, $Notes = "") $entry = [PSCustomObject]@{ Timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss") UPN = $upn Step = $Step Status = $Status Notes = $Notes } $log.Add($entry) $colour = if ($Status -eq "OK") { "Green" } elseif ($Status -eq "SKIPPED") { "DarkGray" } else { "Red" } Write-Host " [$Status] $Step$(if ($Notes) { " — $Notes" })" -ForegroundColor $colour } Write-Host "" # 1. Block sign-in try { Update-MgUser -UserId $user.Id -AccountEnabled $false Log-Action "Block sign-in" "OK" } catch { Log-Action "Block sign-in" "FAILED" $_ } # 2. Reset password to random string try { $rnd = [System.Web.Security.Membership]::GeneratePassword(20, 4) Update-MgUser -UserId $user.Id -PasswordProfile @{ Password = $rnd; ForceChangePasswordNextSignIn = $false } Log-Action "Reset password" "OK" "New password logged in export" } catch { Log-Action "Reset password" "FAILED" $_ } # 3. Revoke sessions try { Revoke-MgUserSignInSession -UserId $user.Id | Out-Null Log-Action "Revoke active sessions" "OK" } catch { Log-Action "Revoke active sessions" "FAILED" $_ } # 4. Remove group memberships try { $groups = Get-MgUserMemberOf -UserId $user.Id | Where-Object { $_.OdataType -eq "#microsoft.graph.group" } $removed = 0 foreach ($group in $groups) { try { Remove-MgGroupMemberByRef -GroupId $group.Id -DirectoryObjectId $user.Id $removed++ } catch { } } Log-Action "Remove group memberships" "OK" "Removed from $removed group(s)" } catch { Log-Action "Remove group memberships" "FAILED" $_ } # 5. Remove admin roles try { $roles = Get-MgUserMemberOf -UserId $user.Id | Where-Object { $_.OdataType -eq "#microsoft.graph.directoryRole" } $removed = 0 foreach ($role in $roles) { try { Remove-MgDirectoryRoleMemberByRef -DirectoryRoleId $role.Id -DirectoryObjectId $user.Id $removed++ } catch { } } Log-Action "Remove admin roles" "OK" "Removed $removed role(s)" } catch { Log-Action "Remove admin roles" "FAILED" $_ } # 6. Remove MFA methods try { # Remove phone methods Get-MgUserAuthenticationPhoneMethod -UserId $user.Id | ForEach-Object { Remove-MgUserAuthenticationPhoneMethod -UserId $user.Id -PhoneAuthenticationMethodId $_.Id } # Remove TAPs Get-MgUserAuthenticationTemporaryAccessPassMethod -UserId $user.Id | ForEach-Object { Remove-MgUserAuthenticationTemporaryAccessPassMethod -UserId $user.Id -TemporaryAccessPassAuthenticationMethodId $_.Id } Log-Action "Remove MFA methods" "OK" } catch { Log-Action "Remove MFA methods" "FAILED" $_ } # 7. Cancel future calendar events try { Remove-CalendarEvents -Identity $upn -CancelOrganizedMeetings -QueryWindowInDays 120 -Confirm:$false Log-Action "Cancel future calendar events" "OK" "120 day window" } catch { Log-Action "Cancel future calendar events" "FAILED" $_ } # 8. Convert to shared mailbox try { Set-Mailbox -Identity $upn -Type Shared Log-Action "Convert to shared mailbox" "OK" "Mailbox preserved, licence can be removed" } catch { Log-Action "Convert to shared mailbox" "FAILED" $_ } # 9. Set Out of Office $oooMessage = Read-Host "`nOut of Office message (leave blank to skip)" if ($oooMessage) { try { Set-MailboxAutoReplyConfiguration -Identity $upn ` -AutoReplyState Enabled ` -InternalMessage $oooMessage ` -ExternalMessage $oooMessage Log-Action "Set Out of Office" "OK" } catch { Log-Action "Set Out of Office" "FAILED" $_ } } else { Log-Action "Set Out of Office" "SKIPPED" "No message provided" } # 10. Hide from GAL try { Set-Mailbox -Identity $upn -HiddenFromAddressListsEnabled $true Log-Action "Hide from address lists" "OK" } catch { Log-Action "Hide from address lists" "FAILED" $_ } # 11. Remove licences try { $licences = (Get-MgUser -UserId $user.Id -Property AssignedLicenses).AssignedLicenses if ($licences.Count -gt 0) { Set-MgUserLicense -UserId $user.Id -AddLicenses @() -RemoveLicenses ($licences.SkuId) Log-Action "Remove licences" "OK" "Removed $($licences.Count) licence(s)" } else { Log-Action "Remove licences" "SKIPPED" "No licences assigned" } } catch { Log-Action "Remove licences" "FAILED" $_ } # Export log $path = "$env:USERPROFILE\Desktop\Offboard_$($upn.Split('@')[0])_$(Get-Date -Format 'yyyyMMdd_HHmm').csv" $log | Export-Csv -Path $path -NoTypeInformation Write-Host "`nOffboarding complete. Log saved to: $path" -ForegroundColor Cyan Write-Host "Review the log for any FAILED steps that need manual follow-up." -ForegroundColor Yellow |