HomeNewsletterCommunityMVP FeedToolsArchiveBlogToday's NewsAboutServicesQuick Links Subscribe free
← Back to Blog
Entra ID Group-Based LicensingEntra IDLicensingMicrosoft GraphPowerShell

Group-Based Licensing Fails Silently for Some Users — Here's How to Find Them

IA
Imran Awan
31 July 2026

You add a shiny new "M365 E3 — Licensed Users" security group, assign the Microsoft 365 E3 product to it, and drop 400 users in. The Licenses page shows the group as configured. Job done, coffee earned. Three weeks later a helpdesk ticket lands: a user has no Exchange mailbox and Teams keeps telling them they need a licence. You open their profile and, sure enough, the licence looks assigned via the group. Nothing in the group view, the product view, or the user's Licenses tab flashes red. The failure is real, it has been sitting there for three weeks, and the only place it is actually recorded is inside a per-user property most admins have never queried: licenseAssignmentStates.

This post walks through exactly why a group licence assignment can succeed at the group level and quietly fail for individual members, how to confirm one user's real state before you trust any tooling, and a read-only PowerShell script that enumerates every user in the tenant and surfaces every licence assignment sitting in an error state — along with which group put it there.

The problem: the group is "assigned", the user has nothing

Group-based licensing does not assign a licence to a group as a single atomic thing that either works or does not. Under the hood, Entra ID takes the products you assigned to the group and processes them per member, one user at a time. For most members that processing succeeds and the licence lands. For some members it can fail — and the failure is scoped to that one user, not to the group.

The reasons a single member's assignment fails are well documented, and every one of them is a normal thing that happens in a live tenant:

Not enough licences

You added more members than you have available seats for one of the products in the group. Entra ID licenses members until the pool runs dry; everyone after that fails with a count error.

Service-plan conflict

A service plan in the group's product is mutually exclusive with a plan the user already has from another product assigned directly or via a different group. Entra ID will not assign both.

Missing usage location

A service in the product is not available in the user's usage location, or the user has no allowed location set. The product cannot be provisioned for that user until the location is corrected.

The trap is that none of this is visible where you would instinctively look. The group shows the product as assigned. The product's overview shows a seat count that looks plausible. The affected user's Licenses tab shows the licence as inherited from the group. Everything reads as "fine" because the group-level operation genuinely did complete — it just completed with a per-user error tucked away that no summary view rolls up to your attention.

The misleading part: a licence assignment error does not remove the licence from the group, does not stop the group from processing other members, and does not raise anything you would notice in day-to-day admin. The user simply never gets the underlying service plans, so they show up with no mailbox, no Teams, or no Intune enrolment — and the first signal you get is a human complaining, not a portal alert.

Why it happens: licenseAssignmentStates and the assignedByGroup field

Every Entra user object carries a collection called licenseAssignmentStates. It holds one entry per licence assignment that applies to the user, and it is the single source of truth for how each licence reached them and whether it landed cleanly. Microsoft documents the entry (the licenseAssignmentState resource type) with these fields:

FieldWhat it tells you
skuIdThe product (SKU) this entry is for.
assignedByGroupThe key field. If the licence was assigned directly to the user, this is null. If it was inherited through a group membership, this contains the object ID of the group.
stateCurrent state of the assignment. Documented values: Active, ActiveWithError, Disabled, Error.
errorThe failure reason. null when the assignment succeeded; otherwise one of the documented error values below.
disabledPlansService plans switched off within this assignment.
lastUpdatedDateTimeWhen the state of this assignment was last updated.

That assignedByGroup field is what makes this problem tractable. It is the difference between "this user has a broken licence" and "this user has a broken licence because of this specific group." Straight from the Microsoft Graph documentation: if the field is populated, the licence is inherited from the group whose ID it contains; if it is null, the licence is directly assigned. That is the exact discriminator the script later uses to attribute every error back to its source group.

When the assignment fails, the error field is set to a specific, documented value. These are the real values — not paraphrases — and knowing them tells you the root cause at a glance:

error valueMeaning
CountViolationNot enough available licences for one of the products. Buy more seats, or free some up.
MutuallyExclusiveViolationA service plan in this product conflicts with a plan the user already has from another product.
DependencyViolationA service plan depends on another that is being removed — commonly triggered when a user is removed from a group and a still-needed underlying plan would go with it.
ProhibitedInUsageLocationViolationA service in the product is not permitted in the user's usage location (or a usable location is not set).
UniquenessViolationA unique attribute clashes — classically a duplicate proxyAddresses value, surfaced as "Proxy address is already being used".
OtherAny failure outside the categories above.
Gotcha worth internalising: a state of ActiveWithError is the sneakiest one. It means the licence is assigned and some service plans are working, but at least one plan hit an error and did not provision. The user looks licensed at a glance, so this is the state most likely to survive for weeks. Do not only look for Error; treat ActiveWithError as a finding too, which is exactly what the script does.

Here is the mechanism end to end — one group assignment fanning out into per-user results, with the failure captured only on the affected user object:

1. Admin action
Assign M365 E3 product to the "Licensed Users" security group.
2. Entra processes
Group membership is expanded; each member is licensed one at a time.
395 members OK
state = Active, error = null. Mailbox, Teams, Intune all provision.
5 members fail
state = Error / ActiveWithError, error = e.g. MutuallyExclusiveViolation.
Where it is recorded
Only inside each failed user's licenseAssignmentStates entry, with assignedByGroup pointing back at the group. The group view and product view stay green.
Note: there is no Group Policy, no Intune configuration profile, and no device-side setting anywhere in this problem. Group-based licensing is a pure Entra ID / Microsoft Graph capability. The entire diagnostic surface is the Graph API and the Microsoft 365 admin center — if you go looking for a CSP or a registry key, you are in the wrong place.

How to verify: check one user manually before trusting anything

Before running a tenant-wide script, confirm what a single affected user actually looks like. This does two things: it proves the property behaves the way this post describes in your tenant, and it gives you a concrete data point to check the script's output against. There are two quick ways.

The portal way. In the Microsoft 365 admin center, the errors are surfaced — you just have to open the right tab. Go to Billing › Licenses, select the product, and open the Errors & issues tab to see the list of users who hit a problem for that product. Select a user to see which error they encountered.

Microsoft 365 admin center — Errors & issues tab (Microsoft 365 E3)
j.okoro@contoso.com
Conflicting services — MutuallyExclusiveViolation
Error
p.novak@contoso.com
Usage location not allowed — ProhibitedInUsageLocationViolation
Error
Assigned via group
M365 E3 — Licensed Users
Group

The PowerShell way. The portal tab is per-product and easy to miss; the definitive check is to read the property directly for one user. This is a single read, and it is the exact shape the full script consumes at scale:

PowerShell 7 — manual single-user check
# Read one user's licence assignment states (read-only) $u = Get-MgUser -UserId "j.okoro@contoso.com" -Property displayName,assignedLicenses,licenseAssignmentStates $u.LicenseAssignmentStates | Format-List State, Error, AssignedByGroup, SkuId, LastUpdatedDateTime
Raw output — one entry in an error state
State : Error Error : MutuallyExclusiveViolation AssignedByGroup : 7023a314-6148-4d7b-b33f-6c775572879a SkuId : 05e9a617-0261-4cee-bb44-138d3ef5d965 LastUpdatedDateTime : 10/07/2026 02:41:18

Two things to read here. First, AssignedByGroup is populated with a GUID — so this licence is inherited from a group, not assigned directly, and that GUID is the group to go fix. Second, Error reads MutuallyExclusiveViolation, so the root cause is a service-plan conflict, not a seat shortage. That is the whole diagnosis in two fields.

Which scopes: reading this needs only least-privilege read permissions. Microsoft's own group-based-licensing examples connect with User.Read.All, Directory.Read.All and Organization.Read.All — the last one so Get-MgSubscribedSku can translate a raw skuId into a readable product name. No write scope is needed or wanted for auditing.

The fix: Get-LicenseAssignmentErrorReport.ps1

Opening the Errors & issues tab for every product, or reading one user at a time, does not scale past a handful of checks. The companion script does the same read across the whole tenant: it enumerates every user, walks each entry in licenseAssignmentStates, flags anything whose state is Error or ActiveWithError (or whose error is set), resolves the SKU to a product name and the assignedByGroup ID to a group display name, and prints a per-user, per-error report.

Report-only, by design. This script never creates, modifies, deletes, reprocesses, or remediates anything. Every Microsoft Graph call it makes is a read. It cannot change a licence, a group, or a user even if you wanted it to — it is an audit tool, full stop. Fixing the root cause (buying seats, resolving the plan conflict, setting a usage location, clearing a duplicate proxy address) is always a deliberate, separate action you take by hand once you know what is broken.
Download the script — GitHub

Download Get-LicenseAssignmentErrorReport.ps1 from the Imran76Awan/Daily-Tasks repository on GitHub — no sign-in required. It is read-only and cannot change anything in your tenant — but always validate any script in your own environment before you rely on it.

Get-LicenseAssignmentErrorReport.ps1 View full repo →

It supports two authentication modes. For unattended runs (a scheduled task or an Azure Automation runbook) use app-only certificate auth with -TenantId, -ClientId and -CertificateThumbprint. For a quick interactive check from your workstation use -UseDeviceCode, which requests the three read scopes and nothing more. Here is the initial run, before anyone has fixed the conflict from earlier:

PowerShell 7 — Get-LicenseAssignmentErrorReport.ps1 (initial run)
Connecting to Microsoft Graph (app-only certificate auth)... Connected. Tenant: 8d4f...c1a2 Enumerating users and inspecting licenseAssignmentStates... Users scanned : 412 License error entries found : 5 ================================================================ LICENSE ASSIGNMENT ERRORS (5 found) ================================================================ User : Jane Okoro <j.okoro@contoso.com> Product : SPE_E3 Source : Group: M365 E3 - Licensed Users State : Error Error : MutuallyExclusiveViolation Updated : 10/07/2026 02:41:18 User : Pavel Novak <p.novak@contoso.com> Product : SPE_E3 Source : Group: M365 E3 - Licensed Users State : Error Error : ProhibitedInUsageLocationViolation Updated : 10/07/2026 02:41:18 ================================================================ SUMMARY ================================================================ Total error entries : 5 From group assignment : 4 From direct assignment : 1 ================================================================

The classification core is deliberately simple, and its error handling is the important part. It treats "this user simply has no error entry" as a non-event, but treats any failed Graph call as a hard failure that must be surfaced — never swallowed into a falsely clean result:

Get-LicenseAssignmentErrorReport.ps1 — classification core
foreach ($entry in $user.LicenseAssignmentStates) { # Finding = failed state OR a populated error value $stateBad = $ErrorStates -contains $entry.State $errorBad = $NonErrorValues -notcontains $entry.Error if (-not ($stateBad -or $errorBad)) { continue } # assignedByGroup populated => inherited; null => direct assignment if ([string]::IsNullOrEmpty($entry.AssignedByGroup)) { $method = 'Direct' } else { $method = 'Group' $groupName = Resolve-GroupName -GroupId $entry.AssignedByGroup -Cache $groupCache } }

If any read — the connection, Get-MgSubscribedSku, Get-MgUser, or a group lookup — fails, the script prints a clear error, sets an internal $hadError flag, and exits with code 1. It will not print "0 errors found, all healthy" on the back of a query that never actually ran. Exit codes are machine-readable: 0 means a clean scan with no findings, 2 means findings were detected, and 1 means the run itself failed and the numbers cannot be trusted. Add -ExportCsv to drop the findings to a timestamped CSV for a ticket or an audit trail.

Tip: a licence assignment error inherited from a deleted group is a documented edge case — if a group that assigned licences is removed, the affected entries can go into an error state. The script handles a group ID that no longer resolves gracefully, labelling it "(group not found - possibly deleted)" rather than crashing, so a tidy-up that removed a group still shows up as an attributable finding.

Proof it worked: re-run after fixing the root cause

Take Jane Okoro from the report above. Her error was MutuallyExclusiveViolation: she already held a standalone service plan directly that conflicts with a plan inside the E3 product the group assigns. The fix is a deliberate, manual one — remove the conflicting direct assignment (or disable the conflicting plan on one side) — and then let Entra reprocess. Crucially, the script did not and cannot do that for you; it only told you precisely which user, which group, and which conflict. After the direct conflict is cleared and the group reprocesses that member, the same read-only script run comes back clean:

PowerShell 7 — Get-LicenseAssignmentErrorReport.ps1 (after remediation)
Connecting to Microsoft Graph (app-only certificate auth)... Connected. Tenant: 8d4f...c1a2 Enumerating users and inspecting licenseAssignmentStates... Users scanned : 412 License error entries found : 0 ================================================================ RESULT ================================================================ No license assignment errors found across 412 users. STATUS: CLEAN ================================================================

Zero error entries and a CLEAN status (exit code 0) is the aggregate signal. But as with any per-object problem, the honest final proof is to check the specific object you fixed — re-read Jane's licenseAssignmentStates and confirm the entry for that SKU now reads state = Active, error = null, with assignedByGroup still populated:

PowerShell 7 — confirming the specific user
$u = Get-MgUser -UserId "j.okoro@contoso.com" -Property licenseAssignmentStates $u.LicenseAssignmentStates | Format-List State, Error, AssignedByGroup State : Active Error : AssignedByGroup : 7023a314-6148-4d7b-b33f-6c775572879a

That is the loop closed: the tenant-wide scan is clean, and the one user who was silently missing services for three weeks now has the licence provisioning correctly, still inherited from the group as intended. Run the script on a schedule and a future CountViolation the day you run out of seats becomes something you catch in an overnight report — not something a user catches for you a fortnight later.

References

Have you ever actually queried licenseAssignmentStates across your whole tenant, or are you assuming group-based licensing is healthy because nobody has complained this week? Run the script, compare it against the Errors & issues tab for one product, and see how many silent failures turn up — I would genuinely like to know how common the "assigned but broken" state is out there.

Share this post
LinkedIn X / Twitter Reddit Bluesky

More from EndpointWeekly

Entra ID
Zero Reviewers Responded, So Entra Auto-Approved a Departed Admin
An Entra access review whose deadline passes with no responses can auto-apply a default…
Entra ID
A Lapsed Entra ID P2 License Doesn't Pause PIM — It Deletes…
When your Entra ID P2 or Governance license lapses, Conditional Access policies freeze in…
Entra ID
Your Entra Cloud Sync Job Has Been Failing Quietly for Three…
Hybrid sync errors pile up silently until a helpdesk ticket arrives weeks later. Here's…