Migrate AzureAD & MSOnline PowerShell to Microsoft Graph

Migrating AzureAD and MSOnline to Graph — header banner on SecurityScriptographer

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-AzureAD with Connect-MgGraph and an explicit list of permission scopes — authentication is the first thing that breaks and the first thing to fix.
  • Most cmdlets map cleanly (Get-AzureADUser becomes Get-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 — The AzureAD to Microsoft Graph cmdlet migration map

The bulk of a migration is mechanical. The table below is the migration map for the MSOnline and AzureAD cmdlets 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.

MSOnline / AzureAD cmdlet Microsoft Graph equivalent Notes
Connect-MsolService / Connect-AzureADConnect-MgGraphRequires explicit -Scopes; see Step 2.
Get-MsolUser / Get-AzureADUserGet-MgUserAdd -All and -Property; see Step 4.
New-MsolUser / New-AzureADUserNew-MgUserPassword goes in a -PasswordProfile hashtable.
Set-MsolUser / Set-AzureADUserUpdate-MgUserVerb changed from Set to Update.
Remove-MsolUser / Remove-AzureADUserRemove-MgUserTakes -UserId (UPN or object ID), not -ObjectId.
Set-AzureADUserPasswordUpdate-MgUser with -PasswordProfileNo dedicated password cmdlet for this scenario.
Revoke-AzureADUserAllRefreshTokenRevoke-MgUserSignInSessionSame effect: invalidates refresh tokens and session cookies.
Get-MsolGroup / Get-AzureADGroupGet-MgGroupSame paging caveat as users — add -All.
New-AzureADGroupNew-MgGroup-MailNickname is mandatory even for security groups.
Get-AzureADGroupMemberGet-MgGroupMemberReturns directory objects; expand with -Property as needed.
Add-AzureADGroupMemberNew-MgGroupMemberVerb changed from Add to New.
Remove-AzureADGroupMemberRemove-MgGroupMemberByRefThe ByRef suffix is required — Remove-MgGroupMember does not exist.
Get-AzureADUserMembershipGet-MgUserMemberOfReturns groups and directory roles as directory objects.
Get-MsolRole / Get-AzureADDirectoryRoleGet-MgDirectoryRoleOnly lists roles that are activated in the tenant.
Add-MsolRoleMember / Add-AzureADDirectoryRoleMemberNew-MgDirectoryRoleMemberByRefMember is passed as an OData reference, not an object ID parameter.
Get-AzureADApplicationGet-MgApplicationApp registrations; filter with OData syntax.
Get-AzureADServicePrincipalGet-MgServicePrincipalEnterprise apps and managed identities.
Get-AzureADDeviceGet-MgDeviceSame object model as the Entra devices blade.
Get-MsolAccountSkuGet-MgSubscribedSkuReturns SKU GUIDs; needed for Set-MgUserLicense.
Set-MsolUserLicense / Set-AzureADUserLicenseSet-MgUserLicense-RemoveLicenses @() is mandatory even when only adding; see Step 4.
Get-MsolDomain / Get-AzureADDomainGet-MgDomainStraight rename.
Get-MsolCompanyInformationGet-MgOrganizationTenant-level settings moved to the organization resource.

Microsoft maintains a longer machine-generated list in the official cmdlet map; the table above is the subset I have actually needed, with the parameter surprises noted.

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
Connect-MgGraph followed by Get-MgUser returning users after migrating from AzureAD and MSOnline PowerShell to the Microsoft Graph PowerShell SDK

Connect-MgGraph plus Get-MgUser returning the directory — the Graph SDK replacement for Connect-AzureAD and Get-MsolUser.

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

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.

Entra ID Microsoft 365 Security Microsoft Graph PowerShell Sysadmin
SecurityScriptographer author

About the author

SecurityScriptographer is written and maintained by one person — a defender who builds and tests the detections, scripts, and Microsoft 365 workflows here before publishing them. More about me · @twi_nox

0 comments:

Post a Comment