PowerShell script to find Inactive M365 users and unused licenses requires the modern Microsoft Graph Framework. This includes the Microsoft Graph PowerShell SDK, Entra ID permissions (AuditLog.Read.All and User.Read.All), and a Microsoft Entra ID P1 or P2 license in the tenant.
Finding inactive or dormant Microsoft 365 user accounts is a critical step for every organization operating in the Microsoft 365 ecosystem. This is because inactive accounts (of former employees, temporary contractors, or abandoned shared mailboxes) continue to consume high-tier licenses (like M365 E3 or E5) while being an active security vulnerability.
So, if you’re an administrator looking for a reliable PowerShell script to get inactive Office 365, be it for performing a security audit or a routine license optimization, this is the “it” guide.
This comprehensive guide will share a modern PowerShell script to find inactive M365 users. It also includes the practices to optimize licensing and the crucial step most IT admins forget before hitting “Delete.”
Why You Need to Find Inactive M365 Users?
Tracking down inactive Office 365 users or orphaned accounts is executed to optimize the licenses and improve the overall organization’s security posture. It is an imperative operational step for IT administrators and security teams.
Leaving inactive user accounts unmanaged is a lose-lose situation. It will not only cost you a fortune but openly invite cybercriminals to penetrate your corporate network. Hence, having a reliable PowerShell script on your side is necessary to find inactive M365 users.
So, let’s check out the three big enterprise problems monitoring these stale accounts solves:
#1: Critical Security Vulnerabilities: When an inactive account is not monitored for a long time, it becomes highly vulnerable to cyberattacks. This is because when a bad actor compromises a dormant account, there is no actual user to report suspicious MFA prompts or strange inbox rules. Once penetrated, cybercriminals can:
- Move laterally across the SharePoint and OneDrive environments.
- Initiate internal phishing campaigns (that appear as trusted, internal email addresses).
- Access privileged corporate data without getting detected.
#2: Stop Financial Drain (License Optimization): Top-tier licenses like Microsoft 365 E3 or E5 cost tens of thousands of dollars annually for a mid-sized enterprise. And as long as it remains active, Microsoft will charge for it. Therefore, running a regular Microsoft 365 license optimization is required to ensure you stop paying the “ghost tax”.
#3: Compliance Maintenance and Directory Management: Messy directories can cause serious operational friction. Also, a cluttered Global Address List (GAL) with ex-employees can overwhelm current staff to find the right contacts and may lead to sharing sensitive emails to unmonitored inboxes.
Additionally, modern compliance frameworks (such as SOC 2, HIPAA, and GDPR) ask for strict access control policies. Hence, being unable to produce an Office 365 user activity report script or audit trail during a compliance review can lead to immediate audit failure. This results in severe regulatory fines.
Microsoft has officially deprecated the old modules that use legacy cmdlets (such as Get-AzureADUser or Get-AzureADAuditSignInLogs last logon time). Using deprecated scripts will trigger authentication errors or fail outright.
Key Prerequisites & Required Permissions
When running the PowerShell script to find inactive M365 users, your environment, account permissions, and tenant setup must meet specific requirements. So, here’s the breakdown of key prerequisites and permissions required to run an Entra ID inactive user detection script:
#1: PowerShell Environment & SDK Setup:
- Install the latest Microsoft Graph PowerShell SDK version: PowerShell 7.x (Core). It manages large JSON objects and page iterations from the Graph API significantly faster and with less memory overhead than PowerShell 5.1.
- To install PowerShell 7.x (Core), open Windows PowerShell [Win + X >> Terminal (Admin)] >> run the given command:
winget install --id Microsoft.PowerShell --source winget
- Restart PowerShell, type pwsh, and hit Enter.
- Ensure the execution policy allows scripts to run. Use the given command to set it for the current session:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
#2: Microsoft Graph API Permissions
- AuditLog.Read.All and User.Read.All (or Directory.Read.All) are the two specific API permission scopes to access the signInActivity property on user objects.
Quick Explanation:
- AuditLog.Read.all is required specifically to read the signInActivity object (lastSignInDateTime and lastNonInteractiveSignInDateTime).
- User.Read.All (or Directory.Read.All) is required to list user profiles, read accountEnabled status, and verify assignedLicenses.
#3: Directory Roles & Least Privilege Access:
- Maintain least-privilege administrative access (such as Reports Reader, Security Reader, or Global Reader) in the tenant (you do not need full Global Admin rights).
- To get least-privilege directory roles, grant or get initial tenant-wide consent for the Audit.Log.Read.All scope from the Global Administrator or Privileged Role Administrator.
#4: Required Tenant Licensing:
- The performing tenant must have an active Microsoft Entra ID P1 or P2 license. Included either in Microsoft 365 E3/E5, Enterprise Mobility + Security E3/E5, or Business Premium.
- Newly purchased or assigned an Entra ID P1/P2 license will take up to 24 to 72 hours for Microsoft Entra to process and backfill historical signInActivity throughout the existing user objects.
#5: Performance & Throttling
- Use the -Property parameter in PowerShell (or $select in direct REST calls) to request Microsoft Graph to return signInActivity.
- [For Massive Tenants] Ensure the PowerShell script involves Retry-After logic or error handling to effectively handle Graph API throttling.
How to Run the PowerShell Script to Find Inactive M365 Users?
Here are the simple steps to execute the PowerShell script to find inactive Microsoft 365 users and dormant accounts:
#1: Connect to Microsoft Graph (With Permissions)
Establishing a secure connection with Microsoft Graph with the necessary AuditLog and User permissions is the primary step. And, to read the user profiles and audit logs, the “PowerShell script to find inactive M365 users” requires the specific API permissions.
- Install two sub-modules of Microsoft Graph (Avoid installing the complete Microsoft. Graph module). To do so, run the following script:
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -AllowClobber Install-Module Microsoft.Graph.Users -Scope CurrentUser -AllowClobber
Optional: To verify installation of the modules and review the currently installed version, run this command:
Get-InstalledModule Microsoft.Graph*
- To grant the admin consent, execute the command:
Connect-MgGraph -Scopes "User.Read.All", "AuditLog.Read.All"
- This will trigger a browser window prompting for Microsoft 365 credentials. Sign in as the Global Reader or User Administrator role.
- Once connected, a “Permission Requested” dialog will pop up. Simply check the Consent on behalf of your organization box and click Accept. It will enable the necessary tenant-wide read access.
#2: Configure the PowerShell Script
Before running the PowerShell script to find inactive users and unused licenses, define the inactivity threshold according to your organization’s retention policies. To do so,
- Copy the given inactive user detection script and paste it into a code editor like Visual Studio Code (or the native PowerShell).
$InactiveDaysThreshold = 90 $CutoffDate = (Get-Date).AddDays(-$InactiveDaysThreshold) $ExportPath = "C:\Reports\M365_Inactive_Users_Report_$(Get-Date -Format 'yyyyMMdd').csv" Write-Host "Fetching user objects and signInActivity from Microsoft Entra ID..." -ForegroundColor Cyan
- Find the $InactiveDays = 90 variable in the following script and change 90 as per your organization’s specific offboarding policy (like 30 or 120).
#3: Retrieve the User with Explicit signInActivity Properties
Execute the following in a sequence to retrieve the user with explicit signInActivity properties:
$Properties = @('id', 'displayName', 'userPrincipalName', 'accountEnabled', 'assignedLicenses', 'signInActivity', 'userType') $AllUsers = Get-MgUser -All -Property $Properties | Select-Object $Properties $ReportResults = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($User in $AllUsers) {
# Extract sign-in timestamps
$LastInteractive = $User.SignInActivity.LastSignInDateTime $LastNonInteractive = $User.SignInActivity.LastNonInteractiveSignInDateTime
# Determine the most recent sign-in date between interactive and non-interactive:
$LatestSignIn = $null if ($LastInteractive -and $LastNonInteractive) { if ($LastInteractive -gt $LastNonInteractive) { $LatestSignIn = $LastInteractive } else { $LatestSignIn = $LastNonInteractive } } elseif ($LastInteractive) { $LatestSignIn = $LastInteractive } elseif ($LastNonInteractive) { $LatestSignIn = $LastNonInteractive }
# Calculate days inactive
if ($LatestSignIn) { $DaysInactive = (New-TimeSpan -Start $LatestSignIn -End (Get-Date)).Days $LastLoginDisplay = $LatestSignIn.ToString("yyyy-MM-dd HH:mm:ss") } else { $DaysInactive = "Never Logged In" $LastLoginDisplay = "Never" }
# Check license status
$HasLicense = if ($User.AssignedLicenses.Count -gt 0) { $true } else { $false }
# Filter based on 90-day inactivity threshold or accounts that never logged in
if ($DaysInactive -eq "Never Logged In" -or $DaysInactive -ge $InactiveDaysThreshold) { $ReportResults.Add([PSCustomObject]@{ "User Display Name" = $User.DisplayName "UserPrincipalName" = $User.UserPrincipalName "Account Enabled" = $User.AccountEnabled "User Type" = $User.UserType "Has Assigned License" = $HasLicense "Last Interactive Login" = if ($LastInteractive) { $LastInteractive.ToString("yyyy-MM-dd") } else { "Never" } "Last Non-Interactive" = if ($LastNonInteractive) { $LastNonInteractive.ToString("yyyy-MM-dd") } else { "Never" } "Latest Login Timestamp" = $LastLoginDisplay "Days Inactive" = $DaysInactive }) } }
#4: Export the Inactive M365 Users to CSV (PowerShell)
To export the results into a CSV report to seamlessly access and share it, run the given command:
$ReportResults | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding utf8 Write-Host "Report successfully generated! File saved to: $ExportPath" -ForegroundColor Green
- Optional: To configure a different save location, modify the export path in the final line (such as C:\Temp\InactiveM365Users.csv).
- Lastly, run the script (press F5 in most editors). It will take a few minutes (entirely depending on the size of the tenant).
Once the console displays the success message, review the generated CSV report to analyze the data and take the following actions:
- Go to the Output folder >> open the file in Microsoft Excel.
- Filter the IsLicensed column to True to surface the inactive or dormant accounts immediately.
- Lastly, verify the AccountEnabled Column.
Also Check Out: Allocate Microsoft 365 Licenses using 4 Techniques
Why Do You Need to Backup Inactive M365 Users Before Deletion?
Performing an inactive 365 user backup before immediately stripping the licenses or deleting the accounts protects your organization from several major risks. This includes:
- Compliance and Legal Blind Spots: Inactive user accounts mentioned in a lawsuit or HR dispute legally require their correspondence. A deleted mailbox will make the company out of compliance.
- Loss of IP (Intellectual Property): Blindly deleting the user account also deletes the company’s intellectual property.
- Lost OneDrive Data: Admins can retain emails by converting the user to a Shared Mailbox, but the user’s OneDrive data can not be preserved.
See, Microsoft 365 natively connects the Exchange mailboxes and OneDrive storage directly to the user account. And when you remove a license or delete an account, Microsoft initiates a 30-day countdown before it permanently deletes the user’s mailbox and OneDrive data from its servers.
In a nutshell, backing up the data of inactive users before deleting them is a must and shouldn’t be skipped to avoid catastrophic events.
So, how can I quickly secure my data before deletion? Let’s find out!
How to Secure Data with SysTools Before Deletion?
Safely offboarding an inactive user requires decoupling their data from their active Microsoft 365 license. To execute this securely, IT administrators and MSPs trust only SysTools 365 Backup & Restore software.
This corporate-grade software allows you to systematically extract and secure the data by automating the complete offboarding workflow. Here are some of the headline features that make it the most popular and first choice of IT administrators across the globe.
- Comprehensive Workload Coverage: It allows you to backup Office 365 email, contacts, calendars, and documents offline.
- Maintains Folder Hierarchy: This enterprise-ready backup tool preserves the exact folder structure. Simply making eDiscovery drastically easier.
- Local or Alternate Cloud Storage: It also enables you to export the inactive users’ mailboxes to a universally accessible PST file saved on the local servers.
- Batch Processing: You can run a batch backup process by importing a CSV of inactive users. You’re not required to back them up one by one.
- In-Place Archive Backup: It can backup In-Place Archive mailboxes too.
- Delta Backup: This backup tool provides the delta backup feature that allows you to store only newly added data in the mailbox.
- OS Compatibility: It is highly compatible and supports all the latest versions of Windows and Mac OS.
So, instead of relying on Microsoft’s temporary 30-day recycle bin, choose Systools advanced backup tool to simplify the offboarding process and secure data.
People Also Find Helpful: PowerShell Script to Get SharePoint Site Permissions (2026 Guide)
Automate the PowerShell Script to Find Inactive M365 Users
Do you know that you can automate and schedule the PowerShell script to find inactive Office 365 users to run monthly? Yes, you can schedule the script with Windows Task Scheduler.
It eliminates the manual labor of running the script and ensures no dormant account remains unmonitored. To access this, you are required to register an application in Microsoft Entra ID and use certificate-based authentication to connect silently.
Once the script is updated to use Connect-MgGraph -ClientId $AppId -TenantId $TenantId -CertificateThumbprint $Thumbprint, you can configure Windows Task Scheduler.
#1: Open Task Scheduler and Create a Task
- Press the Windows key >> type Task Scheduler and hit Enter.
- Click Create Task (right-hand Actions pane). Avoid choosing “Create Basic Task”; it limits security options.
- Name the task (such as Monthly M365 Inactive User Report).
#2: Setup Security Options
- On the General tab, right-click Run whether the user is logged on or not.
- Check the Run with highest privileges box.
#3: Configure the Monthly Trigger
- Go to the Trigger tab >> click New.
- Change the “Begin the Task” dropdown to “On a schedule”.
- Click Monthly >> select all months and pick a specific day (for instance: 1st day of the month
- Hit OK.
#4: Define the PowerShell Action
-
- Navigate to the Actions tab >> click New.
- Configure the “Action to Start a Program”.
- In the Program/script box, enter exactly: powershell.exe
- In the Add arguments (optional) box, provide the path to the script bypassing the execution policy:
-ExecutionPolicy Bypass -WindowStyle Hidden -File "C:\Scripts\InactiveM365Users.ps1"
#5: Save and Provide Credentials
- Hit OK to save the task.
- Enter the account credentials in the prompt. Make sure the account has the required local permissions to run scripts and write to the output folder (e.g., C:\Temp\).
Pro-Tip: Append the Send-MailMessage cmdlet (or the modern Graph equivalent, Send-MgUserMail) to the end of the PowerShell script. It prompts the script to automatically email the generated CSV report directly to the IT helpdesk or security team when the Task Scheduler process ends.
The Takeaway
Utilizing a PowerShell script to find inactive M365 users is the most accurate way to keep the Entra ID tenant optimized and secure. It identifies all unmonitored, dormant user accounts that are actively consuming high-end licenses while being vulnerable to credential stuffing and password spray attacks.
So, if you’re scheduling a security audit or tenant cleanup in your organization, use the precise PowerShell scripts outlined in this thorough article. And if you found yourself confused or in a dreadful situation, get in touch with us at [email protected] and [email protected].
Trending: How to Prepare for Microsoft Entra ID Passkey Default: A Step-by-Step Migration Guide
FAQs (Frequently Asked Questions)
Q.1: How do I find inactive users in Microsoft 365 using PowerShell?
To find inactive Microsoft 365 accounts in PowerShell, follow the given steps:
- Connect to Microsoft Graph using Connect-MgGraph -Scopes “AuditLog.Read.All”
- Run a query (targeting user objects) with the signInActivity property.
- Lastly, filter accounts where signInActivity.lastSignInDateTime is older than your set target duration or $null (accounts that have never logged in).
Q.2: How to get the last sign-in date for all users in Office 365 via MS Graph PowerShell?
Apply the following steps to get the last sign-in date for all users in Office 365 via MS Graph PowerShell:
- Run Get-MgUser -All -Property DisplayName, UserPrincipalName, SignInActivity
- Check SignInActivity.LastSignInDateTime property for interactive logins.
- Inspect SignInActivity.LastNonInteractiveSignInDateTime for background logins.
Q.3: How can I identify unused Microsoft 365 licenses to save costs?
To identify unused M365 licenses, follow the given instructions:
- Run a PowerShell script to cross-reference AssignedLicenses with SignInActivity.
- Look for the users with an active license SKU but have a DaysInactive value greater than 60 or 90 days.
Q.4: What PowerShell cmdlet replaces Get-AzureADUser for finding inactive accounts?
Get-MgUser (from Microsoft.Graph.Users) module replaces Get-AzureADUser. To restore logon metadata previously required for Azure AD audit logs, merge Get-MgUser with the -Property SignInActivity parameter.
Q.5: How far back can Microsoft 365 track user sign-in activity in PowerShell?
The Entra ID sign-in logs and Unified Audit logs preserve event records for 30 to 180 days. However, the signInActivity property is saved directly on the user object, allowing it to retain the last successful sign-in timestamp indefinitely.
Q.6: H What permissions are required to run active user reports in Entra ID?
Here are the permissions required to run an Entra ID inactive user detection script:
- AuditLog.Read.All and User.Read.All Microsoft Graph permissions
- Active Microsoft Entra ID P1 or P2 license (assigned to your tenant).