Microsoft 365 administration

Audit Microsoft 365 Group-Based Licensing with Microsoft Graph PowerShell

Build a read-only Microsoft Graph PowerShell audit that separates direct and group-based licence assignments, resolves source groups, exposes processing errors, and flags overlapping sources.

Article author
Axeti Editorial
Article reading time
14 min read
Article publication date
August 18, 2026

The practical answer

Use licenseAssignmentStates when you need to explain not only which SKU a user has, but who or what assigned it, whether the licensing service processed it successfully, which service plans are disabled, and when the state last changed. The report below creates one row per user, SKU, and assignment source without modifying the tenant.

Explain every source

Separate direct assignments from group-based assignments and resolve each source group to a readable name.

Find processing failures

Surface ActiveWithError and Error states together with the licensing-service error returned by Microsoft Graph.

Detect assignment drift

Flag users who receive the same SKU from several groups or from a group and a direct assignment.

Why assignedLicenses is not enough

The assignedLicenses collection is useful for checking the effective licence SKUs on a user, but it is not a complete provenance record. If the same SKU is inherited from multiple groups, or inherited from a group while also assigned directly, an effective-licence view can hide the operational complexity.

The licenseAssignmentStates property provides the missing detail. Microsoft Graph returns assignedByGroup, skuId, disabledPlans, state, error, and lastUpdatedDateTime for each current assignment source. This is a snapshot of current states, not a historical event log.

Prerequisites and permissions

Install the Microsoft Graph PowerShell SDK with Install-Module Microsoft.Graph -Scope CurrentUser. For an interactive delegated audit, request User.Read.All, Group.Read.All, and LicenseAssignment.Read.All. Tenant policy may require administrator consent, and the signed-in account must hold a directory role allowed to read the relevant directory and licensing information.

  • User.Read.All retrieves users and the non-default licenseAssignmentStates property.
  • Group.Read.All resolves source group IDs to readable display names.
  • LicenseAssignment.Read.All reads subscribed SKUs so the export can show skuPartNumber instead of only a GUID.

Use a dedicated read-only audit identity where possible and request only the permissions the report needs. Group-based licensing also requires an eligible entitlement for every benefiting user and enough product licences for the unique users covered by the licensed groups.

Important design limits

Nested groups do not extend licence assignment

Licences assigned to a parent group are not applied through nested-group membership. Audit direct membership in the licensed group; do not assume that a nested child group passes the licence to its users.

A direct assignment is not automatically a mistake

Direct assignments may be valid for temporary access, specialist add-ons, pilot users, break-glass processes, or documented exceptions. The audit should identify unexplained direct assignments instead of blindly eliminating all of them.

Duplicate sources do not necessarily consume duplicate seats

A user can receive the same SKU from several assignment sources. This is usually a governance and troubleshooting concern, not proof that two product seats are billed for the same person. Review source groups, service-plan configuration, and commercial counts separately.

What the enhanced report contains

  • User display name, UPN, object ID, and account state.
  • SKU part number and SKU ID.
  • Direct or group-based assignment type and source group.
  • Assignment state, Graph error value, and disabled service-plan IDs.
  • Last update timestamp, overlap flag, and a summary of every source for the same user and SKU.

PowerShell audit script

The following script is read-only. It resolves only groups that actually occur as licence sources, retains unresolved IDs, and creates one CSV row per user, SKU, and assignment source.

# Read-only Microsoft 365 group-based licensing audit
# Requires: Install-Module Microsoft.Graph -Scope CurrentUser

$requiredScopes = @(
    "User.Read.All"
    "Group.Read.All"
    "LicenseAssignment.Read.All"
)

Connect-MgGraph -Scopes $requiredScopes -NoWelcome

$skuLookup = @{}
Get-MgSubscribedSku -All | ForEach-Object {
    $skuLookup[$_.SkuId.ToString()] = $_.SkuPartNumber
}

$users = Get-MgUser -All -Property @(
    "Id"
    "DisplayName"
    "UserPrincipalName"
    "AccountEnabled"
    "LicenseAssignmentStates"
)

$sourceGroupIds = $users.LicenseAssignmentStates |
    Where-Object { -not [string]::IsNullOrWhiteSpace($_.AssignedByGroup) } |
    ForEach-Object { $_.AssignedByGroup } |
    Sort-Object -Unique

$groupLookup = @{}
foreach ($groupId in $sourceGroupIds) {
    try {
        $group = Get-MgGroup -GroupId $groupId -Property "Id,DisplayName" -ErrorAction Stop
        $groupLookup[$groupId] = $group.DisplayName
    }
    catch {
        $groupLookup[$groupId] = $null
    }
}

$report = foreach ($user in $users) {
    $assignments = @($user.LicenseAssignmentStates)

    foreach ($assignment in $assignments) {
        $skuId = $assignment.SkuId.ToString()
        $sourceGroupId = $assignment.AssignedByGroup
        $assignmentType = if ([string]::IsNullOrWhiteSpace($sourceGroupId)) {
            "Direct"
        } else {
            "Group-based"
        }

        $sameSkuAssignments = @(
            $assignments | Where-Object { $_.SkuId.ToString() -eq $skuId }
        )

        $sourceSummary = $sameSkuAssignments | ForEach-Object {
            if ([string]::IsNullOrWhiteSpace($_.AssignedByGroup)) {
                "Direct"
            } elseif ($groupLookup[$_.AssignedByGroup]) {
                "Group: $($groupLookup[$_.AssignedByGroup])"
            } else {
                "Group ID: $($_.AssignedByGroup)"
            }
        }

        $sourceGroupName = if ($sourceGroupId) {
            $groupLookup[$sourceGroupId]
        } else {
            $null
        }

        $sourceResolution = if (-not $sourceGroupId) {
            "Not applicable"
        } elseif ($sourceGroupName) {
            "Resolved"
        } else {
            "Unresolved"
        }

        [PSCustomObject]@{
            DisplayName           = $user.DisplayName
            UserPrincipalName     = $user.UserPrincipalName
            UserId                = $user.Id
            AccountEnabled        = $user.AccountEnabled
            LicenseSku            = $skuLookup[$skuId]
            LicenseSkuId          = $skuId
            AssignmentType        = $assignmentType
            SourceGroupName       = $sourceGroupName
            SourceGroupId         = $sourceGroupId
            SourceResolution      = $sourceResolution
            AssignmentState       = $assignment.State
            AssignmentError       = $assignment.Error
            DisabledPlanIds       = ($assignment.DisabledPlans | ForEach-Object { $_.ToString() }) -join ";"
            LastUpdatedDateTime   = $assignment.LastUpdatedDateTime
            SourcesForSameSku     = $sameSkuAssignments.Count
            HasOverlappingSources = ($sameSkuAssignments.Count -gt 1)
            SameSkuSourceSummary  = ($sourceSummary -join " | ")
        }
    }
}

$outputPath = Join-Path -Path (Get-Location) -ChildPath "m365-group-license-audit.csv"

$report |
    Sort-Object AssignmentState, AssignmentError, LicenseSku, UserPrincipalName, AssignmentType |
    Export-Csv -Path $outputPath -NoTypeInformation -Encoding UTF8

Write-Host "Export complete: $outputPath"
Write-Host "Rows: $($report.Count)"
Write-Host "Errors: $(@($report | Where-Object AssignmentError).Count)"
Write-Host "Direct assignments: $(@($report | Where-Object AssignmentType -eq 'Direct').Count)"
Write-Host "Overlapping sources: $(@($report | Where-Object HasOverlappingSources).Count)"
Write-Host "Unresolved groups: $(@($report | Where-Object SourceResolution -eq 'Unresolved').Count)"

How the script works

1. Build a SKU lookup

Graph returns licence SKUs as GUIDs. Get-MgSubscribedSku supplies the tenant's skuPartNumber, which is easier to filter and discuss. The report keeps the GUID because it is the stable identifier used by Graph.

2. Request licenseAssignmentStates explicitly

Get-MgUser returns only a default subset of user properties. The script explicitly requests LicenseAssignmentStates; omitting it can produce an empty report even when users are licensed.

3. Resolve only source groups in use

Large tenants may contain thousands of groups. The script collects unique assignedByGroup values and resolves only those IDs. If a group cannot be resolved, the ID remains in the report and the source is marked Unresolved.

4. Detect overlapping sources

More than one assignment state for the same user and SKU sets HasOverlappingSources to True. The summary then shows whether the overlap is direct plus group-based, or several group sources.

Reading the output

AssignmentType = Direct

Start with direct assignments when the tenant is intended to use role-based groups. Ask for an owner and business reason. If it is a valid exception, document a review date. If it is legacy drift, confirm that a replacement group assignment is active before removing it.

HasOverlappingSources = True

Review whether the overlap is intentional. Two role groups may legitimately provide the same SKU, but different disabled-plan configurations can make the effective result difficult to explain. Record the authoritative group for each role and simplify sources where this is safe.

SourceResolution = Unresolved

An unresolved ID can mean the group no longer exists, the audit identity cannot read it, or directory replication has not completed. Do not classify the assignment as orphaned until an appropriately authorised directory reader verifies the object.

AssignmentState = ActiveWithError or Error

Prioritise these rows. Microsoft Graph documents CountViolation, MutuallyExclusiveViolation, DependencyViolation, ProhibitedInUsageLocationViolation, UniquenessViolation, and Other. The CSV identifies the user, SKU, source, and state; confirm the precise remediation in the Microsoft 365 admin centre and current Microsoft documentation before changing a licence.

DisabledPlanIds

The same product can be assigned with different service plans disabled. For a deeper audit, build a service-plan lookup from Get-MgSubscribedSku.ServicePlans and add readable plan names. This is especially useful when overlapping groups configure the same SKU differently.

Useful follow-up filters

# Licensing-service failures
$report | Where-Object {
    $_.AssignmentState -in @("ActiveWithError", "Error") -or $_.AssignmentError
}

# Direct assignments for enabled accounts
$report | Where-Object {
    $_.AssignmentType -eq "Direct" -and $_.AccountEnabled
}

# Same user and SKU assigned by more than one source
$report | Where-Object HasOverlappingSources

# Source groups that could not be resolved
$report | Where-Object SourceResolution -eq "Unresolved"

A practical remediation workflow

  1. Export and preserve the raw report with a timestamp.
  2. Triage Error and ActiveWithError rows before governance cleanup.
  3. Confirm available licence capacity and user usage locations.
  4. Give every valid direct-assignment exception an owner and review date.
  5. Review overlaps, especially where groups disable different service plans.
  6. Add users to the destination group and confirm the assignment is active before removing the old source.
  7. Re-export after changes, compare exception counts, and schedule a recurring read-only audit.

Microsoft's current guidance for moves between licensed groups is destination first: add the user to the new group, confirm that the licence is applied, and only then remove the user from the original group. Removing the source first can temporarily remove licensed services while processing catches up.

What this report does not prove

  • It does not prove that every assignment is contractually compliant.
  • It does not calculate invoices or duplicate commercial billing.
  • It does not validate whether users should access every workload in the SKU.
  • It is not a historical timeline and does not replace product terms or access reviews.

Recommended operating model

Run the export monthly and after major identity or licensing changes. Track errors, undocumented direct assignments, overlapping sources, unresolved groups, and disabled or departed accounts that retain active assignments. The target is not necessarily zero direct assignments or zero overlaps. The target is that every exception has an owner, a reason, and a review date—and that no licensing error remains invisible.

Microsoft sources

Related Axeti guides

faq

Microsoft 365 group-based licensing audit questions

Why not audit assignedLicenses only?

Expand options

assignedLicenses shows the effective SKUs on a user, but it does not fully explain whether each licence came from a direct assignment, one group, or several groups. licenseAssignmentStates provides the source and processing state.

Does the script change any Microsoft 365 licences?

Expand options

No. The script only reads users, source groups, subscribed SKUs, and licence assignment states, then exports the results to a CSV file.

Which Microsoft Graph permissions are required?

Expand options

The interactive script requests User.Read.All, Group.Read.All, and LicenseAssignment.Read.All. Tenant policy may require administrator consent and a suitable directory reader role.

What does ActiveWithError mean?

Expand options

The licence is active, but Microsoft Graph reports a processing problem for part of the assignment. Review the error value, affected service plans, capacity, usage location, and conflicting products.

Are overlapping assignment sources always wrong?

Expand options

No. Several groups can intentionally provide the same SKU. The overlap becomes risky when owners cannot explain it or when sources use different disabled-plan configurations.

Do nested groups inherit group-based licences?

Expand options

No. A licence assigned to a parent group is not applied through nested-group membership. Audit direct membership in the licensed group.

How often should the audit run?

Expand options

Run it monthly and after major identity, group, or licensing changes. Track errors, undocumented direct assignments, overlaps, unresolved groups, and disabled accounts with active assignments.

Knowledge center

Related Microsoft 365 licensing guides

Practical Microsoft 365 licensing guidance for governance, migrations, and cost control.

Axeti Microsoft 365 licensing specialist in an office
Article author
Anna Becker
Article reading time
8 min read
Article publication date
June 23, 2026

Moving from E3 to Business Premium When Group Licensing Fails

Diagnose failed Microsoft 365 group-based licence transitions from E3 to Business Premium while protecting user access and service continuity.

Read article →
Axeti Microsoft 365 licensing specialist in an office
Article author
Anna Becker
Article reading time
8 min read
Article publication date
June 23, 2026

Microsoft Licensing Governance for Enterprise IT

Build a clear governance model for Microsoft licensing across users, roles, agreements, exceptions, and cloud services.

Read article →
Axeti Microsoft 365 licensing specialist in an office
Article author
Anna Becker
Article reading time
7 min read
Article publication date
June 23, 2026

Microsoft 365 License Mix Guide for Enterprises

Create a practical Microsoft 365 licence mix by matching E3, E5, F3, Office 365 and add-ons to real user roles and service needs.

Read article →
Axeti Microsoft 365 licensing specialist in an office
Article author
Anna Becker
Article reading time
7 min read
Article publication date
June 23, 2026

Microsoft 365 E3 vs E5 vs E5 Add-ons

Compare Microsoft 365 E3, E5 and security and compliance add-ons to decide which capabilities belong in the base plan and which should be assigned selectively.

Read article →
Previous customer story
Next customer story

Secure the best pricing for Microsoft 365 licensing

Secure the best pricing for M365