From 4c49f49f11cec183c974aca226286d1c99ca4657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0t=C4=9Bp=C3=A1n?= Date: Mon, 27 Jul 2026 16:53:43 +0200 Subject: [PATCH] Enhance user MFA method reporting by clarifying sign-in preference handling and adding module dependency checks --- Get-UserMfaMethodReport.ps1 | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/Get-UserMfaMethodReport.ps1 b/Get-UserMfaMethodReport.ps1 index dd08aa3..2726a6e 100644 --- a/Get-UserMfaMethodReport.ps1 +++ b/Get-UserMfaMethodReport.ps1 @@ -257,17 +257,24 @@ function Get-UserAuthMethodDetail { if ($friendly.Category -eq 'Phone') { $hasPhoneMethod = $true } } - # Best-effort default/preferred method. This endpoint reflects the user's - # own sign-in preference and, unlike the tenant-wide usage/insights report, - # is not gated behind a P1/P2 license - but treat failures as "unknown" - # rather than fatal, since behavior can vary by tenant/SDK version. + # Best-effort default/preferred method. This endpoint only reflects an + # explicit override the user (or an admin) has set - unlike the + # tenant-wide usage/insights report, it's not gated behind a P1/P2 + # license - but treat failures as "unknown" rather than fatal, since + # behavior can vary by tenant/SDK version. Deliberately NOT guessed/ + # inferred when unavailable: Entra ID's actual method selection (e.g. + # under System-preferred MFA) depends on per-user state this API doesn't + # expose, so a guess here can be flatly wrong and is worse than admitting + # it's unknown - this value drives real remediation decisions. $defaultMethod = $null try { $preference = Get-MgBetaUserAuthenticationSignInPreference -UserId $UserId -ErrorAction Stop - $defaultMethod = $preference.UserPreferredMethodForSecondaryAuthentication + if ($preference.UserPreferredMethodForSecondaryAuthentication -and $preference.UserPreferredMethodForSecondaryAuthentication -ne 'none') { + $defaultMethod = $preference.UserPreferredMethodForSecondaryAuthentication + } } catch { - Write-Verbose "No sign-in preference available for user '$UserId' (falling back to heuristic): $_" + Write-Warning "No sign-in preference available for user '$UserId': $_" } return [PSCustomObject]@{ @@ -403,6 +410,25 @@ $($methodRows -join "`n") # --- Main -------------------------------------------------------------- try { + # Fail fast and loudly if a module this script depends on isn't + # installed OR can't actually be loaded (e.g. an assembly-version + # conflict with another installed Microsoft.Graph module version), + # rather than letting every per-user call into it fail silently later + # and produce an incomplete/misleading report. Microsoft.Graph.Beta. + # Identity.SignIns provides Get-MgBetaUserAuthenticationSignInPreference, + # which reads each user's actual default sign-in method - there is no + # v1.0/GA equivalent, this data is beta-only in Microsoft Graph. + $requiredBetaModule = 'Microsoft.Graph.Beta.Identity.SignIns' + if (-not (Get-Module -ListAvailable -Name $requiredBetaModule)) { + throw "Required module '$requiredBetaModule' is not installed. It's needed to read each user's actual default sign-in method. Install it with: Install-Module $requiredBetaModule -Scope CurrentUser" + } + try { + Import-Module -Name $requiredBetaModule -ErrorAction Stop + } + catch { + throw "Required module '$requiredBetaModule' is installed but failed to load - this is usually caused by multiple mismatched Microsoft.Graph module versions installed side by side (an assembly-version conflict), not a missing module. Run: Get-Module -ListAvailable 'Microsoft.Graph*' | Group-Object Name | Where-Object { (`$_.Group.Version | Sort-Object -Unique).Count -gt 1 } to find the mismatched ones, update them all to the same version, and remove the old ones. Underlying error: $_" + } + Write-Verbose 'Connecting to Microsoft Graph...' $requiredScopes = @( 'User.Read.All' @@ -473,7 +499,13 @@ try { $_ -notin @('mobilePhone', 'alternateMobilePhone', 'officePhone', 'email', 'password') } $hasStrongMethod = [bool]$strongRegistered + # This report field is also "none" (a literal string, not $null) + # for users without an explicit override - most commonly because + # the tenant runs System-preferred MFA. Normalize it so the + # fallback heuristic below actually kicks in instead of the row + # showing the literal word "none" as if it were a real method. $defaultMethod = $regDetail.UserPreferredMethodForSecondaryAuthentication + if ($defaultMethod -eq 'none') { $defaultMethod = $null } } else { # Fallback path: per-user methods API, works without P1/P2.