Replace AzureAD and MSOnline with Microsoft Graph PowerShell without breaking scripts

Inventory commands, map permissions and objects, secure authentication, and migrate in stages with parity testing and a rollback path.

01

A module migration is not search and replace

AzureAD, AzureADPreview and MSOnline are deprecated. Microsoft Graph PowerShell is their successor, but renaming commands does not make old scripts compatible. Parameters, permissions, filters, pagination and returned object shapes can all change.

The greatest risk sits in unattended scripts that create accounts, assign licences, manage groups, revoke sessions or produce scheduled reports. A safe migration inventories dependencies, reproduces reads first, compares results, then moves writes behind a dry-run control and rollback procedure.

02

1. Inventory scripts and rank their risk

Search repositories, scheduled tasks, runbooks, management servers and service-desk tools for AzureAD and MSOnline imports, connections and commands. Record the owner, schedule, execution identity, write operations, permissions and impact of failure for each script.

Migrate simple reports before critical automation. A read-only script lets the team learn Graph objects without risking a removed licence or an unintended group change.

Read-only local inventory
$root = 'C:\Scripts'
$patterns = 'AzureAD','MSOnline','Connect-AzureAD','Connect-MsolService',`
            'Get-AzureAD','Set-AzureAD','New-AzureAD','Remove-AzureAD',`
            'Get-Msol','Set-Msol','New-Msol','Remove-Msol'
Get-ChildItem $root -Filter *.ps1 -Recurse -File |
  Select-String -Pattern $patterns -SimpleMatch |
  Select-Object Path,LineNumber,Line |
  Export-Csv "$root\graph-migration-inventory.csv" -NoTypeInformation -Encoding UTF8
03

2. Install only the modules you need

PowerShell 7 is recommended, although the SDK also supports Windows PowerShell 5.1. The complete Microsoft.Graph package contains dozens of submodules. On automation hosts, install only the modules in use to reduce loading time and keep updates controlled.

Prefer the v1.0 API in production. Microsoft.Graph.Beta is separate and its contracts may change. Use it only for a required feature that is not available in v1.0 and isolate that dependency in code.

Targeted installation and command discovery
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser
Install-Module Microsoft.Graph.Users -Scope CurrentUser
Install-Module Microsoft.Graph.Groups -Scope CurrentUser
Install-Module Microsoft.Graph.Identity.SignIns -Scope CurrentUser
Find-MgGraphCommand -Command Get-MgUser
Find-MgGraphCommand -Uri '/users/{id}' -Method GET
Find-MgGraphPermission -SearchString 'Get-MgUser'
04

3. Match authentication to the workload

Connect-MgGraph uses MSAL and supports delegated and application permissions. Unattended work must not depend on an administrator's personal token. Prefer a managed identity where supported, otherwise use a dedicated application and certificate. Do not reuse one application for every script because a compromise would expose their combined permissions.

ScenarioMethodSecurity choice
Interactive administrationDelegated permissionsMinimum scopes, human sign-in and Process context
Azure Automation or Azure resourceManaged identityNo distributed secret; minimum application permissions
Scheduled task outside AzureApplication and certificateProtected private key, documented rotation and admin consent
Plain-text client secretAvoidEasy to copy, expire or expose in logs
Interactive, certificate or managed-identity connection
Connect-MgGraph -Scopes 'User.Read.All','Group.Read.All' -ContextScope Process
Connect-MgGraph -TenantId $TenantId -ClientId $ClientId `
  -CertificateThumbprint $Thumbprint -NoWelcome
Connect-MgGraph -Identity -NoWelcome
Get-MgContext | Select-Object TenantId,ClientId,AuthType,Scopes,ContextScope
05

4. Map commands and permissions

Microsoft's command map is a starting point, not proof of equivalence. Some capabilities exist only in Graph Beta, while others have no direct replacement. Read the documentation and underlying API for every command, then choose the least privileged permission that supports the operation.

Legacy commandGraph commandValidate
Connect-AzureAD / Connect-MsolServiceConnect-MgGraphScopes, tenant, authentication type and context
Get-AzureADUser / Get-MsolUserGet-MgUserExplicit properties, filters and pagination
Get-AzureADGroupGet-MgGroupGroup type and returned properties
Get-AzureADGroupMemberGet-MgGroupMemberdirectoryObject results; cast or retrieve type when needed
Add-AzureADGroupMemberNew-MgGroupMemberByRef@odata.id URI and Object ID
Get-AzureADSubscribedSkuGet-MgSubscribedSkuSkuId, SkuPartNumber and available units
Set-AzureADUserLicenseSet-MgUserLicenseaddLicenses/removeLicenses arrays and rollback
Get-AzureADMSConditionalAccessPolicyGet-MgIdentityConditionalAccessPolicyPolicy.Read.All or higher permissions
Revoke-AzureADUserAllRefreshTokenRevoke-MgUserSignInSessionOperational effect and logging
06

5. Rewrite reads correctly

Get-MgUser does not return every property by default. Request every property the script consumes. Use -All when the result must span every page, and prefer a server-side filter over downloading the tenant and applying Where-Object locally.

Advanced queries using $count, certain filters or searches require ConsistencyLevel eventual. Compare results by Id or UserPrincipalName, not display order.

Complete read with explicit properties
$users = Get-MgUser -All `
  -Property Id,DisplayName,UserPrincipalName,AccountEnabled,Department `
  -Filter "accountEnabled eq true"
$users | Select-Object Id,DisplayName,UserPrincipalName,AccountEnabled,Department
Advanced query with count
$count = 0
$guests = Get-MgUser -All -ConsistencyLevel eventual `
  -CountVariable count -Filter "userType eq 'Guest'" `
  -Property Id,DisplayName,UserPrincipalName,CreatedDateTime
"Returned guests: $($guests.Count); Graph count: $count"
07

6. Put writes behind a dry-run control

Do not assume every Graph command supports -WhatIf. Add an explicit DryRun switch, log the target and proposed values, and require an intentional action to execute. Use stable Object IDs and read back the object after critical changes.

For licences, resolve SkuPartNumber to SkuId first, retain the prior state and test with a pilot account. Removing a licence can affect services and retention. The example performs the addition only when DryRun is off.

Licence assignment with an explicit guardrail
param([string]$UserId, [string]$SkuPartNumber, [switch]$DryRun)
$sku = Get-MgSubscribedSku -All | Where-Object SkuPartNumber -eq $SkuPartNumber | Select-Object -First 1
if (-not $sku) { throw "SKU not found: $SkuPartNumber" }
$before = Get-MgUserLicenseDetail -UserId $UserId
$plan = [pscustomobject]@{ UserId=$UserId; AddSkuId=$sku.SkuId; Remove=@() }
$plan | ConvertTo-Json -Depth 5
if (-not $DryRun) {
  Set-MgUserLicense -UserId $UserId `
    -AddLicenses @(@{ SkuId = $sku.SkuId }) -RemoveLicenses @()
  Get-MgUserLicenseDetail -UserId $UserId
}
08

7. Compare old and new before cutover

For a short controlled period, run both versions read-only and normalize their output to the same schema. Compare object counts, identifiers and business values. Differences may reveal a missing property, unhandled page, incompatible filter or insufficient permission.

Never run two writing versions in parallel. Critical automation needs a kill switch, correlation logging, failure alert and a way to temporarily restore the legacy version without losing queued input.

Compare normalized exports
$old = Import-Csv .\baseline-azuread.csv | Sort-Object Id
$new = Import-Csv .\candidate-graph.csv | Sort-Object Id
Compare-Object $old $new -Property Id,UserPrincipalName,AccountEnabled `
  -PassThru | Export-Csv .\graph-differences.csv -NoTypeInformation
09

8. Roll out in waves and monitor

  • Keep the tested module versions in the deployment repository.
  • Test SDK updates before deploying them to runbooks.
  • Log tenant, application, operation, target and result without exposing tokens.
  • Handle errors and throttling without endless retry loops.
  • Remove obsolete certificates, secrets and consent after migration.
WaveScopeExit criterion
0 — InventoryScripts, owners, modules and identitiesNo unknown dependency
1 — ReportsSimple non-critical readsObject and property parity
2 — Pilot automationDedicated app and limited targetsLeast privilege and recovery tested
3 — ProductionMigration by serviceAlerts, logs and procedures operational
4 — RetirementLegacy jobs and consentNo AzureAD or MSOnline calls remain
10

Checklist before removing AzureAD and MSOnline

The most reliable migration is rarely the fastest. It turns every implicit dependency into an explicit choice: which data to read, which identity acts, which permissions are required and how to roll back. That work prevents a module change from becoming a production outage.

  • Every script, runbook and scheduled task has an owner.
  • Each legacy command has a validated mapping or documented decision.
  • Properties, pagination and filters were tested against realistic volume.
  • Graph permissions follow least privilege and consent is documented.
  • Unattended tasks use a managed identity or protected certificate.
  • Writes have dry run, logging and rollback.
  • Read results reached parity before cutover.
  • Support teams can recognize and handle a Graph failure.
  • Legacy modules, identities and permissions are removed only after observation.