Why this keeps happening
Someone leaves the company. IT disables the account and hands their laptop in. HR processes the offboarding. Three months later the M365 license is still assigned and still billing.
It's not negligence — it's just that license cleanup isn't part of any standard offboarding checklist. Same thing happens with contractors whose projects ended, accounts created for people who never actually started, and users who moved to a different role that doesn't need the apps they had before. Microsoft doesn't revoke licenses automatically when an account goes dormant. It just keeps charging.
Option 1: The admin center usage report
Quickest place to start, no scripts required. Go to admin.microsoft.com, then Reports → Usage → Microsoft 365 active users. Export it as CSV and filter for anyone with zero activity in the last 30 days.
While you're there, check Billing → Licenses. The gap between seats purchased and seats assigned is money you're paying for nothing — those can be removed immediately without even checking activity.
The catch: the usage report only covers Microsoft apps — Teams, Exchange, SharePoint, Word, etc. It doesn't tell you whensomeone last signed in, only whether they opened an app in the reporting window. A user who exclusively uses Outlook on mobile might look inactive here because the mobile activity isn't always counted the way you'd expect.
Option 2: Pull sign-in activity via PowerShell
If you want actual last-login timestamps, you need to query Entra ID (formerly Azure AD) directly. The SignInActivity property on each user gives you exactly that.
One important caveat: SignInActivityrequires Microsoft Entra ID P1, P2, or Microsoft 365 Business Premium. On lower plans the property exists but comes back empty for everyone. If that's your situation, the usage report above is your only built-in option.
Assuming you have the right license, here's the script:
PowerShell
# Install module if needed
Install-Module Microsoft.Graph -Scope CurrentUser
# Connect with required scopes
Connect-MgGraph -Scopes "User.Read.All"
# Get all licensed users and their last sign-in
Get-MgUser -All -Property `
DisplayName,UserPrincipalName,`
AssignedLicenses,SignInActivity |
Where-Object { $_.AssignedLicenses.Count -gt 0 } |
Select-Object DisplayName, UserPrincipalName,
@{N="LastSignIn";E={
$_.SignInActivity.LastSignInDateTime
}} |
Where-Object {
$_.LastSignIn -lt (Get-Date).AddDays(-30) -or
$null -eq $_.LastSignIn
} |
Export-Csv -Path "unused_m365_licenses.csv" -NoTypeInformationThis gives you a CSV of every licensed user who hasn't signed in for 30+ days, plus anyone whose sign-in date is null — which usually means the account was created, a license was assigned, and the person never actually logged in.
Note: the script only needs User.Read.Allto run. You don't need audit log permissions for this.
What to do once you have the list
Don't just start removing licenses. A few things to check first:
- Check with their manager or HR. The user might be on extended leave, a sabbatical, or parental leave. Also watch out for shared mailbox accounts and service accounts — these often show zero sign-in activity but are very much in use.
- Remove the license, don't delete the account. In the admin center: Active users → select the user → Licenses and apps → uncheck. For bulk removals, Set-MgUserLicense works. Deleting the account is a separate decision — removing the license is enough to stop the billing.
- Hold onto the mailbox if needed. If the person left recently, there may be compliance or legal hold requirements on their email. You can keep the account in an unlicensed state for retention purposes.
- Reduce your committed seat count. Once you've freed up seats, talk to your reseller or check your agreement for mid-term reduction options. Most Microsoft NCE agreements allow reductions at renewal — some allow them mid-term depending on your contract.
The real problem is doing this repeatedly
Running this once is fine. The issue is it needs to happen every month, and it won't — because there's always something more urgent. People leave, new contractors start, roles change. The waste accumulates quietly between audits.
Most IT teams that do this manually end up doing it once or twice a year during budget reviews. They find the same stale accounts they would have caught months earlier if they'd looked sooner.
If you want this to run automatically
Reach Seats connects to your Microsoft 365 tenant via OAuth and runs the same check — licensed users vs. sign-in activity — every 24 hours. It flags inactive seats, shows you the monthly cost of each one, and keeps the list current without anyone having to remember to run a script.
It covers Google Workspace, GitHub, Slack, Zoom, and Jira at the same time, so if you're trying to get a full picture of SaaS waste across your stack, you're not doing six separate exports.
There's a 7-day free trial, no credit card needed.