Public/Get-IOUsersWithoutMFA.ps1
|
function Get-IOUsersWithoutMFA { <# .SYNOPSIS Lists users who have no MFA authentication methods registered. .EXAMPLE Get-IOUsersWithoutMFA .EXAMPLE Get-IOUsersWithoutMFA -IncludeGuests -ToCsv "no-mfa.csv" .EXAMPLE Get-IOUsersWithoutMFA -AdminsOnly -ToCsv "admins-no-mfa.csv" #> [CmdletBinding()] param( [switch]$IncludeGuests, [switch]$AdminsOnly, [ValidateRange(1, 20)] [int]$BatchSize = 5, [string]$ToCsv ) $cmdName = $MyInvocation.MyCommand.Name Write-IOLog 'Scanning users for MFA registration status...' -Level Info -Component $cmdName $results = [System.Collections.Generic.List[PSCustomObject]]::new() # Build user query — proper URL separator handling $selectFields = 'id,displayName,userPrincipalName,userType,accountEnabled' if ($IncludeGuests) { $uri = "v1.0/users?`$select=$selectFields" } else { $uri = "v1.0/users?`$filter=userType eq 'Member'&`$select=$selectFields" } $users = Invoke-IOGraphRequest -Uri $uri # If AdminsOnly, get directory role members first $adminUserIds = $null if ($AdminsOnly) { Write-IOLog 'Filtering to admin role holders only...' -Level Verbose -Component $cmdName $adminUserIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $roles = Invoke-IOGraphRequest -Uri 'v1.0/directoryRoles?$select=id,displayName' -NoPagination -SkipConnectionCheck foreach ($role in $roles) { try { $members = Invoke-IOGraphRequest -Uri "v1.0/directoryRoles/$($role.id)/members?`$select=id" -NoPagination -SkipConnectionCheck foreach ($m in $members) { [void]$adminUserIds.Add($m.id) } } catch { } } Write-IOLog "Found $($adminUserIds.Count) admin role holder(s)." -Level Verbose -Component $cmdName } # Filter to eligible users first (before making per-user API calls) $eligibleUsers = [System.Collections.Generic.List[object]]::new() foreach ($user in $users) { if (-not $user.accountEnabled) { continue } if ($AdminsOnly -and -not $adminUserIds.Contains($user.id)) { continue } $eligibleUsers.Add($user) } $total = $eligibleUsers.Count if ($total -eq 0) { Write-IOLog 'No eligible users found matching criteria.' -Level Info -Component $cmdName return } Write-IOLog "Checking MFA methods for $total eligible user(s)..." -Level Info -Component $cmdName $counter = 0 $errors = 0 $maxErrors = [math]::Max(50, [math]::Floor($total * 0.1)) # Circuit breaker: stop if >10% fail try { # Process in batches to reduce throttling impact for ($i = 0; $i -lt $total; $i += $BatchSize) { $batchEnd = [math]::Min($BatchSize, $total - $i) $batch = $eligibleUsers.GetRange($i, $batchEnd) foreach ($user in $batch) { $counter++ if ($counter % 25 -eq 0 -or $counter -eq $total) { Write-Progress -Activity 'Checking MFA methods' -Status "$counter / $total users (errors: $errors)" -PercentComplete (($counter / $total) * 100) } try { $methods = Invoke-IOGraphRequest -Uri "v1.0/users/$($user.id)/authentication/methods" -NoPagination -SkipConnectionCheck } catch { $errors++ Write-IOLog "Could not read auth methods for $($user.userPrincipalName): $($_.Exception.Message)" -Level Verbose -Component $cmdName # Circuit breaker — abort if too many errors (likely permissions issue) if ($errors -ge $maxErrors) { Write-IOLog "Circuit breaker triggered: $errors errors out of $counter attempts. Aborting. Check UserAuthenticationMethod.Read.All permission." -Level Warning -Component $cmdName break } continue } # Classify methods — password-only means no MFA $mfaMethods = @($methods | Where-Object { $_.'@odata.type' -and $_.'@odata.type' -notin @( '#microsoft.graph.passwordAuthenticationMethod', '#microsoft.graph.emailAuthenticationMethod' ) }) if ($mfaMethods.Count -eq 0) { $results.Add([PSCustomObject]@{ DisplayName = $user.displayName UserPrincipalName = $user.userPrincipalName ObjectId = $user.id UserType = $user.userType RegisteredMethods = 'Password Only' MFAMethodCount = 0 Risk = if ($AdminsOnly -or ($adminUserIds -and $adminUserIds.Contains($user.id))) { 'Critical' } else { 'High' } }) } } # Throttle-friendly delay between batches for large tenants if ($total -gt 100 -and ($i + $BatchSize) -lt $total) { Start-Sleep -Milliseconds 200 } } } finally { Write-Progress -Activity 'Checking MFA methods' -Completed } if ($errors -gt 0) { Write-IOLog "Completed with $errors error(s) out of $counter user(s) checked." -Level Warning -Component $cmdName } $sorted = $results | Sort-Object Risk, DisplayName Export-IOResult -Data $sorted -ToCsv $ToCsv -CommandName $cmdName } |