Short answer: MSOnline no longer works and AzureAD is on the same path, so move your scripts to Microsoft Graph PowerShell. Install it with Install-Module Microsoft.Graph -Scope CurrentUser, connect with Connect-MgGraph and only the scopes the script needs, then swap each MSOnline or AzureAD cmdlet for its Graph equivalent using the cmdlet map below.
Updated: October 2026
This guide replaces our original “How to install the Azure Active Directory PowerShell Module” article. That article installed the MSOnline module and connected with Connect-MsolService. Microsoft has retired MSOnline, so the old steps no longer work. The new version installs Microsoft Graph PowerShell, signs in with MFA and least-privilege permissions, and maps the old cmdlets to their replacements.
A call we get a lot goes like this: “The monthly licence report has run for years, and now it just fails at the first line.” The first line is almost always Connect-MsolService. Microsoft switched the MSOnline module off for every tenant in 2025, and the AzureAD module is on the same path, so any script built on either one needs moving to Microsoft Graph PowerShell. The good news is that the move is mostly mechanical once the module is installed and you know which new cmdlet replaces which old one. This guide covers installing and connecting, a ready-to-run tenant snapshot script, and a lookup table for the MSOnline and AzureAD cmdlets our older articles used.
Not technical? Let us help.
The steps below are written for IT administrators. If you would rather have someone review and update your Microsoft 365 scripts for you, get in touch with our team.
Who this is for
- Microsoft 365 administrators who manage users, licences, domains or admin roles from PowerShell.
- Anyone following an older GCIT knowledge base article that says “install the Azure Active Directory PowerShell module” or starts with
Connect-MsolServiceorConnect-AzureAD. - IT teams with scheduled scripts that still store an admin username and password.
- Microsoft partners and MSPs whose scripts looped through customers with
Get-MsolPartnerContract.
What changed
Our original article told you to install MSOnline and connect like this:
# RETIRED: MSOnline no longer works in any Microsoft 365 tenant. Install-Module MSOnline Connect-MsolService Get-MsolUser -All
Three changes mean this no longer works:
- MSOnline is retired. Microsoft deprecated the MSOnline and AzureAD modules on 30 March 2024 and retired MSOnline on 30 May 2025. Scripts that use
Connect-MsolServiceor any other Msol cmdlet no longer run. Microsoft switched it off in stages from early April 2025, and it stopped working for all tenants by late May 2025. Sources: Azure AD PowerShell and MSOnline migration FAQ and the Microsoft Entra blog retirement notice. - AzureAD is being retired too. Microsoft announced that the AzureAD and AzureAD-Preview modules would be retired and stop working from mid-October 2025. If you moved from MSOnline to
Connect-AzureADa few years ago, those scripts need moving as well. Source: Important update: AzureAD PowerShell retirement. - The replacement uses different cmdlet names. Microsoft directs everyone still using MSOnline or AzureAD to the Microsoft Graph PowerShell SDK or Microsoft Entra PowerShell. Old scripts don’t run as-is, because the cmdlet names change (for example,
Get-AzureADUserbecomesGet-MgUser). Source: Migrate from Azure AD PowerShell to Microsoft Graph PowerShell.
You’ll also notice a name change. Microsoft renamed Azure Active Directory (Azure AD) to Microsoft Entra ID. It’s the same directory, and nothing needs to change in your tenant because of the new name.
Microsoft Graph PowerShell or Microsoft Entra PowerShell?
Microsoft offers two supported modules, and they work together:
- Microsoft Graph PowerShell (module
Microsoft.Graph) is the module Microsoft supports for working with Microsoft Entra ID and other Microsoft 365 services. Its cmdlets use theVerb-MgNounformat, such asGet-MgUser. This is what we use in this guide, and it’s the best fit for replacing MSOnline scripts. - Microsoft Entra PowerShell (module
Microsoft.Entra) is Microsoft’s newer, Entra-focused module. TheMicrosoft.Entramodule is the general availability (v1.0) release. It builds on, and is part of, the Microsoft Graph PowerShell SDK, so its cmdlets work alongside the Graph cmdlets in the same session. Microsoft says it is over 98% compatible with the AzureAD module (usingEnable-EntraAzureADAlias) and has over 80% parity with MSOnline cmdlets. Sources: What is Microsoft Entra PowerShell? and Install Microsoft Entra PowerShell.
Our recommendation: if you have a large library of Connect-AzureAD scripts, Microsoft Entra PowerShell’s compatibility mode is the fastest way to get them running again. For MSOnline scripts, and for anything new, write it with Microsoft Graph PowerShell. The alias feature covers AzureAD cmdlet names, not Msol ones, so MSOnline scripts need rewriting either way.
Prerequisites
- PowerShell: PowerShell 7 or later is Microsoft’s recommended version on all platforms and needs nothing extra. Windows PowerShell 5.1 also works, but needs .NET Framework 4.7.2 or later and an up-to-date PowerShellGet module.
- Execution policy: PowerShell must be allowed to run scripts. Microsoft’s documented setting is
RemoteSignedfor the current user. - An admin account protected by MFA: Use a dedicated admin account, not your everyday mailbox account. Give it the smallest role that does the job. For example, Global Reader for reporting, License Administrator for licence changes (this role can also set a user’s usage location) or User Administrator for user changes. Avoid Global Administrator for day-to-day scripting.
- Someone who can grant consent: The first time you request a permission scope, an administrator has to consent to it for your tenant. Microsoft notes that this typically needs an account with the Privileged Role Administrator role.
Step-by-step: install and connect
Step 1: Check your PowerShell version and execution policy
Open PowerShell (PowerShell 7 if you have it) and run:
$PSVersionTable.PSVersion Get-ExecutionPolicy
If the execution policy is Restricted or AllSigned, set it for your account only:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
If you’re on Windows PowerShell 5.1, update PowerShellGet first, then close and reopen PowerShell so the new version loads:
Install-Module PowerShellGet -Scope CurrentUser -Force
Step 2: Remove the retired modules (optional but recommended)
Leaving MSOnline and AzureAD installed won’t break anything, but removing them stops old scripts from half-running and makes it obvious which ones still need updating. Only remove the ones you have:
Get-InstalledModule -Name MSOnline, AzureAD, AzureADPreview -ErrorAction SilentlyContinue Uninstall-Module -Name MSOnline -AllVersions Uninstall-Module -Name AzureAD -AllVersions
Step 3: Install Microsoft Graph PowerShell
To install the full module for your account only (no admin rights needed), run:
Install-Module Microsoft.Graph -Scope CurrentUser -Repository PSGallery -Force
The full module pulls in more than 47 submodules, so it can take a while. If you only need user, licence and directory cmdlets, install just those submodules instead. Microsoft.Graph.Authentication provides Connect-MgGraph and is always needed:
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Repository PSGallery -Force Install-Module Microsoft.Graph.Users -Scope CurrentUser -Repository PSGallery -Force Install-Module Microsoft.Graph.Users.Actions -Scope CurrentUser -Repository PSGallery -Force Install-Module Microsoft.Graph.Identity.DirectoryManagement -Scope CurrentUser -Repository PSGallery -Force
To see every available submodule, run Find-Module Microsoft.Graph*. There is also a Microsoft.Graph.Beta module for preview features. Microsoft recommends relying on the v1.0 module for scripts, because beta functionality can change without notice.
Step 4: Verify the installation
Get-InstalledModule Microsoft.Graph Get-InstalledModule Microsoft.Graph.*
The version should match the latest release on the PowerShell Gallery. Keep it current with Update-Module Microsoft.Graph. Installing in PowerShell 7 doesn’t install it for Windows PowerShell 5.1 (or the other way around), so install it in each version you use.
Step 5: Connect with only the permissions you need
With MSOnline, connecting gave you everything your admin role allowed. Microsoft Graph PowerShell works differently: you ask for specific permission scopes when you connect, and the session can only do what those scopes allow. For a read-only look at users and licences:
Connect-MgGraph -Scopes "User.Read.All", "LicenseAssignment.Read.All" -TenantId "contoso.onmicrosoft.com" -ContextScope Process -NoWelcome
A sign-in window opens. Sign in with your admin account and complete MFA. If this is the first time anyone in the tenant has requested these scopes, you’ll be asked to consent. Delegated scopes never give you more than your own account can already do, so a Global Reader who requests a write scope still can’t make changes.
Be careful with the Consent on behalf of your organization box. Ticking it grants those scopes to the Microsoft Graph Command Line Tools app for every user in the tenant, limited only by each user’s own role. Leave it unticked for anything beyond read scopes. Better still, follow Microsoft’s steps to register a dedicated app for admin use, set Assignment required? to Yes, assign only your admins, and connect with Connect-MgGraph -ClientId <your app ID> -TenantId <tenant>. Microsoft describes this as the approach to use if you need to isolate and limit the consent permissions granted for Microsoft Graph PowerShell.
Useful options on Connect-MgGraph:
-TenantIdsets the tenant to sign in to, as a tenant ID or domain. Use it whenever your account can reach more than one tenant, so the session can’t land in the wrong one.-UseDeviceCodesigns in with a code on another device, for a server without a browser. Microsoft recommends blocking device code flow with Conditional Access wherever possible, because attackers use it in phishing. If your tenant blocks it, sign in from an admin workstation that has a browser, or use certificate authentication for scripts on servers (see Running scripts unattended below). Source: Block authentication flows with Conditional Access policy.-ContextScope Processkeeps the sign-in to the current PowerShell window only. By default, Microsoft Graph PowerShell caches your token so you stay signed in across PowerShell sessions until you runDisconnect-MgGraph.-NoWelcomehides the welcome banner, which keeps script output tidy.
Step 6: Check who and what you’re connected as
Get-MgContext Get-MgContext | Select-Object -ExpandProperty Scopes
Get-MgContext shows the tenant ID, the account, whether the session is delegated or app-only, and every scope you’ve consented to. Check it before running anything that makes changes.
Step 7: Find the right cmdlet and permission
Two built-in helpers save a lot of guesswork when you’re converting an old script:
# Which permissions does a cmdlet accept?
Find-MgGraphCommand -Command Get-MgUser | Select-Object -First 1 -ExpandProperty Permissions
# Which cmdlets call a given Microsoft Graph URL?
Find-MgGraphCommand -Uri "/users/{id}"
# Search for permissions by keyword
Find-MgGraphPermission user -PermissionType Delegated
Microsoft notes that the permissions Find-MgGraphCommand lists don’t indicate privilege levels. To pick the smallest permission, check the “least privileged” column on the cmdlet’s Microsoft Graph API page, or use the table further down this article.
Step 8: Disconnect when you’re finished
Disconnect-MgGraph
This clears the cached token for the current context, so the next session has to sign in again. Make it a habit, especially on shared admin machines.
Complete script: read-only tenant snapshot
This script replaces the “connect and have a quick look” commands from our older articles (Get-MsolCompanyInformation, Get-MsolDomain, Get-MsolAccountSku, Get-MsolUser -All, Get-MsolRoleMember and Get-MsolUser -ReturnDeletedUsers). It checks that the submodules it needs are installed (install them first with Step 3), signs in with read-only scopes, exports up to five CSV reports and signs out. It makes no changes to your tenant and installs nothing.
<#
.SYNOPSIS
Read-only Microsoft 365 tenant snapshot using Microsoft Graph PowerShell.
.DESCRIPTION
Replaces the MSOnline "quick look" commands used in older GCIT articles.
Checks that the Microsoft Graph PowerShell submodules it needs are installed,
signs in with MFA using read-only delegated scopes, exports CSV reports and signs out.
Makes no changes to the tenant and installs nothing.
.PARAMETER TenantId
Tenant ID or initial domain to sign in to, for example contoso.onmicrosoft.com
.PARAMETER OutputFolder
Folder for the CSV reports. Created if it doesn't exist.
.EXAMPLE
.\Get-GraphTenantSnapshot.ps1 -TenantId contoso.onmicrosoft.com
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$TenantId,
[string]$OutputFolder = (Join-Path -Path $HOME -ChildPath 'GraphTenantSnapshot')
)
$ErrorActionPreference = 'Stop'
# 1. Stop early if a Microsoft Graph PowerShell submodule this script uses is missing
$requiredModules = @(
'Microsoft.Graph.Authentication'
'Microsoft.Graph.Users'
'Microsoft.Graph.Identity.DirectoryManagement'
)
foreach ($moduleName in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $moduleName)) {
throw "Install $moduleName first (see Step 3 of the article)."
}
}
# 2. Sign in with read-only delegated scopes (least privileged for each call below)
$scopes = @(
'User.Read' # organisation details
'User.Read.All' # users and deleted users
'LicenseAssignment.Read.All' # subscribed licences
'Domain.Read.All' # domains and password expiry settings
'RoleManagement.Read.Directory' # directory roles and their members
)
Connect-MgGraph -TenantId $TenantId -Scopes $scopes -ContextScope Process -NoWelcome
try {
$context = Get-MgContext
Write-Host "Connected to tenant $($context.TenantId) as $($context.Account)"
if (-not (Test-Path -Path $OutputFolder)) {
New-Item -Path $OutputFolder -ItemType Directory | Out-Null
}
$stamp = Get-Date -Format 'yyyyMMdd-HHmm'
# Writes a CSV only when there is something to write, so an empty result doesn't leave a blank file
function Export-Report {
param($Data, [string]$Name)
$rows = @($Data)
if ($rows.Count -gt 0) {
$rows | Export-Csv -Path (Join-Path -Path $OutputFolder -ChildPath "$Name-$stamp.csv") -NoTypeInformation -Encoding UTF8
}
else {
Write-Host "No $Name rows found, so no $Name file was written."
}
}
# 3. Organisation (replaces Get-MsolCompanyInformation)
$org = Get-MgOrganization
Write-Host "Organisation: $($org.DisplayName)"
# 4. Domains and password expiry settings (replaces Get-MsolDomain and Get-MsolPasswordPolicy)
$domains = Get-MgDomain -All |
Select-Object Id, IsDefault, IsVerified, PasswordValidityPeriodInDays, PasswordNotificationWindowInDays
# 5. Licence summary (replaces Get-MsolAccountSku)
$skus = Get-MgSubscribedSku -All
$skuNames = @{}
$licences = foreach ($sku in $skus) {
$skuNames["$($sku.SkuId)"] = $sku.SkuPartNumber
[pscustomobject]@{
SkuPartNumber = $sku.SkuPartNumber
SkuId = $sku.SkuId
Purchased = $sku.PrepaidUnits.Enabled
Assigned = $sku.ConsumedUnits
Available = $sku.PrepaidUnits.Enabled - $sku.ConsumedUnits
}
}
# 6. Users with licences (replaces Get-MsolUser -All)
# -All returns every page. Without it, only the first page of users comes back.
$userProperties = 'Id', 'DisplayName', 'UserPrincipalName', 'AccountEnabled', 'UserType', 'UsageLocation', 'AssignedLicenses'
$users = Get-MgUser -All -Property $userProperties
$userReport = foreach ($user in $users) {
$licenceNames = foreach ($assigned in $user.AssignedLicenses) {
$skuNames["$($assigned.SkuId)"]
}
[pscustomobject]@{
DisplayName = $user.DisplayName
UserPrincipalName = $user.UserPrincipalName
AccountEnabled = $user.AccountEnabled
UserType = $user.UserType
UsageLocation = $user.UsageLocation
Licences = ($licenceNames -join ', ')
}
}
# 7. Global Administrators (replaces Get-MsolRole and Get-MsolRoleMember)
# 62e90394-69f5-4237-9190-012177145e10 is the built-in Global Administrator role template ID.
# Lists users, groups and service principals with an ACTIVE assignment.
# Eligible (PIM) assignments aren't included, so check those in Privileged Identity Management.
$globalAdminTemplateId = '62e90394-69f5-4237-9190-012177145e10'
$globalAdminRole = Get-MgDirectoryRole -All | Where-Object { $_.RoleTemplateId -eq $globalAdminTemplateId }
$globalAdmins = @()
if ($globalAdminRole) {
$globalAdmins = @(Get-MgDirectoryRoleMember -DirectoryRoleId $globalAdminRole.Id -All | ForEach-Object {
[pscustomobject]@{
Type = ($_.AdditionalProperties['@odata.type'] -replace '#microsoft.graph.', '')
DisplayName = $_.AdditionalProperties['displayName']
UserPrincipalName = $_.AdditionalProperties['userPrincipalName']
AppId = $_.AdditionalProperties['appId']
}
})
}
else {
Write-Warning 'The Global Administrator role was not returned, so the Global Admins count below is not reliable.'
}
# 8. Deleted users still recoverable (replaces Get-MsolUser -ReturnDeletedUsers)
$deletedUsers = Get-MgDirectoryDeletedItemAsUser -All -Property 'Id', 'DisplayName', 'UserPrincipalName', 'DeletedDateTime' |
Select-Object Id, DisplayName, UserPrincipalName, DeletedDateTime
# 9. Export the reports (an empty result writes no file)
Export-Report -Data $domains -Name 'Domains'
Export-Report -Data $licences -Name 'Licences'
Export-Report -Data $userReport -Name 'Users'
Export-Report -Data $globalAdmins -Name 'GlobalAdmins'
Export-Report -Data $deletedUsers -Name 'DeletedUsers'
Write-Host ""
Write-Host "Users: $(@($users).Count)"
Write-Host "Global Admins: $(@($globalAdmins).Count) active (users, groups and apps, not counting PIM eligible)"
Write-Host "Deleted users: $(@($deletedUsers).Count)"
$licences | Format-Table SkuPartNumber, Purchased, Assigned, Available -AutoSize
Write-Host "Reports saved to $OutputFolder"
}
finally {
# 10. Always sign out, even if something above failed
Disconnect-MgGraph | Out-Null
}
How to run it
- Save the script as
Get-GraphTenantSnapshot.ps1. - Open PowerShell 7 (or Windows PowerShell 5.1) and change to the folder where you saved it.
- If you haven’t already, install the three submodules it needs (
Microsoft.Graph.Authentication,Microsoft.Graph.UsersandMicrosoft.Graph.Identity.DirectoryManagement) as shown in Step 3. - Run it with your tenant’s initial domain or tenant ID:
.\Get-GraphTenantSnapshot.ps1 -TenantId contoso.onmicrosoft.com - Sign in with your admin account and complete MFA. A Global Reader account is enough, because every scope is read-only.
- If prompted, consent to the five permissions. They are all read-only. If your account can’t consent, ask a Privileged Role Administrator to sign in and consent first (see the consent note in Step 5).
- Open the CSV files in the output folder (by default, a
GraphTenantSnapshotfolder in your user profile).
Things worth checking in the results: more than two or three Global Administrators, licences with nothing left in the Available column, enabled accounts that shouldn’t be, and users without a UsageLocation (they can’t be licensed until it’s set). Any Global Administrator whose Type is servicePrincipal or group deserves a close look, because attackers use both to keep access. The report shows active assignments only, so also check eligible assignments in Privileged Identity Management.
The reports list your admins and users. Keep them somewhere with restricted access and delete them when you’re done.
MSOnline and AzureAD to Microsoft Graph PowerShell: cmdlet map
These are the cmdlets our older articles used most, with their Microsoft Graph PowerShell replacements from Microsoft’s official cmdlet map. The permission column shows the least privileged delegated scope Microsoft lists for each call. Two rows marked “GCIT” aren’t in Microsoft’s map, and the notes explain why.
| Old cmdlet (MSOnline or AzureAD) | Microsoft Graph PowerShell | Least privileged scope | Notes |
|---|---|---|---|
Connect-MsolService, Connect-AzureAD |
Connect-MgGraph |
Whatever the script needs | Pass the scopes with -Scopes. Use Disconnect-MgGraph in place of Disconnect-AzureAD. |
Get-MsolUser, Get-AzureADUser |
Get-MgUser |
User.ReadBasic.All (basic profile), User.Read.All (full profile) | Add -All to get every user. Many properties, such as UsageLocation and AccountEnabled, only come back when you name them in -Property. |
New-MsolUser |
New-MgUser |
User.Create | |
Set-MsolUser, Set-MsolUserPrincipalName |
Update-MgUser |
User.ReadUpdate.All | Some properties have their own narrower scope. For example, enabling or blocking sign-in (accountEnabled) uses User.EnableDisableAccount.All plus User.Read.All. |
Set-MsolUserPassword |
Reset-MgUserAuthenticationMethodPassword |
UserAuthenticationMethod.ReadWrite.All | This scope can also change every user’s MFA methods, so use it only in a short, process-scoped session. To set a new password, Update-MgUser -PasswordProfile needs only User-PasswordProfile.ReadWrite.All, which Microsoft lists as the least privileged permission for that property. Never hard-code a password in a script. Use Temporary Access Pass or self-service password reset for user resets where you can. |
Remove-MsolUser |
Remove-MgUser |
User.ReadWrite.All | Deleted users, including their mailbox and licence assignments, can be restored for 30 days. After that they’re permanently deleted. |
Get-MsolUser -ReturnDeletedUsers |
Get-MgDirectoryDeletedItemAsUser |
User.Read.All | GCIT. Microsoft’s map covers the AzureAD deleted-object cmdlets, not this MSOnline switch. This cmdlet lists deleted items that are users. |
Remove-MsolUser -RemoveFromRecycleBin |
Remove-MgDirectoryDeletedItem |
User.DeleteRestore.All | GCIT. Permanently deletes a user from deleted items. It can’t be undone. |
Restore-MsolUser |
Restore-MgDirectoryDeletedItem |
User.DeleteRestore.All | |
Get-MsolAccountSku, Get-AzureADSubscribedSku |
Get-MgSubscribedSku |
LicenseAssignment.Read.All | The old AccountSkuId (tenant:SKU) is gone. Use SkuPartNumber to find a licence and SkuId (a GUID) to assign it. |
Set-MsolUserLicense |
Set-MgUserLicense |
LicenseAssignment.ReadWrite.All | Always pass both -AddLicenses and -RemoveLicenses, using @() for the one you don’t need. |
Get-MsolCompanyInformation |
Get-MgOrganization |
User.Read | |
Get-MsolDomain |
Get-MgDomain |
Domain.Read.All | |
Set-MsolDomain |
Update-MgDomain |
Domain.ReadWrite.All | Only verified domains can be updated. |
Get-MsolPasswordPolicy |
Get-MgDomain |
Domain.Read.All | Read PasswordValidityPeriodInDays and PasswordNotificationWindowInDays. |
Set-MsolPasswordPolicy |
Update-MgDomain -PasswordValidityPeriodInDays |
Domain.ReadWrite.All | Microsoft recommends passwords that never expire (with MFA). See Important notes below. |
Get-MsolRole |
Get-MgDirectoryRole |
RoleManagement.Read.Directory | Only returns roles that are activated in the tenant. |
Get-MsolRoleMember |
Get-MgDirectoryRoleMember |
RoleManagement.Read.Directory | Returns users, groups and service principals. Get-MgDirectoryRoleMemberAsUser returns user properties such as UserPrincipalName, but leaves out groups and service principals, so don’t use it for an admin audit. |
Add-MsolRoleMember |
New-MgDirectoryRoleMemberByRef |
RoleManagement.ReadWrite.Directory | Prefer eligible (just-in-time) role assignments in Privileged Identity Management over permanent ones where your licensing allows. |
Get-MsolPartnerContract, Get-AzureADContract |
Get-MgContract |
Directory.Read.All | Partner tenants only. See the partner section below. It lists customer relationships, but it doesn’t give you access to them. |
Get-AzureADGroup |
Get-MgGroup |
Check with Find-MgGraphCommand |
Group.Read.All or GroupMember.Read.All covers most read scenarios. |
Add-AzureADGroupMember |
New-MgGroupMemberByRef |
GroupMember.ReadWrite.All | The signed-in admin also needs a role that can update group membership, such as Groups Administrator or User Administrator, or must own the group. |
Example: assign a licence the Graph way
This replaces the Set-MsolUserLicense -AddLicenses "tenant:SKU" pattern from our older articles. A licence can only be assigned to an account that has a usage location set, so the script sets that first. Both changes are written with -WhatIf, so the first run only shows what would change. The License Administrator role can do both steps.
# Least privileged scopes: update the user, read and assign licences
Connect-MgGraph -Scopes "User.ReadUpdate.All", "LicenseAssignment.ReadWrite.All" -TenantId "contoso.onmicrosoft.com" -ContextScope Process -NoWelcome
try {
$userUpn = "jane.citizen@contoso.com.au"
$skuPart = "SPB" # Microsoft 365 Business Premium. Check your own with: Get-MgSubscribedSku -All | Select-Object SkuPartNumber
# 1. Look up the licence by its part number and stop if it isn't in this tenant
$sku = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $skuPart }
if (-not $sku) {
throw "Licence $skuPart not found in this tenant."
}
# 2. A usage location (two-letter country code) is required before a licence can be assigned
Update-MgUser -UserId $userUpn -UsageLocation "AU" -WhatIf
# 3. Assign the licence
Set-MgUserLicense -UserId $userUpn -AddLicenses @{ SkuId = $sku.SkuId } -RemoveLicenses @() -WhatIf
}
finally {
Disconnect-MgGraph | Out-Null
}
When the -WhatIf output looks right, delete -WhatIf from the Update-MgUser and Set-MgUserLicense lines and run it again. Until you do, nothing in the tenant changes. For more than a handful of users, consider group-based licensing in the Microsoft Entra admin center instead of scripting licences user by user.
Running scripts unattended
Our older articles often stored an admin username and password so a script could run on a schedule. Don’t carry that forward. For scripts with no one at the keyboard, Microsoft recommends a service principal that uses a managed identity, a federated identity credential or a certificate, not a stored user password. Service principals and managed identities are also not affected by Microsoft’s mandatory MFA enforcement, which is why they’re the right tool for automation. Microsoft Graph PowerShell supports both:
# App registration with a certificate in the local certificate store (no secret in the script) Connect-MgGraph -ClientId "YOUR_APP_ID" -TenantId "YOUR_TENANT_ID" -CertificateThumbprint "YOUR_CERT_THUMBPRINT" -NoWelcome # Azure Automation, Azure Functions or an Azure VM with a system-assigned managed identity Connect-MgGraph -Identity -NoWelcome
App-only sessions use application permissions, which an administrator grants once to the app registration. They are not chosen with -Scopes at connect time. Give each automation its own app registration with only the application permissions it needs. Avoid client secrets. If a secret is unavoidable, keep it in Azure Key Vault and never in the script, a scheduled task or an app setting. Microsoft’s guide to app-only authentication with Microsoft Graph PowerShell walks through creating the app and certificate.
For Microsoft partners: DAP, GDAP and Get-MgContract
Many of our older partner scripts started with Get-MsolPartnerContract -All and then ran Msol cmdlets against each customer with -TenantId. Two things have changed:
Get-MgContractreplacesGet-MsolPartnerContractand still lists the customer relationships in your partner tenant, including each customer’sCustomerId,DefaultDomainNameandDisplayName. Microsoft notes the name and domain are copies taken when the relationship started, and they don’t update automatically. Source: contract resource type.- Admin access to customers now runs through granular delegated admin privileges (GDAP), which give least-privilege, time-bound access. Microsoft is replacing DAP with GDAP and recommends moving every customer to GDAP as soon as possible. Under GDAP, adding an app to your Admin Agents group no longer grants it access to customers, so each multi-tenant app must be consented in every customer tenant. Source: GDAP frequently asked questions.
In practice, treat Get-MgContract as an inventory list. Access to each customer depends on the GDAP relationship and the roles your technicians’ security groups hold in it, which you manage in Partner Center or through the Microsoft Graph delegatedAdminRelationship API. Our guide to unattended MSP automation with GDAP and Microsoft Graph covers multi-tenant scripting in detail.
Troubleshooting
- “Insufficient privileges to complete the operation” or a 403 error. Your session doesn’t have a scope the cmdlet needs, or your admin role doesn’t allow the action. Run
Find-MgGraphCommand -Command <cmdlet> | Select-Object -First 1 -ExpandProperty Permissions, reconnect with the right scope in-Scopes, and check your role. - Only some of your users come back.
Get-MgUserreturns one page of results by default. Add-All. - Properties such as UsageLocation or AccountEnabled are empty. They aren’t returned by default. Name them with
-Property, as the snapshot script does. - A licence won’t assign. Check the user has a
UsageLocation, and that the licence has free seats (PrepaidUnits.EnabledminusConsumedUnitsonGet-MgSubscribedSku). - Get-MgDirectoryRole doesn’t show a role you expected. It only returns roles that are activated in your tenant. Only the Global Administrator role is activated by default. To list every built-in and custom role definition, whether or not it’s activated, use
Get-MgRoleManagementDirectoryRoleDefinitionfrom theMicrosoft.Graph.Identity.Governancesubmodule. Sources: directoryRole resource type and List roleDefinitions. - Filters with -CountVariable or -Search fail. These are advanced queries and need
-ConsistencyLevel eventualas well. - “The term ‘Connect-MgGraph’ is not recognized”. The module isn’t installed in this version of PowerShell. Installing in PowerShell 7 doesn’t install it for Windows PowerShell 5.1, so run the install command in the version you’re using.
- Scripts are blocked from running. Set the execution policy with
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser. - You’re signed in to the wrong tenant or as the wrong account. Run
Disconnect-MgGraphto clear the cached sign-in, then reconnect with-TenantId. - Something odd you can’t explain. Update the module (
Update-Module Microsoft.Graph) and rerun the cmdlet with-Debug. The output shows the exact request and therequest-idthat Microsoft support will ask for. Check-Debugoutput for tokens or personal details before sharing it outside your organisation.
Important notes
- Don’t ask for more than you need. It’s tempting to connect with
Directory.ReadWrite.Allso every script just works. Don’t. Request the narrowest scopes for each task, and use a read-only role such as Global Reader for reporting. - Sign-ins are cached. With the default settings, Microsoft Graph PowerShell keeps you signed in across PowerShell windows until you run
Disconnect-MgGraph. On shared or jump-box machines, use-ContextScope Processand always disconnect. - Password expiry. The Microsoft 365 admin center labels “Set passwords to never expire” as the recommended setting, alongside MFA. If you have a requirement to expire passwords, set it per domain with
Update-MgDomain -DomainId <domain> -PasswordValidityPeriodInDays <days>or in the admin center under Settings, Org Settings, Security and Privacy. Microsoft also notes that the admin center and Microsoft 365 apps no longer show password expiry notifications. Source: Set the password expiration policy for your organization. - Use v1.0, not beta, in production. The
Microsoft.Graph.Betamodule calls preview APIs that can change without notice. - Permanent deletes are permanent.
Remove-MgDirectoryDeletedItemcan’t be undone. Export the deleted users list first, and run anything destructive with-WhatIfbefore you run it for real. - Review what you’ve consented to. Check the Microsoft Graph Command Line Tools enterprise app’s permissions regularly (Microsoft Entra admin center, Enterprise applications) and remove scopes nobody needs.
- No shared admin accounts. Each admin should sign in as themselves with MFA, so the audit log shows who did what. Automation should use its own app identity, not a person’s account.
Related resources
Microsoft Learn
- Install the Microsoft Graph PowerShell SDK
- Use Microsoft Graph PowerShell authentication commands
- Use Find-MgGraphCommand
- Find Azure AD and MSOnline cmdlets in Microsoft Graph PowerShell
- Migrate from Azure AD PowerShell to Microsoft Graph PowerShell
- Use app-only authentication with the Microsoft Graph PowerShell SDK
- Error handling and troubleshooting cmdlets
- What is Microsoft Entra PowerShell?
- Migrate from Azure AD PowerShell to Microsoft Entra PowerShell
- Assign Microsoft 365 licenses to user accounts with PowerShell
- Microsoft Graph permissions reference
- Microsoft Entra built-in roles
- Introduction to granular delegated admin privileges (GDAP)
GCIT knowledge base
- How to connect to Exchange Online PowerShell
- GDAP Secure Application Model: unattended MSP automation with Microsoft Graph
- Run Exchange Online PowerShell across customer tenants with GDAP
- Run Microsoft 365 PowerShell automation in Azure Functions with a managed identity
- GCIT GDAP partner toolkit on GitHub (Microsoft Graph across GDAP customer tenants, plus rewrites of our older MSOnline and AzureAD scripts)
- How to detect a breach in Microsoft 365
- Export customers’ Microsoft Secure Scores to CSV and HTML reports
Need a hand moving off MSOnline?
GCIT helps Australian businesses replace retired, password-based scripts with secure, least-privilege automation across Microsoft 365. If you’d like us to review your PowerShell scripts or set up certificate-based automation for you, get in touch with our team.