public/Get-MsecEntraAuthenticationMethod.ps1
|
function Get-MsecEntraAuthenticationMethod { <# .SYNOPSIS Which authentication methods the tenant permits, who they are available to, and which of them are phishing-resistant - what "require MFA" actually means here. .DESCRIPTION Every MFA requirement in the tenant - Conditional Access, PIM activation, step-up on a privileged role - resolves to "satisfy one of the enabled methods". This says what that set is. A tenant can require MFA everywhere and still accept a code sent by email. STATE ALONE IS MISLEADING AND THIS IS THE COMMAND'S MAIN POINT. A method reads 'enabled' while being scoped to a single group, so it is enabled for the tenant and available to almost nobody. Measured on one tenant: Fido2 showed state 'enabled' with includeTargets of exactly one group - the emergency-access accounts, two people - while SMS, Voice and Email were enabled for all users. Reporting the state column alone would have described that tenant as having FIDO2. AvailableToAllUsers is therefore the column to read: true only when the method is enabled AND includes the built-in 'all_users' target. IncludeTarget and ExcludeTarget carry the detail, resolved to group names. NOT EVERY ENABLED METHOD CAN SATISFY MFA. Email OTP is usable for B2B guest redemption and self-service password reset, not as a second factor for a member account; QR code PIN and Temporary Access Pass are onboarding and recovery credentials. The command reports what the tenant permits, which is a wider set than what satisfies an MFA grant. Treat -PhishableOnly as "enabled and not phishing-resistant", not as "will satisfy a Conditional Access MFA control". PHISHING RESISTANCE IS MICROSOFT'S CLASSIFICATION, NOT A JUDGEMENT MADE HERE. Microsoft documents FIDO2/passkeys, Windows Hello for Business and certificate-based authentication as phishing-resistant; everything else - SMS, voice, email, software OATH and Authenticator push - is not. IsPhishingResistant reports that mapping and nothing more. A false there is not an accusation that the method is broken; it means a credential-phishing or SIM-swap attack can satisfy it. A DISABLED METHOD IS STILL RETURNED, with State 'disabled'. Dropping it would make "we considered this and turned it off" indistinguishable from "this does not exist", and the disabled list is where TemporaryAccessPass and X509Certificate live - the two a tenant hardening its privileged access usually wants to turn ON. WINDOWS HELLO FOR BUSINESS IS NOT IN THIS POLICY AND SO IS NOT RETURNED. It is governed by device configuration (Intune/group policy), not by the authentication methods policy, so the Graph response has no row for it at all. It is also phishing-resistant and, on the tenant this was built against, the most widely held strong credential there is - 118 registrations. Reading this command's output as the complete list of phishing-resistant coverage would therefore understate it badly. Pair it with Get-MsecEntraMfaRegistration, which reports what users have actually registered. A SCOPED METHOD DOES NOT REVOKE EXISTING REGISTRATIONS. Targeting controls who may REGISTER a method, not who may use one already held. On the same tenant Fido2 was scoped to the break-glass group while 13 users held synced passkeys and 16 held macOS secure enclave keys - registered before the scope narrowed, and still usable. So a narrow IncludeTarget means "no new enrolments", not "nobody has this". Runs as the msec app. Needs 'Policy.Read.All', which the app already holds. Group names are resolved with Group.Read.All; where a group cannot be resolved its id is reported rather than a blank, because an unnamed target is still a target. .PARAMETER EnabledOnly Only methods currently enabled. .PARAMETER PhishableOnly Only enabled methods that are NOT phishing-resistant. Note this is "enabled and not phishing-resistant", not "satisfies an MFA grant" - see the description on Email OTP. .EXAMPLE Get-MsecEntraAuthenticationMethod Every method, its state, who it reaches, and whether it resists phishing. .EXAMPLE Get-MsecEntraAuthenticationMethod -PhishableOnly What "require MFA" can actually be satisfied with by someone who has phished a password. .EXAMPLE Get-MsecEntraAuthenticationMethod | Where-Object { $_.State -eq 'enabled' -and -not $_.AvailableToAllUsers } Methods that are on, but reach only a named group - often a pilot that never widened. .OUTPUTS PSCustomObject per method, PSTypeName 'MsecEntraAuthenticationMethod'. .NOTES Read-only. Read alongside Get-MsecEntraConditionalAccessPolicy and Get-MsecEntraPimPolicy: those say where MFA is demanded, this says what will satisfy it. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [switch] $EnabledOnly, [switch] $PhishableOnly ) Assert-MsecSession try { $policy = Invoke-MsecGraphRequest -Path '/v1.0/policies/authenticationMethodsPolicy' } catch { if ($_.Exception.Message -match '403|Forbidden') { throw "Forbidden reading the authentication methods policy. The msec app needs 'Policy.Read.All' (admin consent required). Original error: $($_.Exception.Message)" } throw } # Microsoft's own classification. Anything not on this list is satisfiable by a # credential-phishing or SIM-swap attack, which is the distinction the column exists for. $phishingResistant = @('Fido2', 'X509Certificate', 'WindowsHelloForBusiness') # Group names are resolved once and cached: several methods target the same groups, and an # id in the output helps nobody. $groupNames = @{} $resolve = { param([string] $Id) if (-not $Id) { return $null } # The built-in sentinel for "everyone" - not a real group, and looking it up 404s. if ($Id -eq 'all_users') { return 'All users' } if ($groupNames.ContainsKey($Id)) { return $groupNames[$Id] } $name = $Id try { $name = [string](Invoke-MsecGraphRequest -Path "/v1.0/groups/$Id`?`$select=displayName").displayName } catch { Write-Verbose "Could not resolve group $Id" } if (-not $name) { $name = $Id } $groupNames[$Id] = $name $name } foreach ($c in @($policy.authenticationMethodConfigurations)) { $id = [string]$c.id $state = [string]$c.state if ($EnabledOnly -and $state -ne 'enabled') { continue } $resistant = $id -in $phishingResistant if ($PhishableOnly -and ($resistant -or $state -ne 'enabled')) { continue } $include = @(foreach ($t in @($c.includeTargets)) { & $resolve ([string]$t.id) }) $exclude = @(foreach ($t in @($c.excludeTargets)) { & $resolve ([string]$t.id) }) # The whole point of the command: enabled AND reaching everyone. A method enabled for # one pilot group is not a method the tenant has. $toAll = ($state -eq 'enabled') -and (@($c.includeTargets | Where-Object { [string]$_.id -eq 'all_users' }).Count -gt 0) [PSCustomObject]@{ PSTypeName = 'MsecEntraAuthenticationMethod' Method = $id State = $state # True only when enabled and targeted at all_users - see the description. AvailableToAllUsers = $toAll # Microsoft's classification, not a judgement made here. IsPhishingResistant = $resistant IncludeTarget = @($include) ExcludeTarget = @($exclude) # Registration being self-service matters for the methods that are: a user can # add a weak method themselves if it is enabled for them. SelfServiceRegistration = if ($null -ne $c.isSelfServiceRegistrationAllowed) { [bool]$c.isSelfServiceRegistrationAllowed } else { $null } Raw = $c } } } |