The Entra ID portal will tell you "MFA enabled" for a user. That answer is meaningful only if you know what methods are registered. A user with SMS-only is technically MFA-enabled and is also the easiest target for SIM-swap. This guide is the SDK-based audit that produces the answers an auditor actually wants.
Table of Contents
Why "MFA enabled" is not enough
Three concrete failure modes the binary view misses:
- SMS as the only second factor โ vulnerable to SIM-swap.
- Voice call as the only second factor โ same problem, slower.
- A passkey or Authenticator registered but the user still using legacy auth that bypasses it.
The audit needs the registered methods plus the methods actually being used at sign-in.
List registered methods per user
Connect-MgGraph -Scopes 'UserAuthenticationMethod.Read.All','User.Read.All'
Get-MgUser -All -Property Id,UserPrincipalName | ForEach-Object {
$u = $_
$m = Get-MgUserAuthenticationMethod -UserId $u.Id
[pscustomobject]@{
UPN = $u.UserPrincipalName
Methods = ($m | ForEach-Object { $_.AdditionalProperties['@odata.type'] -replace '#microsoft.graph.', '' }) -join ','
}
}
The @odata.type values you care about: passwordAuthenticationMethod, microsoftAuthenticatorAuthenticationMethod, fido2AuthenticationMethod, phoneAuthenticationMethod, emailAuthenticationMethod, softwareOathAuthenticationMethod, windowsHelloForBusinessAuthenticationMethod.
Find the gaps
# Users with only password
$weak = Get-MgUser -All -Property Id,UserPrincipalName | Where-Object {
$m = Get-MgUserAuthenticationMethod -UserId $_.Id
($m | Where-Object { $_.AdditionalProperties['@odata.type'] -ne '#microsoft.graph.passwordAuthenticationMethod' }).Count -eq 0
}
$weak | Select UserPrincipalName | Export-Csv mfa-missing.csv -NoType
# Users with only SMS
$smsOnly = Get-MgUser -All | Where-Object {
$m = Get-MgUserAuthenticationMethod -UserId $_.Id |
Where { $_.AdditionalProperties['@odata.type'] -notin
@('#microsoft.graph.passwordAuthenticationMethod') }
($m.Count -eq 1 -and $m[0].AdditionalProperties['@odata.type'] -eq '#microsoft.graph.phoneAuthenticationMethod')
}
Cross-check with sign-in logs
Registration is intent. Sign-in logs are reality. Pull the last 30 days of successful sign-ins and look at authenticationDetails:
Get-MgAuditLogSignIn -Filter "createdDateTime ge 2026-03-01 and status/errorCode eq 0" -All |
Select UserPrincipalName,
@{N='Method';E={ ($_.AuthenticationDetails | Select -Last 1).AuthenticationMethod }} |
Group-Object UserPrincipalName, Method | Sort Count -Descending
Conditional Access intersection
An MFA-registered user can still bypass MFA if no Conditional Access policy enforces it for them. Pull policies and intersect with users to find genuinely-protected vs registered-but-not-enforced:
Get-MgIdentityConditionalAccessPolicy -All |
Where State -eq 'enabled' |
Select DisplayName, @{N='IncludesAll';E={$_.Conditions.Users.IncludeUsers -contains 'All'}}
Export and report
Build one CSV with: UPN, registered methods (comma-joined), strongest method, last MFA method used, in-scope-of-CA-MFA-policy. That single sheet answers the auditor's question.
Audit checklist
- Zero users with only password (1 pt)
- Zero privileged users with phone-only (1 pt)
- FIDO2 / Authenticator coverage > 80% (1 pt)
- Conditional Access policy enforces MFA for All Users (1 pt)
- Sign-in log review run monthly (1 pt)
From audit to enforcement: rolling out FIDO2 and phishing-resistant MFA
Counting users with MFA is the easy part. The harder, more valuable work is moving the population from SMS and authenticator-push to phishing-resistant methods (FIDO2, Windows Hello for Business, Passkeys) and proving that the migration is complete.
Method-strength reporting
Not all MFA is equal. SMS can be SIM-swapped; push can be MFA-fatigued; FIDO2 cannot be phished. The userRegistrationDetails endpoint lets you classify the population by method strength, not just enrolment count.
$users = Get-MgReportAuthenticationMethodUserRegistrationDetail -All
$users | Group-Object {
if ($_.MethodsRegistered -contains 'fido2SecurityKey') { 'phishing-resistant' }
elseif ($_.MethodsRegistered -contains 'windowsHelloForBusiness') { 'phishing-resistant' }
elseif ($_.MethodsRegistered -contains 'passKeyDeviceBound') { 'phishing-resistant' }
elseif ($_.MethodsRegistered -contains 'microsoftAuthenticator') { 'app-strong' }
elseif ($_.MethodsRegistered -contains 'sms' -or
$_.MethodsRegistered -contains 'voice') { 'weak' }
else { 'none' }
} | Select-Object Name, Count
Conditional Access policies that match the audit
Pair the report with a Conditional Access policy that requires authentication strength = phishing-resistant for privileged roles. Start with Global Admins, then expand to all admin roles, then to high-risk apps. Export the policy via Graph and version-control it next to your scripts.
Get-MgIdentityConditionalAccessPolicy |
Where-Object DisplayName -like '*Phishing-Resistant*' |
ConvertTo-Json -Depth 10 |
Out-File C:\ca\phishing-resistant.json
Track break-glass accounts separately
Every tenant should have two break-glass accounts that are excluded from MFA and Conditional Access. Your audit script must list and verify them every run โ both that they exist, and that nothing else slipped into the exclusion list.
$bg = Get-MgIdentityConditionalAccessPolicy |
Select-Object -ExpandProperty Conditions |
Select-Object -ExpandProperty Users |
Select-Object -ExpandProperty ExcludeUsers -Unique
$bg | ForEach-Object {
Get-MgUser -UserId $_ | Select DisplayName, UserPrincipalName, AccountEnabled
}
Common pitfalls
- Counting "registered" as "enforced". A user can be registered for MFA and never prompted. Cross-reference with sign-in logs to confirm enforcement.
- Treating Per-User MFA and Security Defaults as equivalent. They are not. Per-User MFA is legacy; Security Defaults and Conditional Access are the modern path. Document which one your tenant uses.
- Excluding service accounts the wrong way. If a script account cannot do MFA, give it a managed identity or app registration with cert auth โ do not blanket-exclude from CA.
- SMS as the fallback. Even if SMS is not the primary method, a user with SMS in their list can downgrade to it. Disable SMS at the authentication-methods policy level when ready.
- No baseline export. Without yesterday's report you cannot tell whether the situation is improving. Schedule the audit weekly and store the output in source control.
Phishing-resistant MFA: a 90-day rollout plan
Once the audit identifies who has weak MFA, the rollout is a 90-day project with three concurrent workstreams: hardware procurement, communications, and policy enforcement. Run them in parallel; serialise nothing.
- Week 1โ2 โ pilot with security team. Issue FIDO2 keys to the security and IT teams. They register, switch to passkey sign-in, and document every snag they hit. Their feedback shapes the help-desk runbook.
- Week 3โ6 โ admin enforcement. Conditional Access policy: require phishing-resistant authentication strength for all directory roles. Notify each admin individually 7 days before enforcement. Excluded: two break-glass accounts, monitored.
- Week 7โ10 โ high-value users. Define "high-value" via business input โ finance, legal, executive assistants, source-code repository admins. Same enforcement, longer notification window (14 days).
- Week 11โ12 โ broad rollout. All users. Communicate via email + intranet + manager cascade. Stage a virtual help-desk session for two hours each weekday during the rollout.
- Day 90 โ measure. Re-run the audit. Target: 100% of admins on phishing-resistant; >95% of all users on phishing-resistant or app-strong; <5% on weak methods (and those documented as exceptions).
Procurement is often the gating item. Order keys two months ahead of the pilot โ global shortages are unpredictable. For users who refuse hardware, Windows Hello for Business and platform passkeys are equally phishing-resistant and require no shipping.
Track the uplift: the number of compromised accounts in the 12 months before rollout vs. the 12 months after. The chart is the easiest argument for the next security budget.
From audit to outcome: tracking the metrics that matter
An MFA audit that produces a CSV every week and never changes a number is theatre. The audits that move the needle live inside an outcome dashboard with three time-series and a small set of explicit targets, reviewed monthly with the security and IT leadership.
The first time-series is the phishing-resistant percentage by population segment. Plot the percentage of admins, of high-value users, and of all users registered for FIDO2, Windows Hello for Business or platform passkeys. The lines should trend monotonically upward; any flat or dropping segment surfaces a rollout that has lost momentum. Set explicit targets: 100% for admins by quarter end, 95% for high-value, 80% for general population by year end.
The second is the weak-method count. Users with SMS or voice as their only registered method are a finite, shrinking population. The chart of "users with SMS as primary method" trending toward zero is the easiest one-line story for an executive update. When it reaches zero, you remove SMS at the authentication-methods policy level and prevent the regression.
The third is the incident attribution: of the credential-related incidents in the last 12 months, what percentage involved a user who had only weak MFA at the time of the incident? The number should drop as the rollout progresses; if it does not, your phishing-resistant rollout is not yet covering the actually-targeted users and the prioritisation needs revision.
Pair these metrics with two operational signals: a daily report of new accounts created with no MFA registration (closes within 24 hours), and a weekly report of MFA registration changes (catches account takeover where the attacker registers their own device). Both run from the same audit script and add minutes, not hours, to the operations workload.
Once the dashboard is in place, the audit script is no longer the deliverable โ the trending chart is. The script is a tool; the chart is the change.
Field notes from real MFA audits
Auditing MFA across more than a handful of tenants reveals patterns that no documentation captures. The first surprise is always the same: the tenant administrator believes the rollout is in better shape than the data shows. The portal's compliance dashboard reports a healthy percentage of users registered for MFA, but the audit reveals that the headline number combines strong methods, weak methods, and stale registrations into a single optimistic figure. The first job of every audit is to separate them, because each subgroup has a different remediation path and a different urgency.
The second pattern is the gap between policy and reality. A Conditional Access policy that requires MFA for "all users" almost never covers all users in practice. Service accounts are excluded for legacy reasons; an executive's account was excluded during travel and never re-enabled; a guest-user policy quietly carved out an entire population because the original engineer was solving a different problem. None of these exclusions are visible from the policy summary view. They surface only when you join the policy export to the user directory and count what is actually covered. Doing this once per tenant is enlightening; doing it monthly is the difference between a maintained programme and a paper one.
The third pattern is the long tail of weak registrations. Most tenants have a small population of users โ typically less than five percent โ who registered SMS years ago and have never been pushed to upgrade. They are usually long-tenured, often privileged, and frequently the highest-value targets in the organisation. The audit makes this population visible. The communication and rollout work that follows is mostly social rather than technical: explaining to a finance director why the inconvenience of registering a new method is justified, scheduling a fifteen-minute walk-through, and confirming the registration completed within a week. The audit is the prompt; the conversation is the deliverable.
The fourth pattern, less common but more dangerous, is the silent registration change. An attacker who phishes a credential and gets through a weak factor will often add their own authenticator immediately, then sit on the account for weeks before acting. The audit's "registrations changed in the last seven days" view is the single most useful detection control for this pattern. A user who has not changed methods in two years and suddenly added a phone number from an unfamiliar country deserves a phone call from the security team within the hour. That phone call has, in real engagements, prevented six-figure fraud losses on more than one occasion.
The fifth pattern is reporting fatigue. The audit produces a CSV every week, and the recipient โ usually an overworked IT director โ stops opening it after the third week of "nothing new". The audits that survive long-term wrap the data in a one-paragraph summary that highlights only what changed since last week: new admins without strong MFA, users who downgraded their methods, registrations from new geographies. Three sentences read in fifteen seconds, with the full CSV one click away if the summary triggers a question. Reports designed for the reader's attention budget get read; reports designed only for completeness get filtered to a folder nobody opens.
Across every engagement, the audit script itself is rarely the hard part. Reading the data, communicating the implications, and sustaining the rhythm are the work that converts the script's output into measurable risk reduction.
FAQ
Why two cmdlets โ registered methods vs sign-in details?
Registration tells you what is possible. Sign-in details tell you what was actually used. Both are needed.
Does this require P2 licensing?
Sign-in logs are P1. Authentication methods report is P1. Conditional Access is P1.
What about per-user MFA legacy?
The legacy per-user MFA toggle still exists but Microsoft is moving everyone to CA. Audit both until per-user is fully retired in your tenant.
How long does Graph keep authentication-method history?
The userRegistrationDetails snapshot is current state, not history. For history use the Audit Log (auditLogs/directoryAudits) โ typically 30 days for E3 and 90 days for E5/Entra ID P2.
Can I report on per-user MFA status via Graph?
Only via the legacy MSOnline cmdlet today. Microsoft has roadmap items to expose it via Graph; until then keep one PowerShell 5.1 host with MSOnline for that single report.
What about external (B2B) guests?
Guests show in the report but their MFA may be enforced by their home tenant. Filter with userType eq 'Guest' and treat the count as informational, not a finding.
Does this audit cover Microsoft 365 admin centre access?
Yes โ admin portals authenticate against the same Entra ID. If a Global Admin lacks phishing-resistant MFA, the audit catches it regardless of how they sign in.