How to Install Microsoft Graph PowerShell (Replaces MSOnline and AzureAD)

Microsoft Entra logo

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-MsolService or Connect-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-MsolService or 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-AzureAD a 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-AzureADUser becomes Get-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 the Verb-MgNoun format, such as Get-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. The Microsoft.Entra module 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 (using Enable-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 RemoteSigned for 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

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:

  • -TenantId sets 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.
  • -UseDeviceCode signs 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 Process keeps 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 run Disconnect-MgGraph.
  • -NoWelcome hides 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

  1. Save the script as Get-GraphTenantSnapshot.ps1.
  2. Open PowerShell 7 (or Windows PowerShell 5.1) and change to the folder where you saved it.
  3. If you haven’t already, install the three submodules it needs (Microsoft.Graph.Authentication, Microsoft.Graph.Users and Microsoft.Graph.Identity.DirectoryManagement) as shown in Step 3.
  4. Run it with your tenant’s initial domain or tenant ID: .\Get-GraphTenantSnapshot.ps1 -TenantId contoso.onmicrosoft.com
  5. Sign in with your admin account and complete MFA. A Global Reader account is enough, because every scope is read-only.
  6. 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).
  7. Open the CSV files in the output folder (by default, a GraphTenantSnapshot folder 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-MgContract replaces Get-MsolPartnerContract and still lists the customer relationships in your partner tenant, including each customer’s CustomerId, DefaultDomainName and DisplayName. 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-MgUser returns 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.Enabled minus ConsumedUnits on Get-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-MgRoleManagementDirectoryRoleDefinition from the Microsoft.Graph.Identity.Governance submodule. Sources: directoryRole resource type and List roleDefinitions.
  • Filters with -CountVariable or -Search fail. These are advanced queries and need -ConsistencyLevel eventual as 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-MgGraph to 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 the request-id that Microsoft support will ask for. Check -Debug output 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.All so 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 Process and 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.Beta module calls preview APIs that can change without notice.
  • Permanent deletes are permanent. Remove-MgDirectoryDeletedItem can’t be undone. Export the deleted users list first, and run anything destructive with -WhatIf before 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.

Microsoft Learn

GCIT knowledge base

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.

Was this article helpful?
Ready to secure and simplify your IT? Talk to a GCIT expert today.