Microsoft 365 Licenses: 5 Warning Signs on Disabled Accounts

By Ahmet Tolga KAYA 6 min read
Microsoft 365 Licenses: 5 Warning Signs on Disabled Accounts

Disabling an account in Microsoft Entra ID stops sign-in immediately. It does nothing to the Microsoft 365 licenses already assigned to that account. An offboarded employee, a contractor whose access was pulled, or an account disabled mid-investigation can sit there indefinitely, still holding an E3 or E5 seat that’s counted against your subscription and unavailable to anyone else.

Nobody catches this by browsing the admin center — checking every disabled account by hand doesn’t scale past a handful of users. A short Microsoft Graph PowerShell script cross-referencing account status against license assignment finds every case in one pass, tenant-wide.

What This Script Actually Checks

The script connects to Microsoft Graph, pulls the tenant’s subscribed SKUs so IDs can be resolved to names, retrieves every account with AccountEnabled set to false, and filters that list down to the accounts still holding at least one license. It’s read-only. Nothing gets changed or removed — it’s a safe first pass before deciding what to actually reclaim.

Five warning signs are worth watching for in the output: an account disabled more than 90 days ago still on a full E3/E5 seat, multiple licenses stacked on one disabled account, a disabled account still in a licensing group, a disabled service account holding a paid SKU it never needed, and license counts that don’t match your last true-up. Each one usually traces back to the same root cause — offboarding that disabled the account without touching Microsoft 365 licenses in the same step.

Connecting With the Right Scopes

Connect-MgGraph -Scopes "User.Read.All", "Organization.Read.All"Code language: CSS (css)

If the module isn’t installed yet, see how to install the Microsoft Graph PowerShell module first, then connect to Microsoft Graph with PowerShell using the scopes below. Both scopes are load-bearing. User.Read.All covers reading other users’ AccountEnabled and AssignedLicenses properties — your own User.Read isn’t enough once you’re querying the whole directory. Organization.Read.All is what Get-MgSubscribedSku actually depends on, even though the cmdlet name doesn’t obviously suggest an organization-level permission.

Resolving License Names Before You Need Them

$ServicePlans = Get-MgSubscribedSku | Select-Object SkuId, SkuPartNumberCode language: JavaScript (javascript)

AssignedLicenses only stores a SkuId — a GUID, meaningless on sight. Pulling every SKU the tenant has ever subscribed to once, up front, turns the per-user lookup later into an in-memory table match instead of a Graph call per user. For a tenant with more than a handful of disabled-but-licensed accounts, that’s the whole performance story.

Finding Accounts With Disabled Status but Active Microsoft 365 Licenses

$DisabledUsers = Get-MgUser -Filter "AccountEnabled eq false" -Property Id, DisplayName, UserPrincipalName, AssignedLicenses, AccountEnabled -AllCode language: PHP (php)

-Property isn’t optional politeness. Graph doesn’t return AssignedLicenses by default, and asking for exactly the fields you need keeps the query fast across a large directory. -All handles pagination automatically.

AccountEnabled eq false is a single, simple equality filter, and it runs as written with no extra parameters. That changes the moment you extend it — combine it with and, filter on a collection with $count, or switch to ne, and Graph starts requiring -ConsistencyLevel eventual and -CountVariable before the query runs at all.

Reading the Results

$Result = foreach ($User in $DisabledUsers) {
    if ($User.AssignedLicenses.Count -gt 0) {
        $UserLicenses = foreach ($License in $User.AssignedLicenses) {
            ($ServicePlans | Where-Object { $_.SkuId -eq $License.SkuId }).SkuPartNumber
        }
        [PSCustomObject]@{
            DisplayName       = $User.DisplayName
            UserPrincipalName = $User.UserPrincipalName
            AccountEnabled    = $User.AccountEnabled
            ActiveLicenses    = $UserLicenses -join ", "
        }
    }
}
$Result | Format-Table -AutoSizeCode language: PHP (php)

This is the filter that matters: out of every disabled account, keep only the ones where AssignedLicenses.Count is greater than zero. In a reasonably well-run tenant, most disabled accounts are already unlicensed. The ones that show up here are the exceptions worth a closer look.

SkuPartNumber prints values like ENTERPRISEPACK or SPE_E5, not “Office 365 E3” or “Microsoft 365 E5.” Microsoft maintains a reference table mapping product names to service plan identifiers, including a downloadable CSV. That table lags newer SKUs — Copilot bundles especially — so an unrecognized code isn’t necessarily a bug in your script.

For a report you’ll actually act on rather than just eyeball, export it:

$Result | Export-Csv -Path "C:\Reports\DisabledUsersWithLicenses.csv" -NoTypeInformationCode language: PHP (php)

Reclaiming the Licenses

Removing a license is a deliberate, separate step from reporting on one. Don’t fold the two into a single script that reports and revokes in the same run.

$Sku = Get-MgSubscribedSku -All | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK'
Set-MgUserLicense -UserId "[email protected]" -RemoveLicenses @($Sku.SkuId) -AddLicenses @()Code language: JavaScript (javascript)

One thing trips this up on some accounts: if the license came from group-based licensing rather than direct assignment, Set-MgUserLicense fails with an error saying the license is inherited from a group. The fix there is removing the account from the licensing group, not fighting the cmdlet — if you’re not sure which group that is, exporting the account’s group memberships first will show you.

For a deeper walkthrough of the removal cmdlet itself, including bulk removal from a CSV of users, see how to remove Microsoft 365 licenses with PowerShell.

Making This a Recurring Check

Run once, this catches whatever’s already accumulated. Run on a schedule, it catches the process gap that let Microsoft 365 licenses survive account disablement in the first place the more useful outcome of the two. Wire it into a scheduled task or an Azure Automation runbook using certificate or managed identity authentication instead of the interactive sign-in shown here, and it becomes a standing check instead of something that only runs when someone remembers to.

Microsoft 365 Licenses

Frequently Asked Questions

Why would a disabled account still have a license?

Usually because disabling and de-licensing were separate manual steps in offboarding, and the second one got missed. Group-based licensing can also keep a license attached as long as the account stays in the licensing group, even after the account itself is disabled.

Do I need Organization.Read.All if I only care about user properties?

Yes, if the script also calls Get-MgSubscribedSku. That cmdlet depends on Organization.Read.All specifically.

Why does SkuPartNumber show a code instead of a plan name I recognize?

SkuPartNumber is Microsoft’s internal product identifier, not the name shown in the admin center. Cross-reference it against Microsoft’s product names and service plan identifiers reference table.

Can I have this script remove Microsoft 365 licenses automatically?

You can, with Set-MgUserLicense, but treat it as a deliberate second step after reviewing the report, not part of the same run that generates it.

What if Set-MgUserLicense fails with a group membership error?

That license is assigned through group-based licensing. Remove the account from the licensing group instead of trying to remove the license directly from the user.

References

Microsoft — Product Names and Service Plan Identifiers for Licensing https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference

Microsoft — Set-MgUserLicense (Microsoft.Graph.Users.Actions) https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.users.actions/set-mguserlicense

Microsoft — Get-MgSubscribedSku (Microsoft.Graph.Identity.DirectoryManagement) https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.identity.directorymanagement/get-mgsubscribedsku

Ahmet Tolga KAYA

Systems Engineer and Technical Writer focused on Windows, Microsoft technologies, infrastructure, cybersecurity, automation, and platform reliability.