๐ณ Recipe ยท Licensing & Cost Optimization
Audit Microsoft 365 licenses: find inactive users, unassigned SKUs, and savings opportunities
Identify expensive licenses assigned to inactive accounts, surface unassigned SKUs, and produce a prioritized optimization plan with estimated monthly savings.
Complexity
Intermediate
Impact
cost-savings + license-optimization + reporting + quarterly-audit + finops
Context
Why This Matters
Microsoft 365 licensing is one of the largest recurring line items in most IT budgets, and license sprawl is easy to accumulate: departed employees whose accounts were never disabled, contractors who only needed access for a quarter, or bulk purchases that left unassigned SKUs sitting idle. Without a regular audit, organizations routinely pay for 10-25% more seats than they actually use.
This recipe walks through a repeatable audit that cross-references subscribed SKUs (what you own) against user sign-in activity (who actually uses it) to surface three categories of waste:
- Inactive licensed users โ accounts with expensive licenses (E3, E5, Visio, Project) that have not signed in for 90+ days.
- Unassigned SKUs โ purchased seats sitting idle in the tenant.
- Downgrade candidates โ users on E5 who only use E3-tier features (no Defender, no Power BI Pro, no phone system).
Run this quarterly, before renewal negotiations, or whenever Finance asks why the Microsoft bill keeps growing.
Expected Outcomes
After completing this recipe you will have:
- A complete inventory of subscribed SKUs with total/assigned/available counts.
- A list of inactive users (90+ days no sign-in) who currently hold paid licenses.
- A count of unassigned licenses per SKU and their monthly cost.
- An optimization plan identifying which licenses to reclaim, downgrade, or let lapse.
- An estimated monthly and annual savings figure to bring to Finance or your Microsoft account rep.
Risks & Considerations
Before you reclaim licenses
- Don't remove licenses from shared mailboxes or resource accounts. These legitimately have no sign-in activity but still need an Exchange license if over 50 GB or if litigation hold is applied.
- Check for service account exemptions. Break-glass accounts, automation identities, and hybrid sync accounts may show as inactive but must retain licensing.
- Removing an E5/E3 license purges mailbox data after 30 days. If the user is on leave, convert the mailbox to shared before unlicensing, or place on retention hold first.
- Guest (B2B) users do not consume licenses up to a 1:5 ratio โ don't count them as waste.
- signInActivity requires Azure AD P1 or higher. If you see null values everywhere, your tenant lacks the SKU to populate this field.
- Coordinate with HR before disabling accounts flagged as inactive โ an "inactive" user may simply be on parental leave.
Required Permissions
| Permission | Why It's Needed |
|---|---|
| Organization.Read.All | List subscribed SKUs, their assigned/consumed unit counts, and service plans. |
| User.Read.All | Enumerate users and their assignedLicenses property to cross-reference with activity. |
| AuditLog.Read.All | Access the signInActivity property on the user object to determine last sign-in date. |
| Reports.Read.All | Pull per-service usage reports (Teams, OneDrive, Exchange, SharePoint) to spot users with a license but no actual product usage. |
The fastest way to get this done: just ask Dex. Copy the prompt below and paste it into your Dex conversation.
For IT Admins
Paste into Dex Pro
Manual walkthrough โ Microsoft 365 admin center
Use this path for a quick one-off audit or when you only need to spot-check. For tenants over ~500 users, use the Graph API or PowerShell approaches instead.
Step 1 โ Review overall license position
- Sign in to admin.microsoft.com as a Global Reader or Global Administrator.
- Navigate to .
- For each subscription, note Assigned vs Total. The delta is your unassigned SKU count.
- Click any SKU to see the per-user assignment list.
Step 2 โ Identify inactive users
- Go to .
- Click Add filter and choose Last sign-in date (interactive).
- Set the filter to Before a date 90 days ago.
- Add a second filter for Account enabled = Yes.
- Export the filtered list to CSV using the Download users button.
Step 3 โ Cross-reference licenses
- Open the exported CSV in Excel and add a column for assigned license SKUs (visible in the export).
- Filter rows where the license column is non-empty โ these are your reclaim candidates.
- Sort by SKU to prioritize the most expensive licenses (E5 > E3 > Business Premium > Business Standard).
Step 4 โ Spot unused premium features
- In the M365 admin center, open .
- Review the Microsoft 365 Apps usage, Teams user activity, and OneDrive usage reports for the last 90 days.
- Users with an E5 license but zero Teams/OneDrive activity are strong downgrade candidates.
Step 5 โ Act on findings
- For confirmed inactive accounts: remove license via .
- For unassigned SKUs: open a billing support ticket or contact your CSP/reseller to reduce seat count at renewal.
- For downgrades: assign the cheaper SKU first, then remove the expensive one (to avoid service interruption).
Graph API approach
The audit requires three Graph endpoints. Run them in order and join the results client-side.
1. List all subscribed SKUs
GET https://graph.microsoft.com/v1.0/subscribedSkusReturns an array of SKU objects. The fields you care about:
skuIdโ GUID used in user.assignedLicenses.skuPartNumberโ human-readable (e.g.ENTERPRISEPREMIUM= E5).prepaidUnits.enabledโ total seats purchased.consumedUnitsโ seats currently assigned.prepaidUnits.enabled - consumedUnitsโ unassigned (waste).
2. Enumerate users with licenses and sign-in activity
GET https://graph.microsoft.com/v1.0/users?$select=id,userPrincipalName,displayName,accountEnabled,assignedLicenses,signInActivity&$top=999Pagination: follow @odata.nextLink until it is absent. Do not rely on a single page for tenants over 999 users.
Example response fragment:
{
"userPrincipalName": "jane@contoso.com",
"accountEnabled": true,
"assignedLicenses": [{ "skuId": "c7df2760-..." }],
"signInActivity": {
"lastSignInDateTime": "2024-03-14T09:22:11Z"
}
}3. (Optional) Pull per-service usage reports
For deeper downgrade analysis, pull 90-day activity reports. These endpoints return CSV by default:
GET /v1.0/reports/getOffice365ActiveUserDetail(period='D90')
GET /v1.0/reports/getTeamsUserActivityUserDetail(period='D90')
GET /v1.0/reports/getOneDriveUsageAccountDetail(period='D90')A user with an E5 license but empty Teams Last Activity Date and zero OneDrive file count is a high-confidence downgrade candidate.
Join logic
- Build a dictionary of
skuId -> {partNumber, unassigned, estimatedCost}from call #1. - For each user from call #2, flag as inactive if
accountEnabled = trueAND (signInActivity.lastSignInDateTimeis null OR older than 90 days) ANDassignedLicenses.length > 0. - Aggregate potential savings:
(inactive users per SKU + unassigned units per SKU) ร SKU monthly price.
Note: Graph does not return license prices. Maintain a lookup table of your negotiated rates (from your EA or CSP invoice).
PowerShell approach (Microsoft.Graph module)
The downloadable script below automates the full audit end-to-end. High-level flow:
- Connect to Graph with the four required scopes.
- Fetch all
subscribedSkusand build a price lookup (you provide rates in a hashtable at the top of the script). - Page through all users with
Get-MgUser, selectingsignInActivityandassignedLicenses. - Classify each licensed user as active, inactive (90+ days), or never-signed-in.
- Emit three CSVs:
InactiveLicensedUsers.csv,SkuUtilization.csv,OptimizationPlan.csv. - Print a summary with estimated monthly/annual savings.
Prerequisites
Install-Module Microsoft.Graph -Scope CurrentUser- Azure AD Premium P1 or higher (required for
signInActivitydata). - At minimum Global Reader role; License Administrator if you also plan to act on findings.
Running it
# Read-only audit (default)
.\Audit-M365Licenses.ps1 -InactiveThresholdDays 90 -OutputPath .\audit
# Custom threshold and specific tenant
.\Audit-M365Licenses.ps1 -InactiveThresholdDays 60 -TenantId "contoso.onmicrosoft.com"The script does not remove licenses automatically. It produces a plan for you to review. Add a -Apply parameter and a confirmation prompt if you want to extend it.
Next steps