My tenant contains around 500 SharePoint sites and many internal and external users.
When users are offboarded, their email addresses are stored in a CSV file. I want to create a PowerShell script that reads this CSV and then checks the entire tenant—including all SharePoint sites, lists, libraries, folders, files, and item-level permissions—and removes those users wherever they have access.
In addition, if an offboarded user is a member of any group (SharePoint groups, Microsoft 365 groups, security groups, or any other group in the tenant), the script should also remove the user from those groups.
The goal is to completely remove all SharePoint and Microsoft 365 access for offboarded users across the whole tenant using a single PowerShell script. I have started creating the script, but it is not working as expected. Can someone help me correct the script or provide a working example to fully remove permissions for offboarded users tenant-wide?
Below is the PowerShell script I have created for tenant-wide user offboarding, but it is not working correctly. I would appreciate help identifying what is wrong and how to fix it.
$TenantId = "<Tenant ID>"
$ClientId = "<Client ID>"
$ClientSecret =
"<Secret ID>"
$TenantName = "<TenantName>"
$CsvPath =
"C:\Scripts\OffboardedUsers.csv"
$LogFile =
"C:\Scripts\OffboardingLog_$(Get-Date -Format yyyyMMdd).txt"
Start-Transcript
-Path $LogFile
Write-Host
"Starting Offboarding Process..." -ForegroundColor Green
$SecureSecret =
ConvertTo-SecureString $ClientSecret -AsPlainText -Force
$Credential = New-Object
System.Management.Automation.PSCredential ($ClientId, $SecureSecret)
Connect-PnPOnline
-Url $AdminUrl -ClientId $ClientId -TenantId $TenantId -ClientSecret
$ClientSecret
$AdminUrl = "https://$TenantName-admin.sharepoint.com"
Connect-PnPOnline
-Url $AdminUrl -ClientId $ClientId -Tenant $TenantId -ClientSecret
$ClientSecret
Write-Host
"Fetching all SharePoint sites..." -ForegroundColor Cyan
$Sites =
Get-PnPTenantSite -IncludeOneDriveSites $false
Write-Host
"Total sites found: $($Sites.Count)" -ForegroundColor Cyan
$Users = Import-Csv
$CsvPath
foreach ($User in
$Users) {
$UPN = $User.UserPrincipalName
Write-Host "`nProcessing user:
$UPN" -ForegroundColor Cyan
try {
$GraphUser = Get-MgUser -UserId $UPN
-ErrorAction Stop
}
catch {
Write-Warning "User $UPN not found
in Entra ID"
continue
}
try {
$Groups = Get-MgUserMemberOf -UserId
$GraphUser.Id -All
}
catch {
Write-Warning "Failed to fetch
groups for $UPN"
$Groups = @()
}
foreach ($Group in $Groups) {
if ($Group.'@odata.type' -eq
"#microsoft.graph.group") {
try {
Remove-MgGroupMemberByRef
-GroupId $Group.Id -DirectoryObjectId $GraphUser.Id -ErrorAction Stop
Write-Host "Removed from
group: $($Group.DisplayName)" -ForegroundColor Yellow
}
catch {
Write-Warning "Failed to
remove $UPN from group: $($Group.DisplayName)"
}
}
}
foreach ($Site in $Sites) {
try {
# Connect to each site individually
Connect-PnPOnline -Url $Site.Url
-ClientId $ClientId -Tenant $TenantId -ClientSecret $ClientSecret
# Attempt to remove user
Remove-PnPUser -LoginName $UPN
-Force -ErrorAction SilentlyContinue
Write-Host "Removed $UPN from
site: $($Site.Url)" -ForegroundColor Yellow
}
catch {
Write-Warning "Failed to
remove $UPN from site: $($Site.Url)"
}
}
Write-Host "Completed cleanup for
$UPN" -ForegroundColor Green
}
Disconnect-MgGraph
Disconnect-PnPOnline
Stop-Transcript
Write-Host
"`nOffboarding process completed." -ForegroundColor Green
PS C:\Windows\system32> $TenantId = "<Tenant ID>"
>> $TenantName = "<Name>"
>> $ClientId = "<Client id>"
>> $ClientSecret = "<Client Secret id>"
>> $AdminUrl = "https://<Name>-admin.sharepoint.com"
>>
>> $CsvPath = "C:\Scripts\OffboardedUsers.csv"
>> $LogFile = "C:\Scripts\OffboardingLog_$(Get-Date -Format yyyyMMdd).txt"
>>
>> Start-Transcript -Path $LogFile
>>
>>
>> $SecureSecret = ConvertTo-SecureString $ClientSecret -AsPlainText -Force
>> $ClientCredential = New-Object System.Management.Automation.PSCredential($ClientId, $SecureSecret)
>>
>> Connect-MgGraph `
>> -TenantId $TenantId `
>> -ClientSecretCredential $ClientCredential
>>
>> Write-Host "Connected to Microsoft Graph" -ForegroundColor Green
>>
>>
>> # ================================
>> # CONNECT TO SHAREPOINT ADMIN
>> # ================================
>> Connect-PnPOnline `
>> -Url $AdminUrl `
>> -ClientId $ClientId `
>> -ClientSecret $ClientSecret
>>
>> Write-Host "Connected to SharePoint Admin" -ForegroundColor Green
>> # ================================
>>
>> $Users = Import-Csv $CsvPath
>> $Sites = Get-PnPTenantSite
>>
>> Write-Host "Total Sites Found: $($Sites.Count)" -ForegroundColor Cyan
>>
>> # ================================
>> # PROCESS EACH USER
>> # ================================
>> foreach ($User in $Users) {
>>
>> $UPN = $User.UserPrincipalName
>> Write-Host "`nProcessing $UPN" -ForegroundColor Cyan
>>
>> try {
>> $GraphUser = Get-MgUser -UserId $UPN -ErrorAction Stop
>> }
>> catch {
>> Write-Warning "User not found: $UPN"
>> continue
>> }
>>
>> # ----------------------------
>> # REMOVE FROM ALL GROUPS
>> # ----------------------------
>> $Groups = Get-MgUserMemberOf -UserId $GraphUser.Id -All
>>
>> foreach ($Group in $Groups) {
>> if ($Group.'@odata.type' -eq "#microsoft.graph.group") {
>> try {
>> Remove-MgGroupMemberByRef `
>> -GroupId $Group.Id `
>> -DirectoryObjectId $GraphUser.Id
>> Write-Host "Removed from group: $($Group.Id)" -ForegroundColor Yellow
>> }
>> catch {
>> Write-Warning "Failed group removal"
>> }
>> }
>> }
>>
>> # ----------------------------
>> # REMOVE SHAREPOINT SITE ACCESS
>> # ----------------------------
>> foreach ($Site in $Sites) {
>> try {
>> Connect-PnPOnline `
>> -Url $Site.Url `
>> -ClientId $ClientId `
>> -Tenant $TenantName `
>> -ClientSecret $ClientSecret
>>
>> Remove-PnPUser `
>> -LoginName $UPN `
>> -Force `
>> -ErrorAction SilentlyContinue
>>
>> Write-Host "Removed from site: $($Site.Url)" -ForegroundColor Green
>> }
>> catch {
>> Write-Warning "Site access removal failed: $($Site.Url)"
>> }
>> }
>> }
>>
>> # ================================
>> # CLEANUP
>> # ================================
>> Disconnect-MgGraph
>> Disconnect-PnPOnline
>> Stop-Transcript
>>
>> Write-Host "`nOffboarding completed successfully" -ForegroundColor Green
>>
Transcript started, output file is C:\Scripts\OffboardingLog_20260128.txt
Welcome to Microsoft Graph!
Connected via apponly access using e29a565d-854b-467f-aa06-f33183c2b4d9
Readme: https://aka.ms/graph/sdk/powershell
SDK Docs: https://aka.ms/graph/sdk/powershell/docs
API Docs: https://aka.ms/graph/docs
NOTE: You can use the -NoWelcome parameter to suppress this message.
NOTE: Sign in by Web Account Manager (WAM) is enabled by default on Windows systems and cannot be disabled. Any setting stating otherwise will be ignored.
Connected to Microsoft Graph
WARNING:
Connecting with Client Secret uses legacy authentication and provides limited functionality. We can for instance
not execute requests towards the Microsoft Graph, which limits cmdlets related to Microsoft Teams, Microsoft
Planner, Microsoft Flow and Microsoft 365 Groups. You can hide this warning by using Connect-PnPOnline [your
parameters] -WarningAction Ignore
Connected to SharePoint Admin
Get-PnPTenantSite : Exception has been thrown by the target of an invocation.
At line:35 char:10
+ $Sites = Get-PnPTenantSite
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [Get-PnPTenantSite], PSInvalidOperationException
+ FullyQualifiedErrorId : InvalidOperation,PnP.PowerShell.Commands.GetTenantSite
Total Sites Found: 0
Processing dipenshah@1qwy0b.onmicrosoft.com
Processing Test@1qwy0b.onmicrosoft.com
ClientId : e29a565d-854b-467f-aa06-f33183c2b4d9
TenantId : e2523fae-05b9-48e4-a0ed-123b4b9a97ae
Scopes : {User.ReadWrite.All, Group.Read.All, Directory.ReadWrite.All, Sites.Read.All...}
AuthType : AppOnly
TokenCredentialType : ClientSecret
CertificateThumbprint :
CertificateSubjectName :
SendCertificateChain : False
Account :
AppName : SharePoint Access Remover
ContextScope : Process
Certificate :
PSHostVersion : 5.1.26100.7462
ManagedIdentityId :
ClientSecret : System.Security.SecureString
Environment : Global
Transcript stopped, output file is C:\Scripts\OffboardingLog_20260128.txt
Offboarding completed successfully
The script shows “Total sites found: 0”, but in reality there are many SharePoint sites in the tenant. The user has access to several of these sites, including lists, libraries, and Power Automate flows, yet the script is not detecting any sites.