My AzureAD Scripts Stopped Working — Now What?

Microsoft deprecated the PowerShell modules your automation runs on. Here's how to migrate to Microsoft Graph SDK without turning it into a 2 a.m. incident.

My AzureAD scripts still run today. Do I actually need to migrate?

Yes. Not “eventually” — now, on your schedule, before someone else’s schedule picks the date for you.

Microsoft has deprecated the AzureAD, AzureADPreview and MSOnline modules and is steering everyone to the Microsoft Graph PowerShell SDK — that’s Microsoft’s own published position, not mine. When retirement lands, the cmdlets don’t get a polite deprecation warning — the underlying endpoints stop answering. Your nightly license-assignment job doesn’t fail loudly at 9 a.m. where you’d see it. It fails at 2 a.m., silently, and you find out when the helpdesk queue fills up with people who can’t reach a mailbox. Migrate while it’s a project, not an incident.

Confirm the current retirement status yourself before you plan around a date — Microsoft has moved these timelines more than once, and I’m not going to quote you a day that shifts. The live source of truth is the deprecation notice on the Microsoft Entra blog and the “Migrate from Azure AD PowerShell to Microsoft Graph PowerShell” guidance on Microsoft Learn. Check those, not a blog’s memory of a date.

Is Microsoft.Graph just the old module with a new name?

No, and pretending otherwise is how migrations go sideways. The Microsoft Graph PowerShell SDK is a different shape: different cmdlet nouns (Mg prefix), different parameter names, different auth model, different default behaviours. Get-AzureADUser becomes Get-MgUser — that mapping is published in the Microsoft Learn migration guide — but that’s where the comfort ends.

The big conceptual shift: the SDK talks to Microsoft Graph, so you now think in terms of Graph permissions (scopes) and Graph property names. A script that “just worked” because your admin session implicitly had rights now has to ask for those rights explicitly. That’s more friction, and it’s also the point — you can finally see exactly what a script is allowed to do.

Do I use Microsoft.Graph or the new Microsoft Entra PowerShell module?

It depends, but here’s the short version: if you’re writing new automation, start with Microsoft.Graph. If you’re migrating old AzureAD scripts and want the least rewriting, look at the Microsoft Entra PowerShell module (Microsoft.Entra), which — per its Microsoft Learn documentation — is built on the same Graph foundation but keeps cmdlet shapes closer to the ones your fingers already know.

They’re not rivals — Entra PowerShell sits on top of the Graph SDK. Pick one per script and stay consistent. Mixing connection contexts in a single runbook is a debugging tax you’ll pay later. My default: Graph SDK for anything I’m building fresh, Entra module when I’m porting a pile of legacy AzureAD calls and want to minimise the diff.

Why does Connect-MgGraph throw a permissions error when the same admin worked fine before?

Because the SDK doesn’t inherit your directory role rights automatically — you request scopes, and the tenant (or an admin) consents to them. If you didn’t ask for a scope, you don’t have it, regardless of how much of a Global Admin you are. This is how Connect-MgGraph is documented to work on Microsoft Learn.

audit.ps1PowerShell
# Connect with only the scopes this script needs — least privilege, not "everything"
Connect-MgGraph -Scopes "User.Read.All", "Directory.Read.All"
​
# Verify what you actually got
$context = Get-MgContext
$context.Scopes

Ask for read scopes when you’re only reading. The moment a script needs User.ReadWrite.All or Directory.ReadWrite.All, that’s a flag in code review, not a default you reach for. If consent fails, an admin has to approve the scope in the tenant first — that’s working as intended, even when it’s inconvenient at 2 a.m.

Get-MgUser only returns 100 users. Where did the rest go?

Nowhere — you’re seeing one page. The SDK paginates, and the default page is small (the Microsoft Graph paging documentation covers this). This is the single most common “my report is wrong” bug in freshly migrated scripts, because it fails quietly: no error, just an undercount that looks plausible.

audit.ps1PowerShell
# Wrong: returns the first page only
$some = Get-MgUser
​
# Right: -All follows pagination to the end
$all = Get-MgUser -All -Property "id","userPrincipalName","accountEnabled" `
    -ErrorAction Stop
​
"Retrieved $($all.Count) users"

Use -All for anything that needs a complete set. Ask for the -Property fields you actually use — pulling every attribute for 40,000 users is slow and rude to the service. And wrap it in -ErrorAction Stop inside a try/catch so a throttling blip doesn’t leave you with a half-empty array you then treat as gospel.

Why do advanced filters throw a “not supported” error?

Because some Graph queries need the advanced query engine, and you have to opt in with -ConsistencyLevel eventual and -CountVariable. Filtering on things like endsWith, or counting, or ordering on certain properties won’t work without it — Microsoft documents this under “Advanced query capabilities on directory objects.”

audit.ps1PowerShell
# Find guest accounts — needs advanced query support
$guests = Get-MgUser -All `
    -Filter "userType eq 'Guest'" `
    -ConsistencyLevel eventual `
    -CountVariable guestCount `
    -Property "id","userPrincipalName","createdDateTime","externalUserState" `
    -ErrorAction Stop
​
"Found $guestCount guests"

If a filter mysteriously returns nothing or errors, add the consistency header before you assume the data’s missing. Nine times out of ten that’s it.

How do I bulk-update users without nuking my tenant?

Never pipe a live query straight into a write cmdlet. That’s the “I’ll just disable the stale accounts real quick” command that becomes a Sev 1. Split it into two phases: export and review, then act on the reviewed file, with -WhatIf on by default.

audit.ps1PowerShell
# PHASE 1 — export candidates for a human to eyeball
$stale = Get-MgUser -All `
    -Filter "accountEnabled eq true" `
    -Property "id","userPrincipalName","signInActivity" `
    -ErrorAction Stop |
    Where-Object {
        $_.SignInActivity.LastSignInDateTime -lt (Get-Date).AddDays(-180)
    }
​
$stale | Select-Object Id, UserPrincipalName,
    @{n='LastSignIn';e={$_.SignInActivity.LastSignInDateTime}} |
    Export-Csv .\stale_review.csv -NoTypeInformation
​
# --- STOP. Open stale_review.csv. Delete rows you don't trust. ---
audit.ps1PowerShell
# PHASE 2 — act only on the reviewed file, dry-run first
$reviewed = Import-Csv .\stale_review.csv
​
foreach ($u in $reviewed) {
    try {
        Update-MgUser -UserId $u.Id -AccountEnabled:$false -WhatIf -ErrorAction Stop
    }
    catch {
        Write-Warning "Failed on $($u.UserPrincipalName): $($_.Exception.Message)"
    }
}
# Remove -WhatIf only after the dry run reads correctly, and keep the CSV.

Note -UserId, not -ObjectId — that rename is in the Update-MgUser reference, and it’s exactly what breaks a naive find-and-replace migration. The signInActivity property is another trap: per the Microsoft Graph user resource docs it needs the AuditLog.Read.All scope and a Microsoft Entra ID P1 or P2 license to be populated, so a null last-sign-in means “unknown,” not “never.” Don’t disable on nulls.

What replaces Get-MsolUser and Set-MsolUserLicense for licensing?

Get-MgUser for the read, Set-MgUserLicense for the write. The license object model is different — as the Set-MgUserLicense reference shows, you pass SKU IDs in a structured body rather than the old string-friendly parameters.

audit.ps1PowerShell
# Look up the SKU you want to assign
$sku = Get-MgSubscribedSku -All -ErrorAction Stop |
    Where-Object SkuPartNumber -eq "ENTERPRISEPACK"
​
# Dry-run the assignment against one reviewed user
Set-MgUserLicense -UserId $targetUserId `
    -AddLicenses @(@{ SkuId = $sku.SkuId }) `
    -RemoveLicenses @() `
    -WhatIf -ErrorAction Stop

Group-based licensing in Entra ID is the better answer for anything recurring — let group membership drive assignment and stop hand-running license scripts entirely. Verify assignments in the Entra admin center under Billing > Licenses and on the user’s Licenses blade.

Can I run this unattended in Azure Automation?

Yes — and this is where you stop using interactive login. Use a managed identity or an app registration with a certificate, granted the exact application permissions the runbook needs. No secrets in the script, no passwords in variables. The -Identity switch on Connect-MgGraph is documented for exactly this.

audit.ps1PowerShell
# Managed identity in Azure Automation — no stored credentials
Connect-MgGraph -Identity -ErrorAction Stop

Grant that identity read-only scopes wherever the job only reports. Reserve write scopes for the runbooks that genuinely change state, and document which is which. When an audit asks “what can this automation do,” you want the answer to be one line in a consent screen, not a shrug.

Will migrating break my break-glass access?

It shouldn’t — break-glass accounts are cloud-only admins you sign in with interactively, not automation. But test them before you retire the old modules, not after, and confirm they’re excluded from the Conditional Access policies you’re about to lean on. The one time you need that account is the one time you can’t afford a surprise.

What’s the one thing that bites people six months later?

Scope creep in reverse: someone requests Directory.ReadWrite.All “to be safe” during migration, admin-consents it, and it never gets walked back. A year on it’s a standing write permission nobody remembers granting, sitting on an app registration nobody owns. Migrate to least privilege now, review consented scopes quarterly, and treat every ReadWrite as a decision you have to defend. The Graph SDK finally makes those permissions visible — the mistake is not looking.

Where do I verify all this before trusting a blog?

Good instinct. Don’t take my word for any of it — take Microsoft’s. Everything above traces back to primary docs:

  • Deprecation status and timeline — the Microsoft Entra blog deprecation announcements for the AzureAD, AzureADPreview and MSOnline modules, and “Migrate from Azure AD PowerShell to Microsoft Graph PowerShell” on Microsoft Learn. This is the only place I’d trust for a current retirement date.
  • Cmdlet and parameter mapping (Get-AzureADUser → Get-MgUser, -ObjectId → -UserId) — the same migration guide, plus the individual Get-MgUser and Update-MgUser cmdlet references.
  • Scopes and consent (User.Read.All, Directory.Read.All, AuditLog.Read.All) and the Connect-MgGraph -Identity flow — the Microsoft Graph PowerShell authentication docs on Microsoft Learn.
  • Pagination and -All — the Microsoft Graph paging documentation.
  • Advanced queries (-ConsistencyLevel eventual, -CountVariable) — “Advanced query capabilities on directory objects” in the Microsoft Graph docs.
  • signInActivity licensing and permission — the Microsoft Graph user resource reference, which states the P1/P2 license and AuditLog.Read.All requirement.
  • Licensing cmdlets — the Set-MgUserLicense and Get-MgSubscribedSku references, and the group-based licensing docs in the Microsoft Entra ID documentation.

If any of those pages disagrees with me, believe the page. That’s the whole discipline: trust the source that gets updated, not the one that got written once.