Find inactive Microsoft 365 users and reclaim their licences
Every tenant collects accounts nobody uses: leavers who were never fully offboarded, test accounts, shared mailboxes someone licensed by mistake. Each one can be holding a paid licence. This guide shows how to find them with Microsoft Graph PowerShell in a few minutes.
What you need
- PowerShell 7 (Windows PowerShell 5.1 also works)
- The Microsoft Graph PowerShell SDK
- Microsoft Entra ID P1 or P2 in the tenant. The
signInActivityproperty isn't returned without it. - An account that can consent to
User.Read.AllandAuditLog.Read.All
Install-Module Microsoft.Graph -Scope CurrentUser
Step 1: Connect to Microsoft Graph
Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All","Organization.Read.All"
The first time, you'll be asked to consent to these permissions. They're all read-only.
Step 2: Pull users with their sign-in activity
signInActivity isn't returned by default, so ask for it explicitly with -Property:
$cutoff = (Get-Date).ToUniversalTime().AddDays(-90)
$users = Get-MgUser -All -Property DisplayName,UserPrincipalName,AccountEnabled,
CreatedDateTime,SignInActivity,AssignedLicenses
Step 3: Filter to the inactive ones
Check both interactive and non-interactive sign-ins. A user who only syncs mail on their phone shows up as non-interactive, and you don't want to flag them.
$inactive = $users | Where-Object {
$_.AccountEnabled -and
$_.CreatedDateTime -lt $cutoff -and
(
(-not $_.SignInActivity.LastSignInDateTime -or
$_.SignInActivity.LastSignInDateTime -lt $cutoff) -and
(-not $_.SignInActivity.LastNonInteractiveSignInDateTime -or
$_.SignInActivity.LastNonInteractiveSignInDateTime -lt $cutoff)
)
}
The CreatedDateTime check skips brand-new accounts that simply haven't signed in yet.
Step 4: Show licence names and export
Licences come back as SKU IDs. Map them to readable names with Get-MgSubscribedSku:
$sku = @{}
Get-MgSubscribedSku -All | ForEach-Object { $sku[$_.SkuId] = $_.SkuPartNumber }
$inactive | Select-Object DisplayName, UserPrincipalName,
@{n='LastSignIn'; e={ $_.SignInActivity.LastSignInDateTime }},
@{n='Licences'; e={ ($_.AssignedLicenses | ForEach-Object { $sku[$_.SkuId] }) -join '; ' }} |
Export-Csv .\InactiveUsers.csv -NoTypeInformation
Before you remove anything: send the list to the user's manager or HR first. Some accounts are inactive on purpose: people on long leave, service accounts and break-glass admin accounts.
Step 5: Decide what to do with each account
- Leavers: block sign-in, convert the mailbox to a shared mailbox if it's needed, then remove the licence.
- Test accounts: delete them, or remove their licences.
- Long leave: leave them alone and note the return date.
Skip the copy-paste
The free M365 Inactive User Report does all of this in one command and prints a licence total.