Block Inactive Microsoft 365 Users with PowerShell and Microsoft Graph API
After identifying inactive Microsoft 365 users, the next step is often to disable or block these accounts to enhance security and optimize license usage. This post provides a PowerShell script that can block multiple inactive user accounts using the Microsoft Graph API.
⚠️ Important Warning ⚠️
This script will disable user accounts in your Microsoft 365 tenant. Always review the code carefully and test in a non-production environment first. Consider creating a backup or snapshot before running this in production.
Prerequisites
- PowerShell 5.1 or higher
- Microsoft Graph PowerShell modules (
Microsoft.Graph.Authentication
andMicrosoft.Graph.Users
) - Microsoft 365 account with appropriate permissions (User.ReadWrite.All)
- CSV file with inactive users (optional - can be generated using our Get-InactiveUsers script)
The Script
Here's the complete PowerShell script for blocking inactive Microsoft 365 users:
<#
.SYNOPSIS
Blocks Microsoft 365 users from a CSV file or a direct list of user principal names.
.DESCRIPTION
This script blocks (disables) Microsoft 365 user accounts either from a CSV file
(such as one generated by Get-InactiveUsers.ps1) or from a direct list of user
principal names provided as a parameter. It requires the Microsoft Graph PowerShell
module and appropriate permissions to modify user accounts.
.PARAMETER CsvPath
Path to the CSV file containing the users to block. The CSV must have a 'UserPrincipalName' column.
Cannot be used with -UserPrincipalNames.
.PARAMETER UserPrincipalNames
An array of user principal names (email addresses) to block.
Cannot be used with -CsvPath.
.PARAMETER LogPath
Path where the log file will be saved. Default is desktop.
.PARAMETER WhatIf
If specified, shows what would happen if the script runs without making any changes.
.EXAMPLE
.Block-InactiveUsers.ps1 -CsvPath "C:path oInactiveUsers.csv"
Blocks all users listed in the specified CSV file.
.EXAMPLE
.Block-InactiveUsers.ps1 -UserPrincipalNames "user1@domain.com", "user2@domain.com"
Blocks the specified users by their email addresses.
.EXAMPLE
.Block-InactiveUsers.ps1 -CsvPath "C:path oInactiveUsers.csv" -WhatIf
Shows what users would be blocked without making any changes.
#>
[CmdletBinding(SupportsShouldProcess=$true, DefaultParameterSetName="CsvFile")]
param (
[Parameter(Mandatory=$true, ParameterSetName="CsvFile", Position=0)]
[string]$CsvPath,
[Parameter(Mandatory=$true, ParameterSetName="UserList")]
[string[]]$UserPrincipalNames,
[Parameter(Mandatory=$false)]
[string]$LogPath = [Environment]::GetFolderPath("Desktop")
)
# Function to write log entries
function Write-Log {
param (
[Parameter(Mandatory=$true)]
[string]$Message,
[Parameter(Mandatory=$false)]
[ValidateSet("Info", "Warning", "Error", "Success")]
[string]$Level = "Info"
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$Level] $Message"
# Define colors for console output
switch ($Level) {
"Info" { $color = "White" }
"Warning" { $color = "Yellow" }
"Error" { $color = "Red" }
"Success" { $color = "Green" }
default { $color = "White" }
}
# Output to console
Write-Host $logEntry -ForegroundColor $color
# Append to log file
$logEntry | Out-File -FilePath $script:LogFile -Append
}
# Main script execution
Clear-Host
Write-Host "=== Block Inactive Users Script ===" -ForegroundColor Magenta
# Setup logging
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$script:LogFile = Join-Path -Path $LogPath -ChildPath "BlockInactiveUsers_$timestamp.log"
Write-Log "Script started" -Level "Info"
# Check for required modules
$requiredModules = @("Microsoft.Graph.Authentication", "Microsoft.Graph.Users")
foreach ($module in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $module)) {
Write-Log "Required module $module is not installed. Please install it using: Install-Module $module -Scope CurrentUser" -Level "Error"
exit 1
}
}
# Process users based on input method (CSV or direct list)
$usersToProcess = @()
if ($CsvPath) {
# Verify CSV file exists
if (-not (Test-Path -Path $CsvPath)) {
Write-Log "CSV file not found: $CsvPath" -Level "Error"
exit 1
}
# Import CSV file
try {
$importedUsers = Import-Csv -Path $CsvPath
$userCount = $importedUsers.Count
Write-Log "Imported $userCount users from CSV file" -Level "Info"
# Verify CSV has required columns
if (-not ($importedUsers | Get-Member -Name "UserPrincipalName" -MemberType NoteProperty)) {
Write-Log "CSV file does not contain required column: UserPrincipalName" -Level "Error"
exit 1
}
# Add imported users to processing list
foreach ($user in $importedUsers) {
$usersToProcess += [PSCustomObject]@{
UserPrincipalName = $user.UserPrincipalName
DisplayName = if ($user.DisplayName) { $user.DisplayName } else { $user.UserPrincipalName }
}
}
} catch {
Write-Log "Error importing CSV file: $($_.Exception.Message)" -Level "Error"
exit 1
}
} else {
# Process direct user list
foreach ($upn in $UserPrincipalNames) {
$usersToProcess += [PSCustomObject]@{
UserPrincipalName = $upn
DisplayName = $upn # We'll update this when we get the user details
}
}
Write-Log "Processing $($usersToProcess.Count) users from direct input" -Level "Info"
}
# Connect to Microsoft Graph
try {
Write-Log "Connecting to Microsoft Graph..." -Level "Info"
Connect-MgGraph -Scopes "User.ReadWrite.All" -ErrorAction Stop
Write-Log "Connected to Microsoft Graph successfully" -Level "Success"
} catch {
Write-Log "Failed to connect to Microsoft Graph: $($_.Exception.Message)" -Level "Error"
exit 1
}
# Initialize counters
$successCount = 0
$failureCount = 0
$alreadyBlockedCount = 0
# Process each user
$i = 0
$userCount = $usersToProcess.Count
foreach ($user in $usersToProcess) {
$i++
$upn = $user.UserPrincipalName
$displayName = $user.DisplayName
Write-Progress -Activity "Blocking inactive users" -Status "Processing $i of $userCount - $upn" -PercentComplete (($i / $userCount) * 100)
try {
# Get current user status
$mgUser = Get-MgUser -UserId $upn -Property "Id,DisplayName,UserPrincipalName,AccountEnabled" -ErrorAction Stop
if ($mgUser.AccountEnabled -eq $false) {
Write-Log "User $displayName ($upn) is already blocked" -Level "Warning"
$alreadyBlockedCount++
continue
}
# Block the user
if ($PSCmdlet.ShouldProcess($upn, "Block user account")) {
Update-MgUser -UserId $mgUser.Id -AccountEnabled:$false -ErrorAction Stop
Write-Log "Successfully blocked user $displayName ($upn)" -Level "Success"
$successCount++
} else {
Write-Log "Would block user $displayName ($upn)" -Level "Info"
}
} catch {
Write-Log "Failed to block user $displayName ($upn): $($_.Exception.Message)" -Level "Error"
$failureCount++
}
}
Write-Progress -Activity "Blocking inactive users" -Completed
# Display summary
Write-Host ""
Write-Host "=== Summary ===" -ForegroundColor Cyan
Write-Log "Total users processed: $userCount" -Level "Info"
Write-Log "Successfully blocked: $successCount" -Level "Success"
Write-Log "Already blocked: $alreadyBlockedCount" -Level "Warning"
Write-Log "Failed to block: $failureCount" -Level "Error"
Write-Log "Log file saved to: $script:LogFile" -Level "Info"
# Disconnect from Microsoft Graph
try {
Disconnect-MgGraph -ErrorAction SilentlyContinue
Write-Log "Disconnected from Microsoft Graph" -Level "Info"
} catch {
# Ignore any disconnection errors
}
Write-Host ""
Write-Host "Script completed. See log file for details: $script:LogFile" -ForegroundColor Green
How to Use the Script
Follow these steps to use the script:
- Save the script to your computer as
Block-InactiveUsers.ps1
- Open PowerShell as an administrator
- Navigate to the directory where you saved the script
- Run the script using one of the following commands:
# Block users from a CSV file .\Block-InactiveUsers.ps1 -CsvPath "C:\path\to\InactiveUsers.csv" # Block specific users by their email addresses .\Block-InactiveUsers.ps1 -UserPrincipalNames "user1@domain.com", "user2@domain.com" # Preview changes without making them (WhatIf mode) .\Block-InactiveUsers.ps1 -CsvPath "C:\path\to\InactiveUsers.csv" -WhatIf
Key Features
- Blocks (disables) Microsoft 365 user accounts using Microsoft Graph API
- Supports two input methods: CSV file or direct list of user principal names
- Creates a detailed log file for auditing and troubleshooting
- Includes WhatIf support to preview changes without making them
- Skips users that are already blocked
- Provides a summary of actions taken
Understanding the Results
When the script completes, it will display a summary of the actions taken:
- Total users processed: The total number of users in the input
- Successfully blocked: The number of users that were successfully blocked
- Already blocked: The number of users that were already blocked
- Failed to block: The number of users that could not be blocked (with errors logged)
A detailed log file is also created on your desktop with timestamps and color-coded messages for easy troubleshooting.
Combining with Get-InactiveUsers
This script pairs perfectly with our Get-InactiveUsers script to create a complete workflow:
- First, run
Get-InactiveUsers.ps1
to identify inactive users and export them to a CSV file - Review the CSV file to ensure you want to block all the listed users
- Run
Block-InactiveUsers.ps1
with the CSV file to block the inactive users
Pro Tip
Always run the script with the -WhatIf
parameter first to see what changes would be made without actually making them. This is especially important in production environments.
Security Considerations
When running this script, keep the following security considerations in mind:
- The script requires the
User.ReadWrite.All
permission, which is a highly privileged scope - Always review the list of users before blocking them to avoid disrupting critical accounts
- Consider creating a backup or snapshot before running this in production
- The log file contains user information, so ensure it's stored securely
Conclusion
Blocking inactive users is an important part of maintaining security and optimizing license usage in Microsoft 365. This script provides a streamlined way to block multiple inactive users at once, with detailed logging and error handling.
By combining this script with our Get-InactiveUsers script, you can create a complete workflow for identifying and managing inactive users in your Microsoft 365 tenant.
If you have any questions or suggestions for improving this script, feel free to reach out!