Remove permissions for multiple offboarded users across the entire SharePoint and Microsoft 365 tenant using PowerShell

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


Hi, welcome to the forum :wave:

One thing I note, is that you have Connect-PnPOnline twice, but you never call Connect-MgGraph.

If you want further help, you’re going to have to provide some error information or go into more detail explaining what you mean by ‘not working’.

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.

What permissions does the App have?


This permissions i have been assigned to the app.