๐ณ Recipe ยท Licensing & Cost Optimization
Export a Report of All Users and Their Assigned Licenses
Generate a complete CSV inventory of every Microsoft 365 user with their assigned license SKUs, resolved to friendly product names
Complexity
Beginner
Impact
read-only + reporting + license-audit + compliance + bulk
Context
Why This Matters
License reporting is one of the most common recurring tasks for a Microsoft 365 administrator. Finance wants to reconcile monthly spend, security wants to verify who holds privileged SKUs (E5 Security, P2, etc.), and IT leadership wants to identify unused or overprovisioned licenses before renewal.
This recipe produces a full per-user license inventory across the tenant: display name, UPN, and every assigned SKU (translated from GUIDs to human-readable product names like ENTERPRISEPACK or SPE_E5). Run it whenever you need an audit-ready snapshot โ typically before a renewal, during a license true-up, or as part of quarterly access reviews.
Microsoft 365 does not expose a single built-in export that joins users with their resolved license names, so admins either click through the GUI one user at a time, use the Licenses blade's limited export, or script against Microsoft Graph. Scripting is the fastest and most repeatable option.
Expected Outcomes
After running this recipe you will have:
- A CSV file (
User_License_Report.csv) with one row per user containing Display Name, User Principal Name, Account Enabled, and a comma-separated Licenses column. - License SKU GUIDs resolved to friendly
skuPartNumbervalues (e.g.ENTERPRISEPACK,SPE_E5,FLOW_FREE). - A clear view of unlicensed accounts โ useful for spotting guest users, shared mailboxes, room resources, and stale enabled users who are wasting no license but may still pose a risk.
- A reusable script you can schedule (Task Scheduler / Azure Automation) for monthly reporting.
Risks & Considerations
Before you run this:
- Data sensitivity. The output contains every user's UPN and license entitlements. Treat the CSV as confidential and store it in a location governed by your DLP / retention policy.
- Read-only scopes only. This recipe needs
User.Read.AllandOrganization.Read.All. Do not consent to write scopes (User.ReadWrite.All,Directory.ReadWrite.All) for a reporting task. - Guest users are included.
GET /usersreturns members and guests by default. If you only want employees, filter onuserType eq 'Member'. - Disabled accounts are included. Disabled accounts can still hold licenses (common licensing waste). Review before deprovisioning โ if you remove a license from a mailbox-enabled user without a grace-period plan, mailbox data can be deleted after 30 days.
- Friendly SKU names are internal codes.
skuPartNumbervalues likeSPE_E5are Microsoft's internal identifiers. Use Microsoft's product names and service plan identifiers reference to translate for non-technical stakeholders. - Large tenants require paging. Tenants over 999 users need
@odata.nextLinkhandling โ the script provided does this automatically.
Required Permissions
| Permission | Why It's Needed |
|---|---|
| User.Read.All | Read every user's profile and assignedLicenses property |
| Organization.Read.All | Read tenant subscribedSkus to map SKU GUIDs to skuPartNumber friendly names |
| Directory.Read.All | Alternative broader scope if User.Read.All is not available; grants the same read-only view |
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 CoAdmin