If you still have scripts that call Connect-MsolService or Connect-AzureAD, they have probably stopped working by now. The plan to migrate AzureAD to Microsoft Graph PowerShell is no longer a "someday" item — both legacy modules are retired, and anything depending on them fails the moment a scheduled task fires. This post walks through what actually changed, how to translate the cmdlets you used most, and the gotchas that make the rewrite take longer than the cmdlet names suggest.
Key Takeaways
- The MSOnline and AzureAD PowerShell modules were deprecated on 30 March 2024 and have since been retired, so any automation built on them needs to move to the Microsoft Graph PowerShell SDK.
- To migrate AzureAD to Microsoft Graph PowerShell you replace
Connect-AzureADwithConnect-MgGraphand an explicit list of permission scopes — authentication is the first thing that breaks and the first thing to fix. - Most cmdlets map cleanly (
Get-AzureADUserbecomesGet-MgUser), but parameter names, default property sets, and paging behaviour all changed and will silently return wrong results if ignored. - The Microsoft Entra PowerShell module offers a compatibility mode that runs legacy AzureAD cmdlets unchanged, which is the fastest bridge for large script libraries you cannot rewrite at once.
- License assignment, filtering, and "get everything" queries are the three areas where a straight cmdlet swap is not enough and the logic has to be rewritten.
Environment
- Windows Server 2022 and Windows 11 admin workstations running PowerShell 7.4 (the Graph SDK also works on Windows PowerShell 5.1).
- Microsoft.Graph PowerShell SDK v2.x, installed from the PowerShell Gallery.
- Microsoft.Entra PowerShell module v1.0 (generally available since March 2025) for the compatibility-mode bridge.
- A Microsoft 365 / Entra ID P1 tenant with an account holding the directory roles needed for the operations being scripted.
The Problem
For years there were two ways to manage identity from PowerShell: the old MSOnline module (the Msol* cmdlets, Connect-MsolService) and the slightly newer AzureAD module (the AzureAD* cmdlets, Connect-AzureAD). Both talked to the legacy Azure AD Graph API, which Microsoft has been winding down for a long time. The modules were deprecated on 30 March 2024, MSOnline stopped working for all tenants through April and May 2025, and AzureAD followed in the third quarter of 2025. There is no extension coming — Microsoft has been clear that the retirement is final.
The replacement is the Microsoft Graph PowerShell SDK, which talks to Microsoft Graph instead of Azure AD Graph. The cmdlet noun prefix is Mg, so Get-AzureADUser becomes Get-MgUser. If that were the whole story, this would be a find-and-replace job. It is not. The SDK changed how you authenticate, how much data a query returns by default, which properties come back, and how you filter — and none of those failures are loud. A migrated script often runs without error and quietly returns the first 100 objects, or null for a property you depend on. That is the part worth getting right.
The Solution
Step 1 — Install the Microsoft Graph PowerShell SDK
The SDK is a meta-module that pulls in a large set of sub-modules. Installing the whole thing is the simplest start; if you only need users and groups, you can install just those sub-modules to keep load times down.
# Everything (large, but complete)
Install-Module Microsoft.Graph -Scope CurrentUser
# Or only what you need
Install-Module Microsoft.Graph.Users -Scope CurrentUser
Install-Module Microsoft.Graph.Groups -Scope CurrentUser
Get-InstalledModule Microsoft.Graph* | Select-Object Name, Version
One practical note: the full Microsoft.Graph module is heavy, and importing all of it on every script run is slow. In production I import only the specific sub-modules a script actually uses. It is the difference between a one-second and a fifteen-second start.
Step 2 — Replace Connect-AzureAD with Connect-MgGraph and scopes
This is the change that breaks first. Connect-AzureAD gave you whatever your role allowed. Connect-MgGraph uses Microsoft Graph's permission model, so you declare the scopes you need up front. Request too few and the call fails with an authorization error; request sensibly and you get a consent prompt the first time.
# Interactive sign-in with explicit delegated scopes
Connect-MgGraph -Scopes "User.ReadWrite.All","Group.ReadWrite.All","Organization.Read.All"
# Confirm who you are and what you were granted
Get-MgContext | Select-Object Account, Scopes
For unattended automation, do not use delegated sign-in. Register an app, grant it application permissions, and connect with a certificate. That keeps the scheduled task working without a human at the keyboard and avoids storing a password.
Connect-MgGraph -ClientId $appId -TenantId $tenantId -CertificateThumbprint $thumb
If you are not sure which permission a given operation needs, Find-MgGraphPermission searches the catalogue (for example Find-MgGraphPermission user), and Find-MgGraphCommand maps a Graph API path back to the cmdlet that calls it. Both are part of the SDK and save a lot of guesswork during a migration.
Step 3 — Translate the cmdlets you actually used
The bulk of a migration is mechanical. Below are the swaps that cover most real scripts. The nouns line up; watch the parameter names, because -ObjectId is gone and most cmdlets now take -UserId, -GroupId, or a plain -Filter.
# Connect
Connect-MsolService -> Connect-MgGraph -Scopes ...
Connect-AzureAD -> Connect-MgGraph -Scopes ...
# Users
Get-MsolUser / Get-AzureADUser -> Get-MgUser
New-AzureADUser -> New-MgUser
Set-AzureADUser -> Update-MgUser
Remove-AzureADUser -> Remove-MgUser
# Groups
Get-AzureADGroup -> Get-MgGroup
Get-AzureADGroupMember -> Get-MgGroupMember
Add-AzureADGroupMember -> New-MgGroupMember
Get-AzureADUserMembership -> Get-MgUserMemberOf
# Directory roles, apps, service principals
Get-MsolRole -> Get-MgDirectoryRole
Get-AzureADApplication -> Get-MgApplication
Get-AzureADServicePrincipal -> Get-MgServicePrincipal
# Tenant / licensing
Get-MsolAccountSku -> Get-MgSubscribedSku
Get-MsolDomain -> Get-MgDomain
Disabling an account is a good example of a rename that also changes shape. The old "block credential" flag became a plain boolean property:
# Old
Set-MsolUser -UserPrincipalName user@contoso.com -BlockCredential $true
# New
Update-MgUser -UserId user@contoso.com -AccountEnabled:$false
Step 4 — Fix the three things a straight swap breaks
This is where migrated scripts go wrong quietly. Three behaviours changed, and all three return plausible-looking but incorrect results if you leave the old assumptions in place.
Paging. Graph returns one page — 100 objects — unless you ask for more. A report that used to enumerate every user now silently covers the first hundred. Always add -All.
# Returns 100 users, not all of them
Get-MgUser
# Returns the whole directory
Get-MgUser -All
Property selection. The SDK returns a small default set of properties to keep responses fast. Anything outside that set comes back as $null even though it exists in the directory. You have to name the properties you want with -Property.
# Department and SignInActivity will be $null without -Property
Get-MgUser -All -Property DisplayName,UserPrincipalName,Department,SignInActivity |
Select-Object DisplayName, UserPrincipalName, Department,
@{ N = 'LastSignIn'; E = { $_.SignInActivity.LastSignInDateTime } }
Filtering. There is no -SearchString. Filtering uses OData syntax through -Filter. Simple comparisons work directly; advanced operators (endsWith, not, ne, counting) require the eventual-consistency header via -ConsistencyLevel eventual and a -CountVariable.
# Simple filter
Get-MgUser -Filter "startsWith(displayName,'A')" -All
# Advanced filter needs the eventual-consistency header
Get-MgUser -Filter "endsWith(mail,'@contoso.com')" `
-ConsistencyLevel eventual -CountVariable total -All
Licensing is the fourth landmine and deserves its own note. Set-MgUserLicense takes the SKU GUID inside a hashtable array, not a friendly name, and -RemoveLicenses is mandatory even when you are only adding — pass an empty array.
$sku = Get-MgSubscribedSku -All | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK'
Set-MgUserLicense -UserId user@contoso.com `
-AddLicenses @{ SkuId = $sku.SkuId } `
-RemoveLicenses @()
Step 5 — Bridge legacy scripts with Microsoft Entra PowerShell compatibility mode
If you have dozens of scripts and cannot rewrite them all this week, there is a supported middle path. The Microsoft Entra PowerShell module went generally available in March 2025, is built on top of the Graph SDK, and ships a compatibility mode that maps legacy AzureAD cmdlet names onto its own. Microsoft puts the coverage at over 98% of the old AzureAD and AzureADPreview surface.
Import-Module Microsoft.Entra
Connect-Entra
Enable-EntraAzureADAlias
After those three lines, an existing script that calls Get-AzureADUser or New-AzureADGroup runs unchanged — the aliases redirect each call to the Entra equivalent for the current session. In practice you replace the old Connect-AzureAD line with the block above and leave the rest of the script alone. The official migration guide documents the cmdlets that do not have a clean one-to-one mapping, which is the short list worth reading before you rely on this.
I treat compatibility mode as a bridge, not a destination. It buys time to retire scripts on a schedule instead of in a panic, but new automation should target the native Mg* or Entra* cmdlets directly so you are not carrying a translation layer forever.
Frequently Asked Questions
Are the MSOnline and AzureAD modules really gone, or just unsupported?
Both are retired, not merely unsupported. MSOnline stopped working for all tenants during April and May 2025, and the AzureAD module was retired in the third quarter of 2025. They were deprecated on 30 March 2024, and the service-side endpoints they depend on are being switched off, so reinstalling an old module version does not bring functionality back.
Should I migrate to the Microsoft Graph PowerShell SDK or the Microsoft Entra PowerShell module?
They are not competitors — the Entra module is built on the Graph SDK and is fully interoperable with it. Use the Graph SDK (Mg*) for broad coverage across all of Microsoft Graph; use the Entra module (Entra*) when you want a more familiar, identity-focused cmdlet set or the AzureAD compatibility aliases. Mixing both in one session is supported.
Why does Get-MgUser return null for properties that clearly have values?
Because the SDK returns a limited default property set for performance. Properties outside that set come back as $null until you request them explicitly with -Property. This is the single most common surprise when you migrate AzureAD to Microsoft Graph PowerShell, and it does not raise an error.
Can I run the Graph SDK on Windows PowerShell 5.1?
Yes. The Microsoft Graph PowerShell SDK supports both Windows PowerShell 5.1 and PowerShell 7. PowerShell 7 is the better choice for new work, but you do not have to upgrade your hosts just to start migrating.
How do I find the right scope for a cmdlet?
Use Find-MgGraphPermission to search the permission catalogue by keyword, and Find-MgGraphCommand to see which cmdlet maps to a given Graph API path and which permissions it needs. Requesting least-privilege scopes in Connect-MgGraph rather than broad ones keeps consent grants tidy and auditable.
Conclusion
Is this migration more work than the cmdlet names imply? Yes. The renames are trivial; the changes to authentication, paging, property selection, and filtering are where the real time goes, and they fail quietly enough that a "working" script can return wrong data for weeks before anyone notices. Test migrated reporting scripts against known counts before you trust them.
The upside is that this is a one-time cost with a clear payoff. Once a script is on the Graph SDK it talks to a current, supported API with a sane permission model, and the same patterns carry across every workload in Microsoft 365 rather than just identity. Lean on the Entra compatibility mode to stop the bleeding on legacy scripts, then rewrite them onto native cmdlets on your own timeline. Not glamorous, but it is the kind of cleanup that pays for itself the next time Microsoft retires an endpoint.
Related Posts
- PowerShell Quick Guide: Creating Your First Security Audit Script — the kind of identity audit script you will be porting to the Graph SDK.
- PowerShell Quick Guide: Exporting Data to CSV Files — useful once
Get-MgUser -Allis feeding a report. - PowerShell Quick Guide: Remote Management Basics — the on-premises companion to cloud identity automation.
Editorial note: posts on this blog are drafted with AI assistance and then reviewed, edited, and tested against a real environment before publishing. Commands, output, and screenshots come from systems I actually ran the work on.
0 comments:
Post a Comment