Table of Contents
Update the Microsoft 365 Licensing Report Script to Find Underused Accounts
In October 2021, I wrote about how to use the Microsoft Graph PowerShell SDK to create a licensing report for a Microsoft 365 tenant. That report lists the licenses assigned to each user account together with any disabled service plans for those licenses. It’s a valuable piece of information to help tenants manage license costs.
But we can do better. At least, that’s what some readers think. They’d like to know if people use their assigned licenses so that they can remove expensive licenses from accounts that aren’t active. One way to approach the problem is to use the Microsoft 365 User Activity Report script to identify people who haven’t been active in Exchange Online. SharePoint Online, Teams, OneDrive for Business, and Yammer over the last 180 days. The report already includes an assessment of whether an account is in use, so all you need to do is find those who aren’t active and consider removing their licenses.
Another solution to the problem is to update the licensing report script. To do this, I made several changes to the script (the updated version is available from GitHub).
Filtering for Licensed Accounts
The first change is to the filter used with the Get-MgUser cmdlet. The new filter selects only member accounts that have licenses. Previously, I selected all member accounts, but now we’re interested in chasing down underused licensed accounts. Here’s the command I used:
[Array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual -CountVariable Records -All -Property signInActivity | Sort-Object DisplayName
The filter applied to Get-MgUser finds member accounts with at least one license. The command also retrieves the values of the signInActivity property for each account. This property holds the date and time for an account’s last interactive and non-interactive sign-ins. Here’s what the data for an account looks like:
LastNonInteractiveSignInDateTime : 27/09/2022 13:04:58 LastNonInteractiveSignInRequestId : bcd2d562-76f0-4d29-a266-942f7ee31a00 LastSignInDateTime : 11/05/2022 12:19:18 LastSignInRequestId : 3f691116-5e0a-4c4c-a3a9-aecb3ae99800 AdditionalProperties : {}
The last non-interactive sign-in might be something like a synchronization operation performed by the OneDrive sync client or a sign-in using an access token for the user account to another Microsoft 365 app. I’m not too interested in these sign-in activities as I want to know about licensed accounts that aren’t taking full advantage of their expensive licenses. Hence, we focus on the timestamp for the last interactive sign-in.
Update: Microsoft now supports a timestamp for the last successful sign in for Entra ID accounts. The LastSignInDateTime property can capture an unsuccessful sign-in, so using the new lastSuccessfulSignInDateTime property is a better choice in most situations. However, Entra ID only captures data for the property from December 1, 2023.
Calculating How Long Since an Account Sign-in
To detect an underused account, we need to define how to recognize such an account. To keep things simple, I define an underused account as being more that hasn’t signed in interactively for over 60 days. An account in this category costs $23/month if it holds an Office 365 E3 license while one assigned an E5 license costs $38/month. And that’s not taking any add-on licenses into account. At $30/month, we’ve already paid $60 for an underused account when it matches our criterion.
The script checks to see if any Entra ID sign-in information is available for the account (i.e., the account has signed in at least once). If it does, we extract the timestamp for the last interactive sign-in and compute how many days it is since that time. If not, we mark the account appropriately.
# Calculate how long it's been since someone signed in If ([string]::IsNullOrWhiteSpace($User.SignInActivity.LastSignInDateTime) -eq $False) { [datetime]$LastSignInDate = $User.SignInActivity.LastSignInDateTime $DaysSinceLastSignIn = ($CreationDate - $LastSignInDate).Days $LastAccess = Get-Date($User.SignInActivity.LastSignInDateTime) -format g If ($DaysSinceLastSignIn -gt 60) { $UnusedAccountWarning = ("Account unused for {0} days - check!" -f $DaysSinceLastSignIn) } } Else { $DaysSinceLastSignIn = "Unknown" $UnusedAccountWarning = ("Unknown last sign-in for account") $LastAccess = "Unknown" }
Note that it can take a couple of minutes before Entra ID updates the last interactive timestamp for an account. This is likely due to caching and the need to preserve service resources.
Reporting Underused Accounts
The last change is to the output routine where the script now reports the percentage of underused accounts that it finds. Obviously, it’s not ideal if this number is more than a few percent.
I usually pipe the output of reports to the Out-GridView cmdlet to check the data. Figure 1 shows the output from my tenant. Several underused accounts are identified, which is what I expect given the testing and non-production usage pattern within the tenant. Another advantage of Out-GridView is that it’s easy to sort the information to focus in on problem items as seen here.

Customizing the Output
Seeing that the script is PowerShell, it’s easy to adjust the code to meet the requirements of an organization. Some, for instance, might have a higher tolerance level before they consider an account underutilized and some might be more restrictive. Some might like to split the report up into departments and send the underused accounts found for each department to its manager for review. It’s PowerShell, so go crazy and make the data work for you.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
I got this error: The term ‘Get-MgSubscribedSku’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify
that the path is correct and try again.
Did you connect to the Graph SDK by running:
Connect-MgGraph
Select-MgProfile Beta
Those lines are in the script…
Hello, i test the Script an it looks very good. Problem that the Report is show in ROW “Licence” only Comma and no Licence Names Info. On Disabled Plans i see entrys like “VIVA_LEARNING”.
Did you create the CSV files with the expanded license information as per the comments in the script?
Hi Tony,
Thank you for your guide and all the material you are sharing with us all.
I make similar reports for our customers but utilize the data through MS Graph API and then load the data to Power BI.
When going through your guide I was little confused about you ignoring the ‘lastNonInteractiveSignInDateTime’ property, because ignoring it will likely produce false positives on accounts that might appear unused.
Every time you sign-in into your system the ‘lastSignInDateTime’ updates. The Sign-in creates a token on your system that e.g. includes location data and basically allows you to use the installed M365 suite products and other Microsoft services on your system in that given location. You don’t need to sign-in into Teams, when you already signed in Outlook and thats basically the ‘lastNonInteractiveSignInDateTime’ in play here.
Before implementing the ‘lastNonInteractiveSignInDateTime’ I once presented a report for one of our customers with a list of inactive users. One of the users (with an expensive M365 E5 license) had not been active in more than 180 days according to ‘lastSignInDateTime’, which was immediately questioned by our customer, because the “inactive” user was his secretary working just beside him. She never worked from home, but only from the office. The only way I could verify his claim was to include the non interactive sign in property, which in her case was updated the day before the report was presented.
In my own case the ‘lastSignInDateTime’ constantly changes, because I’m working back and forth between home and office and my sign in token needs to be replaced all the time due to changing location.
Microsoft even recommends using both properties in order to correctly identify inactive users. (https://learn.microsoft.com/en-us/graph/api/resources/signinactivity?view=graph-rest-1.0).
Sorry for any spelling errors, I’m not a native English speaker.
Have a good day
/Michael
Hi Michael,
Microsoft now supports a timestamp for the last successful sign in for Entra ID accounts (see https://office365itpros.com/2023/12/08/lastsuccessfulsignindatetime/). The lastSignInDateTime property can capture an unsuccessful sign-in, so using the new lastSuccessfulSignInDateTime property is a better choice in most situations. Even though Entra ID only captures data for the property from December 1, 2023, this will be the property to use going forward.
As to using the lastNonInteractiveSignInDateTime property, I find it hard to understand that a user could be allowed to maintain credentials for over 180 days… That means the secretary had not successfully produced their credentials to sign into Entra ID for over 180 days, which seems poor from a security perspective.
Anyway, these scripts are intended to illustrate principles. They are not solutions. You always need to take the code and refashion it for your own purposes and to fit the circumstances in a tenant.