Back to Blog

Block Inactive Microsoft 365 Users with PowerShell and Microsoft Graph API

May 29, 2025|8 min read

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 and Microsoft.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:

  1. Save the script to your computer as Block-InactiveUsers.ps1
  2. Open PowerShell as an administrator
  3. Navigate to the directory where you saved the script
  4. 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:

  1. First, run Get-InactiveUsers.ps1 to identify inactive users and export them to a CSV file
  2. Review the CSV file to ensure you want to block all the listed users
  3. 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!