To find inactive licensed users in Microsoft 365, read each enabled member's licences and last successful sign-in, and keep those that hold a licence but haven't signed in for your cutoff, such as 90 days. The sign-in date needs Microsoft Entra ID P1 or P2, and an account that has never signed in counts from the day it was created.
| Method | Best fit |
|---|---|
| PowerShell | A CSV of licensed members with no successful sign-in for the days you choose, with their products. |
| Admin centre | A view of who uses each licensed product, from the usage reports. |
| Altivane | Recommended for regular licence reviews, with the count rated against a threshold. |
Before you start
Use an account with a role that can read sign-in activity: Reports Reader is the least privileged role Microsoft lists, and it can open the usage reports too. The tenant needs Microsoft Entra ID P1 or P2 for the sign-in date. The PowerShell example requests User., AuditLog. and LicenseAssignment.; your organisation may require administrator consent.
Know how fresh the date is. The last successful sign-in is recorded from 1 December 2023 and isn't filled in for earlier ones, and it can take up to 24 hours to update. Microsoft calls 90 to 180 days a reasonable window for many organisations: the script uses 90 and Altivane starts at 180. The examples leave out guests and disabled accounts. They read and export, and change no account or licence.
Option 1
Find inactive licensed users with PowerShell
Install the modules once, then connect and export the licensed member accounts with no successful sign-in for 90 days or more. Change $days to widen or narrow the window.
# Install once, if the modules are not already installed.
Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Users -Scope CurrentUser
Install-Module Microsoft.Graph.Identity.DirectoryManagement -Scope CurrentUser$ErrorActionPreference = 'Stop'
$days = 90
$cutoff = (Get-Date).ToUniversalTime().AddDays(-$days)
$scopes = 'User.Read.All', 'AuditLog.Read.All', 'LicenseAssignment.Read.All'
Connect-MgGraph -Scopes $scopes -NoWelcome
try {
$names = @{}
foreach ($sku in Get-MgSubscribedSku -All) {
$names[$sku.SkuId.ToString()] = $sku.SkuPartNumber
}
$properties = 'displayName,userPrincipalName,userType,createdDateTime,' +
'assignedLicenses,signInActivity'
$users = Get-MgUser -All -Filter 'accountEnabled eq true' -Property $properties
$report = @(foreach ($user in $users) {
if ($user.UserType -ne 'Member' -or $user.AssignedLicenses.Count -eq 0) { continue }
$last = $user.SignInActivity.LastSuccessfulSignInDateTime
# With no sign-in on record, count from the day the account was created.
$since = if ($last) { $last } else { $user.CreatedDateTime }
if ($since -and $since.ToUniversalTime() -gt $cutoff) { continue }
$held = @($user.AssignedLicenses | ForEach-Object {
$id = $_.SkuId.ToString()
if ($names.ContainsKey($id)) { $names[$id] } else { $id }
})
[pscustomobject]@{
DisplayName = $user.DisplayName
UserPrincipalName = $user.UserPrincipalName
LastSuccessfulSignIn = $last
Created = $user.CreatedDateTime
Licences = $held -join '; '
}
})
if ($report.Count -eq 0) {
Write-Output "No licensed member has been inactive for $days days or more."
} else {
$report | Sort-Object LastSuccessfulSignIn |
Export-Csv -Path './inactive-licensed-users.csv' -NoTypeInformation -Encoding utf8
Write-Output "$($report.Count) licensed members inactive for $days days or more."
}
} finally {
Disconnect-MgGraph | Out-Null
}The CSV holds each account's name, sign-in name, last successful sign-in, creation date and the part numbers of its licences, the longest idle first; a blank sign-in means none on record. The script reads every enabled account's sign-in activity, because Microsoft says a filter on signInActivity can't be combined with another filterable property. Remove the UserType test to include guests.
Check a single account
Reconnect with Connect-MgGraph, then read 1 person's sign-in dates and licences.
# Run while connected to Microsoft Graph. Replace the example account.
$filter = "userPrincipalName eq 'person@example.com'"
$user = Get-MgUser -Filter $filter -Property 'displayName,signInActivity'
$user.SignInActivity | Select-Object LastSuccessfulSignInDateTime, LastSignInDateTime
Get-MgUserLicenseDetail -UserId 'person@example.com' | Select-Object SkuPartNumberLastSignInDateTime counts failed interactive attempts as well, so it can be later than the last successful sign-in.
Option 2
Review licensed product use in the admin centre
- Sign in to the Microsoft 365 admin centre for the correct tenant.
- Go to Reports > Usage. If you don't see Reports, select Show all first.
- Under Reports, select Microsoft 365 apps, then the Active users tab.
- In the table, read each person's last active date for Exchange, OneDrive, SharePoint, Viva Engage and Teams beside the licences they hold for each. Use Choose columns to add the columns you need.
- Select Export for a CSV of every user to sort by last activity.
The report measures product use, not sign-ins, and its data usually runs 24 to 48 hours behind. Names are hidden in usage reports by default, so the list may show codes in place of people until the tenant's report settings show them. It suits a review of who uses what; for sign-in dates, use PowerShell or Altivane.
Option 3 ยท Recommended for regular licence reviews
Find inactive licensed users with Altivane
- Open Altivane and choose the tenant.
- Run the Licensing scorecard report.
- Open the Excel workbook and find Licences Held by Stale Users: each enabled member with a licence who is stale or has never signed in, with the products, how each was assigned, the last sign-in and the usage location.
- Read Licence Summary for the count, and the scorecard for how it rates against your thresholds.
Altivane reads the same sign-in activity, so the same licence applies: without Microsoft Entra ID P1 or P2 the count says Not licensed and the table is left out. An account is stale after 180 days with no successful sign-in, and one that has never signed in is listed once it is 30 days old; both are settings you can change. The scorecard marks the count Warning from 5 accounts and Critical from 20 until you change the thresholds. The report needs a tenant signed in with a certificate.
Contact usCommon questions
Why is a shared mailbox's account on the list?
A shared mailbox has a user account that nobody signs in to, so a licensed one shows as never signed in. It may need its licence: a shared mailbox over 50 GB, with an archive or on litigation hold does.
Why does a recent sign-in not show yet?
The last sign-in date can take up to 24 hours to update. An account whose last successful sign-in came before 1 December 2023 has no date in that property, so check its older sign-in dates before treating it as never used.
What about disabled accounts that still hold licences?
The list keeps enabled accounts only. See how to find disabled users with licences in Microsoft 365.
Microsoft references
Technical references checked on 7 November 2026. The example scripts have been reviewed and syntax checked; they have not been run against a live tenant.
- How to detect and investigate inactive user accounts
- signInActivity properties
- List users in Microsoft Graph
- Get-MgUser reference
- Get-MgSubscribedSku reference
- List subscribedSkus in Microsoft Graph
- View licensed and unlicensed users with PowerShell
- Microsoft 365 apps Active users report
- Microsoft 365 admin centre usage reports
- About shared mailboxes