Microsoft Graph PowerShell: 12 commands for users and licenses

Inventory users, distinguish direct and inherited licenses, investigate assignment errors, and prepare two pilot changes with permissions and rollback.

01

12 useful recipes, not a complete audit

A visible license does not always explain its assignment source or why a service fails to provision. These 12 PowerShell recipes connect users, SKUs, service plans, and groups. The first ten read data; the last two illustrate individual changes behind a disabled execution switch.

Run them in order in a dedicated session. All identifiers are fictional. This targeted check measures neither actual service usage nor overall Microsoft 365 security and does not replace a complete assessment.

02

Prepare PowerShell, identity, and permissions

Use stable Microsoft Graph modules with v1.0 APIs, not legacy AzureAD/MSOnline. PowerShell 7 is recommended. Install submodules in an approved setup step, pin tested versions, and record loaded versions; automatically updating modules is not a collection prerequisite.

This is an interactive delegated workflow: named administrative account, MFA, and correct tenant. OAuth scopes, admin consent, and Entra roles are separate controls. Start with necessary reads. Directory Readers is supported for SKU and license-detail reads; some operations require specific roles.

Setup — connection, no user changes
# PowerShell 7; approved, tested module versions
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Users.Actions
Import-Module Microsoft.Graph.Groups
Import-Module Microsoft.Graph.Identity.DirectoryManagement
Get-Module Microsoft.Graph.* | Select-Object Name,Version
$tenantId = '33333333-3333-3333-3333-333333333333' # fictional tenant
Connect-MgGraph -TenantId $tenantId -Scopes 'User.Read.All','LicenseAssignment.Read.All' -ContextScope Process
Get-MgContext | Select-Object TenantId,Account,AuthType,Scopes
# Only for checks 8–10, reconnect with approved group read scopes:
# Connect-MgGraph -TenantId $tenantId -Scopes 'User.Read.All','LicenseAssignment.Read.All','Group.Read.All','GroupMember.Read.All' -ContextScope Process
03

1. Inventory users with useful properties

Delegated permission : User.Read.All.

-All follows pages; -Property requests fields absent by default. Large tenants need planned collection and throttling recovery. Inventory is a snapshot, not an activity log.

Read-only
$users = @(Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,UserType,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop)
$users | Select-Object Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,UserType
04

2. Inspect one precise pilot account

Delegated permission : User.Read.All.

Prefer object ID to avoid ambiguity after renaming. Confirm UPN and owner before changing anything. A field omitted from -Property cannot be assumed empty in Entra.

Read-only
$userId = '11111111-1111-1111-1111-111111111111' # fictional OBJECT ID
$user = Get-MgUser -UserId $userId -Property Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop
$user | Select-Object Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,AssignedLicenses
05

3. Find disabled accounts with assigned licenses

Delegated permission : User.Read.All.

This local selection reuses check 1. Disabled users may need licenses for data, retention, or business handover. Validate the requirement; never remove licenses based solely on this filter.

Read-only
$users | Where-Object { $_.AccountEnabled -eq $false -and @($_.AssignedLicenses).Count -gt 0 } |
  Select-Object Id,UserPrincipalName,UsageLocation,
    @{Name='SkuIds';Expression={($_.AssignedLicenses.SkuId -join ';')}}
06

4. List subscribed SKUs and counters

Delegated permission : LicenseAssignment.Read.All.

Use SkuId for operations and SkuPartNumber for identification; verify commercial names in Microsoft's mapping. Counters do not replace contract terms or reserve capacity. Retain subscription status.

Read-only
$skus = @(Get-MgSubscribedSku -All -ErrorAction Stop)
$skus | Select-Object SkuId,SkuPartNumber,CapabilityStatus,ConsumedUnits,
  @{Name='EnabledUnits';Expression={$_.PrepaidUnits.Enabled}}
07

5. Read a user's license and service-plan details

Delegated permission : LicenseAssignment.Read.All.

Inspect each plan's ProvisioningStatus. A present license does not imply every service is available. This endpoint is used in delegated mode here; do not assume app-only support. Directory Readers is among supported roles for this read.

Read-only
Get-MgUserLicenseDetail -UserId $userId -All -ErrorAction Stop |
  Select-Object SkuId,SkuPartNumber,ServicePlans |
  ConvertTo-Json -Depth 10
08

6. Separate direct and group assignment

Delegated permission : User.Read.All.

Null AssignedByGroup indicates direct assignment; otherwise resolve the group ID. One SKU can have several assignment paths. AssignedLicenses alone does not identify the source or policy needing correction.

Read-only
$user.LicenseAssignmentStates |
  Select-Object SkuId,AssignedByGroup,State,Error,DisabledPlans,LastUpdatedDateTime
09

7. Isolate assignment errors

Delegated permission : User.Read.All.

Correlate state and code: capacity, plan dependencies, incompatible offers, or usageLocation may matter. Do not repeatedly reassign without fixing the cause. No snapshot error does not prove application availability.

Read-only
foreach ($u in $users) {
  foreach ($a in $u.LicenseAssignmentStates) {
    if ($a.State -in @('Error','ActiveWithError') -or
        ($a.Error -and $a.Error -ne 'None')) {
      [pscustomobject]@{
        UserId=$u.Id; UPN=$u.UserPrincipalName; SkuId=$a.SkuId
        GroupId=$a.AssignedByGroup; State=$a.State; Error=$a.Error
      }
    }
  }
}
10

8. Inventory groups carrying licenses

Delegated permission : Group.Read.All.

The group may be the correct remediation unit. A change may affect every member. Check dynamic rules, disabled plans, and licensing requirements for group-based licensing and dynamic membership; Graph output alone does not establish entitlement.

Read-only
$groups = @(Get-MgGroup -All -Property Id,DisplayName,GroupTypes,AssignedLicenses,MembershipRule,MembershipRuleProcessingState -ErrorAction Stop)
$groups | Where-Object { @($_.AssignedLicenses).Count -gt 0 } |
  Select-Object Id,DisplayName,GroupTypes,AssignedLicenses,MembershipRule,MembershipRuleProcessingState |
  ConvertTo-Json -Depth 10
11

9. List a pilot group's direct members

Delegated permission : GroupMember.Read.All.

Results may contain multiple object types. These are direct members; do not assume transitive expansion or licensing through nested groups. Hidden membership requires additional justified permission.

Read-only
$groupId = '22222222-2222-2222-2222-222222222222' # fictional group ID
Get-MgGroupMember -GroupId $groupId -All -ErrorAction Stop |
  Select-Object Id,@{Name='Type';Expression={$_.AdditionalProperties['@odata.type']}}
12

10. List the pilot user's direct memberships

Delegated permission : User.Read.All.

Objects may include groups and directory roles. Resolve only needed IDs with appropriate permission; limited details do not mean the object is absent. Compare groups against check 6 sources before removing direct assignments.

Read-only
Get-MgUserMemberOf -UserId $userId -All -ErrorAction Stop |
  Select-Object Id,@{Name='Type';Expression={$_.AdditionalProperties['@odata.type']}}
13

Before changes: reduce the blast radius

Obtain business-owner approval, confirm license entitlements, and choose one nonprivileged pilot user. Export object ID, location, SKUs, DisabledPlans, and every assignment source to a restricted location. Exports contain personal data: restrict access and define retention.

Reading Graph does not grant product entitlements. Check offer and group-licensing conditions, especially for dynamic groups. Inherited licensing is asynchronous; nested groups do not distribute licenses to indirect members. Do not widen permissions merely to bypass a 403.

14

11. Correct the pilot's UsageLocation

Explicit change: User.ReadWrite.All and a role authorized to update this user, such as User Administrator for this nonadmin pilot. UsageLocation is a two-letter ISO country code, not an address or language preference. Confirm the applicable country; CA is only an example.

The example rejects an initially missing value: this property is not nullable, so setting it back to null is not a valid rollback. The false switch only displays the proposal. Preserve the previous value outside the session before authorizing the write.

Pilot change — disabled by default
# CHANGE: existing location correction on a non-admin pilot
# Separate approved write session; refresh check 2 first
Connect-MgGraph -TenantId $tenantId -Scopes 'User.ReadWrite.All' -ContextScope Process
$before = Get-MgUser -UserId $userId -Property Id,UserPrincipalName,UsageLocation -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($before.UsageLocation)) {
  throw 'Missing previous location: define an initial-population recovery plan first.'
}
$previousLocation = $before.UsageLocation
$applyLocation = $false
[pscustomobject]@{User=$before.UserPrincipalName; Before=$previousLocation; After='CA'}
if ($applyLocation) {
  Update-MgUser -UserId $userId -UsageLocation 'CA' -ErrorAction Stop
  Get-MgUser -UserId $userId -Property Id,UserPrincipalName,UsageLocation -ErrorAction Stop
}
# Approved rollback: Update-MgUser -UserId $userId -UsageLocation $previousLocation
15

12. Add a new direct license to the pilot

Explicit change: LicenseAssignment.ReadWrite.All, plus User.Read.All for the reread, and an authorized License Administrator role. Choose a real SkuId from check 4; verify capacity, dependencies, country, and business need. The guard rejects an existing SKU regardless of assignment source.

Empty DisabledPlans enables all available plans in this assignment; approve that selection before switching to true. Empty RemoveLicenses removes no licenses. HTTP success does not guarantee completed provisioning: repeat checks 5, 6, and 7 and test the intended service.

New pilot assignment — disabled by default
# CHANGE: new direct assignment, never a bulk removal
Connect-MgGraph -TenantId $tenantId -Scopes 'User.Read.All','LicenseAssignment.ReadWrite.All' -ContextScope Process
$skuId = [guid]'44444444-4444-4444-4444-444444444444' # replace from check 4
$baseline = Get-MgUser -UserId $userId -Property Id,UserPrincipalName,UsageLocation,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($baseline.UsageLocation)) { throw 'Confirm usage location first.' }
if (@($baseline.AssignedLicenses | Where-Object { $_.SkuId -eq $skuId }).Count -gt 0) {
  throw 'SKU already assigned: inspect source and plans; do not overwrite.'
}
$applyLicense = $false
$approvedDisabledPlans = @() # explicitly enables all available plans: review first
[pscustomobject]@{User=$baseline.UserPrincipalName; SkuId=$skuId; DisabledPlans=$approvedDisabledPlans}
if ($applyLicense) {
  Set-MgUserLicense -UserId $userId -AddLicenses @(@{
    SkuId=$skuId; DisabledPlans=$approvedDisabledPlans
  }) -RemoveLicenses @() -ErrorAction Stop
}
16

Validate, troubleshoot, and roll back

  • Recollect the pilot after processing. Compare assignment sources and plans with baseline, then validate sign-in and the intended service with its owner.
  • For 403, separate consent, scope, role, and administrative scope. For 404, check object ID and tenant. For 429, honor Retry-After and space collection; do not run aggressive loops.
  • For licensing errors, inspect capacity, plan dependencies, and geographic availability. Record outcomes and operational timestamps without exposing data in public tickets.
  • Location rollback: restore the recorded value if still correct. License rollback: consider removing only the new direct assignment created by this pilot, after business and data-retention validation. No automatic removal is provided.
  • Manage inherited assignments at their source; group changes can affect every member. Reassigning a license does not restore already deleted data.
  • Finish with Disconnect-MgGraph. This closes the session but does not revoke previously granted OAuth consent. Review unnecessary permissions separately.
17

Turn collection into an action plan

A useful result has four columns: observation, impact, owner, next validation. For example, a pilot capacity error becomes an entitlement and business-need review, not license removal elsewhere. Microsoft 365 consulting can help standardize permissions, groups, and account lifecycle beyond these commands.