PowerShell automation for Microsoft 365 licensing
Microsoft 365 licensing becomes difficult when tenant growth, staff movement and changing subscription bundles turn manual administration into a daily chore. PowerShell provides a repeatable way to assign products, remove obsolete entitlements and produce reports that explain who has access to what.
The modern approach uses the Microsoft Graph PowerShell SDK rather than older MSOnline or AzureAD modules. Graph exposes subscription SKUs, user license details, group-based licensing information and assignment errors through an automation-friendly interface.
For Australian organisations, licensing also needs to fit local operating practices. A Sydney or Melbourne service desk may manage users across Brisbane, Perth and regional offices, while subscription purchases are often handled through a Microsoft partner or CSP with Australian GST and billing requirements.
A sound script should be safe to run during an arvo maintenance window, record every change and account for privacy obligations. It should also cope with hybrid identities, where a user is synchronised from on-premises Active Directory but receives cloud licensing in Microsoft 365.
Connect to Microsoft Graph securely
Install the Microsoft Graph module on an administration workstation or automation worker:
Install-Module Microsoft.Graph -Scope CurrentUser
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Identity.DirectoryManagement
For interactive administration, connect with delegated permissions:
Connect-MgGraph -Scopes `
"User.ReadWrite.All",
"Directory.Read.All",
"Organization.Read.All"
A production runbook should use an app registration with certificate authentication, a managed identity or another non-interactive method. Grant only the required application permissions, protect the certificate in a suitable vault and keep the tenant ID and application ID outside the script.
Check the active context before making changes:
Get-MgContext | Select-Object TenantId, Scopes, AuthType
Using the Graph SDK also makes it easier to standardise automation across Microsoft 365 and Azure. It is a better long-term foundation than building new workflows around retired or deprecated modules.
Discover SKUs and prepare users
Start by retrieving the subscriptions available in the tenant. The SKU part number is readable, while the SKU ID is the GUID required by assignment commands:
$skus = Get-MgSubscribedSku -All
$skus |
Select-Object SkuPartNumber, SkuId, ConsumedUnits, PrepaidUnits
Common values include ENTERPRISEPACK for Office 365 E3 and SPE_E5 for Microsoft 365 E5, although available products vary by agreement and market. Do not hard-code assumptions about Australian plans, especially when a CSP has supplied a mixture of commercial, education or frontline subscriptions.
Microsoft Graph generally requires UsageLocation before a user can receive a licence. Set it from an authoritative HR or identity source rather than guessing from a domain name:
$user = Get-MgUser -UserId "alex.tan@example.com" `
-Property Id,DisplayName,UserPrincipalName,UsageLocation
if ([string]::IsNullOrWhiteSpace($user.UsageLocation)) {
Update-MgUser -UserId $user.Id -UsageLocation "AU"
}
The location code is not a physical office locator. It determines service availability, taxation treatment and licence eligibility, so an employee working from a Perth home office can still correctly use AU.
Assign licences with guardrails
Resolve the target SKU once, then check the user’s current assignments before adding anything:
$sku = $skus | Where-Object SkuPartNumber -eq "ENTERPRISEPACK"
$user = Get-MgUser -UserId "alex.tan@example.com" `
-Property Id,DisplayName,AssignedLicenses
$alreadyAssigned = $user.AssignedLicenses |
Where-Object SkuId -eq $sku.SkuId
if (-not $alreadyAssigned) {
Set-MgUserLicense -UserId $user.Id `
-AddLicenses @(@{SkuId = $sku.SkuId}) `
-RemoveLicenses @()
}
The idempotent check prevents a rerun from producing unnecessary changes. In a larger script, add -WhatIf-style preview logic, structured logging and an approval file containing the proposed user, SKU and action. This is particularly useful when a contractor population changes quickly or when a Canberra department has stricter change-control requirements.
For bulk assignment, import a CSV containing user principal names and a plan name, validate every row, then process in small batches. Catch Graph exceptions individually so one invalid account does not hide successful assignments. Expect throttling in a large tenant and add exponential back-off for HTTP 429 responses.
Group-based licensing is often preferable when membership represents a stable business rule, such as “all finance employees receive Microsoft 365 E3”. Direct assignment remains useful for exceptions, temporary access and service accounts, but it can become difficult to audit.
Generate practical licence reports
A useful report shows both the purchased capacity and the people consuming it:
$report = foreach ($sku in Get-MgSubscribedSku -All) {
[pscustomobject]@{
Product = $sku.SkuPartNumber
SkuId = $sku.SkuId
Purchased = $sku.PrepaidUnits.Enabled
Assigned = $sku.ConsumedUnits
Available = $sku.PrepaidUnits.Enabled - $sku.ConsumedUnits
WarningLevel = $sku.PrepaidUnits.Warning
}
}
$report | Export-Csv ".\m365-license-capacity.csv" -NoTypeInformation
For user-level reporting, request only the properties needed and expand assigned SKU IDs into readable product names:
$skuMap = @{}
Get-MgSubscribedSku -All | ForEach-Object {
$skuMap[$_.SkuId.Guid] = $_.SkuPartNumber
}
$users = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,AccountEnabled,AssignedLicenses
$userReport = foreach ($person in $users) {
[pscustomobject]@{
DisplayName = $person.DisplayName
UserPrincipalName = $person.UserPrincipalName
Enabled = $person.AccountEnabled
Licences = ($person.AssignedLicenses.SkuId |
ForEach-Object { $skuMap[$_.Guid] }) -join "; "
}
}
$userReport | Export-Csv ".\m365-user-licenses.csv" -NoTypeInformation
Treat exported reports as sensitive information. Store them in restricted locations, apply retention rules and avoid sending full user lists through ordinary email. A daily scheduled report can highlight disabled accounts that still consume subscriptions, unlicensed active users and products approaching capacity.
Operate licensing as a controlled service
Automation should include audit trails with timestamps, operator or application identity, target account, old state, new state and Graph error details. Send failures to a monitored queue rather than silently writing them to a local console. A script that runs successfully but skips 30 users is operationally unsuccessful.
In hybrid environments, separate identity synchronisation from cloud licensing. Confirm that the source account is active, wait for a synchronisation cycle where necessary and then apply the cloud-side entitlement. When troubleshooting a broader platform issue, infrastructure dependencies matter too; the distributed switch guide is a useful reference when virtual network behaviour affects automation hosts or hybrid services.
Choose an assignment method according to how stable the rule is and how much exception handling the service desk needs:
| Assignment method | Best fit | Strength | Main risk |
|---|---|---|---|
| Direct Graph assignment | Individual exceptions and short-term access | Precise and easy to automate | Assignment drift |
| Group-based licensing | Departments and repeatable role rules | Centralised membership control | Incorrect group membership |
| Scheduled PowerShell | HR-driven joiner, mover and leaver workflows | Flexible validation and reporting | Script and credential maintenance |
| Manual admin centre changes | Small, infrequent changes | Minimal initial setup | Weak repeatability and auditability |
A mature workflow combines these methods: groups for standard roles, PowerShell for reconciliation and reporting, and direct assignment only where a documented exception exists. This keeps Microsoft 365 licensing predictable across Australian offices, remote workers and partner-managed subscriptions.