๐ณ Recipe ยท Licensing & Cost Optimization
List Microsoft 365 E5 Users with Last Sign-In Time
Generate an audit-ready report of every E5-licensed user, their UPN, and their last interactive and non-interactive sign-in timestamps.
Complexity
Intermediate
Impact
licensing + cost-optimization + governance + audit + reporting
Context
Why This Matters
Microsoft 365 E5 is typically the most expensive SKU in a tenant. Unused or rarely used E5 licenses are a common source of license waste, and stale privileged accounts holding E5 can also represent a security risk (Defender, Purview, and AAD P2 features attached to dormant identities).
This recipe produces a complete inventory of E5-assigned users along with their lastSignInDateTime and lastNonInteractiveSignInDateTime, sorted by recency. It is the starting point for:
- License rightsizing (E5 โ E3 downgrade candidates)
- Quarterly access reviews of premium licenses
- Identifying abandoned or service accounts still consuming premium SKUs
- Feeding a finance/procurement true-up conversation with concrete data
Run it ad-hoc before renewal, or on a monthly schedule as part of a broader license governance cadence.
Expected Outcomes
What you will have when finished
- The exact
skuIdandskuPartNumberfor Microsoft 365 E5 in your tenant (commonlySPE_E5) - A count of consumed E5 licenses and the full list of assigned users
- Per-user: UPN, display name, last interactive sign-in, last non-interactive sign-in
- A CSV report suitable for sharing with security, finance, or auditors
- A clearly identified list of users who have never signed in or who have been inactive beyond your threshold (e.g. 30/60/90 days)
Risks & Considerations
Warnings & considerations
- signInActivity requires AuditLog.Read.All plus a tenant with Azure AD Premium P1 or P2. Free-tier tenants will receive
nullfor all sign-in fields. - signInActivity cannot be combined with
$filteron assignedLicenses in a single Graph call โ Graph returns HTTP 400. Always fetch users + sign-in activity, then filter client-side. - Do not assume a null value means the account is unused. Sign-in logs are retained for 30 days (P1) or 30 days rolling. A null can mean "never signed in in the retention window".
- Non-interactive sign-ins (tokens, service principals, mail clients) can be high even when the human has left โ always check both fields before reclaiming a license.
- Service accounts, shared mailboxes, and break-glass accounts may legitimately hold E5 with rare interactive logins. Whitelist these before any automated reclamation.
- The report contains PII (UPN, display name, activity data). Store it in a controlled location and delete when no longer needed.
Required Permissions
| Permission | Why It's Needed |
|---|---|
| Organization.Read.All | Required to enumerate subscribedSkus and identify the E5 SKU ID in your tenant. |
| User.Read.All | Required to list users and read their assignedLicenses property. |
| AuditLog.Read.All | Required to read the signInActivity property (lastSignInDateTime / lastNonInteractiveSignInDateTime) on user objects. |
| Directory.Read.All | Fallback directory read permission for license-detail and user enumeration scenarios. |
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
Option A: Microsoft 365 Admin Center (basic)
- Sign in to admin.microsoft.com as a Global Reader or License Admin.
- Navigate: .
- Select Microsoft 365 E5 from the license list.
- Click the Assigned licenses tab to see every user currently consuming an E5 license.
- Use Export to download the list (UPN + display name). Note: this export does not include last sign-in time.
Option B: Entra admin center (adds last sign-in)
- Sign in to entra.microsoft.com.
- Navigate: .
- Click Add filter โ Assigned license โ select Microsoft 365 E5.
- Click Columns and enable Last sign-in date (interactive) and Last sign-in date (non-interactive).
- Click Download users to export a CSV.
Note: The Entra portal surfaces the same signInActivity data the Graph API exposes. This requires Azure AD Premium P1/P2. For anything beyond a point-in-time export (scheduling, automation, cross-tenant), use the API or script path.
Microsoft Graph API walkthrough
Step 1 โ Find the E5 SKU ID
GET https://graph.microsoft.com/v1.0/subscribedSkus?$select=skuId,skuPartNumber,consumedUnitsLook for skuPartNumber values such as SPE_E5 (Microsoft 365 E5), ENTERPRISEPREMIUM (Office 365 E5), or Microsoft_365_E5. Capture the skuId GUID.
Step 2 โ Enumerate users with license + sign-in activity
Do NOT combine $filter=assignedLicenses/any(...) with $select=signInActivity โ Graph returns 400 Bad Request. Instead, select everything and filter client-side:
GET https://graph.microsoft.com/v1.0/users?$select=userPrincipalName,displayName,assignedLicenses,signInActivity&$top=999Follow the @odata.nextLink until all pages are consumed.
Step 3 โ Filter & shape the output
Keep users where any assignedLicenses[].skuId equals the E5 skuId from Step 1. Read signInActivity.lastSignInDateTime and signInActivity.lastNonInteractiveSignInDateTime.
Example response shape
{
"userPrincipalName": "jane.doe@contoso.com",
"displayName": "Jane Doe",
"assignedLicenses": [{ "skuId": "06ebc4ee-1bb5-47dd-8120-11324bc54e06" }],
"signInActivity": {
"lastSignInDateTime": "2026-03-26T05:00:08Z",
"lastNonInteractiveSignInDateTime": "2026-03-26T00:40:44Z"
}
}Gotchas
signInActivityisnullfor tenants without AAD Premium P1/P2.- A null
lastSignInDateTimemeans "no interactive sign-in within retention", not necessarily "never". - Use
ConsistencyLevel: eventualand$count=trueif you need to pair with advanced queries.
PowerShell with Microsoft Graph SDK
Install and connect once:
Install-Module Microsoft.Graph -Scope CurrentUser
Connect-MgGraph -Scopes "Organization.Read.All","User.Read.All","AuditLog.Read.All"Why this script works the way it does
- It first resolves the E5 SKU dynamically rather than hard-coding a GUID โ tenant SKU IDs can vary by region/offer.
- It requests
signInActivityfor all users (paginated) and filters client-side, because Graph does not support$filteronassignedLicensestogether with$select=signInActivity. - It sorts descending by last interactive sign-in so stale accounts bubble to the bottom of the CSV.
- It emits a CSV at a path you specify, ready for auditors or a finance handoff.
Running it
# Auto-detect the E5 SKU and write the default report
.\Get-E5UsersLastSignIn.ps1
# Specify a custom output path and an explicit SKU part number
.\Get-E5UsersLastSignIn.ps1 -SkuPartNumber "SPE_E5" -OutputPath "C:\Reports\E5-$(Get-Date -f yyyyMMdd).csv"Schedule it via Task Scheduler or an Azure Automation runbook (with a managed identity granted the same Graph scopes) for monthly license governance.
Next steps