Src/Private/Get-AbrEntraIDAuthMethods.ps1
|
function Get-AbrEntraIDAuthMethods { <# .SYNOPSIS Documents the Entra ID Authentication Methods Policy configuration. .DESCRIPTION Collects and reports on: - Authentication Methods Policy overview (which methods are enabled tenant-wide) - Per-method configuration detail (target groups, restrictions, settings) - Per-user registered authentication methods - Temporary Access Pass (TAP) policy - FIDO2 security key policy - Microsoft Authenticator policy (number matching, additional context) - Password policy (smart lockout) .NOTES Version: 0.1.20 Author: Pai Wei Sing #> [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory)] [string]$TenantId ) begin { Write-PScriboMessage -Message "Collecting Entra ID Authentication Methods for tenant $TenantId." Show-AbrDebugExecutionTime -Start -TitleMessage 'Authentication Methods' } process { #region Authentication Methods Section -Style Heading2 'Authentication Methods' { Paragraph "The following section documents the Authentication Methods Policy configured in tenant $TenantId." BlankLine #region Authentication Methods Policy -- method enablement overview try { Write-Host " - Retrieving Authentication Methods Policy..." $AuthPolicy = Get-MgPolicyAuthenticationMethodPolicy -ErrorAction Stop # Detect system-preferred MFA setting (systemCredentialPreferences) $SystemPreferredMfaEnabled = $false $SystemPreferredMfaState = 'Not configured' try { $SysPref = Invoke-MgGraphRequest -Method GET ` -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' ` -ErrorAction SilentlyContinue if ($SysPref -and $SysPref.systemCredentialPreferences) { $state = $SysPref.systemCredentialPreferences.state $SystemPreferredMfaEnabled = ($state -eq 'enabled') $SystemPreferredMfaState = if ($SystemPreferredMfaEnabled) { 'Enabled [OK]' } else { "Disabled [$state] -- [WARN] Enable so users are always prompted for their strongest method" } } } catch { $SystemPreferredMfaState = 'Unable to retrieve (requires beta endpoint)' } if ($AuthPolicy -and $AuthPolicy.AuthenticationMethodConfigurations) { # Security classification for each method -- drives the Security Rating column # and the HealthCheck colour-coding. Kept in one place so updating is easy. $MethodMeta = @{ 'microsoftAuthenticator' = @{ Name = 'Microsoft Authenticator'; Rating = 'Strong'; Recommendation = 'Enable -- strong MFA, supports number matching to prevent push-fatigue attacks.' } 'fido2' = @{ Name = 'FIDO2 Security Keys'; Rating = 'Phishing-Resistant'; Recommendation = 'Enable -- phishing-resistant, highest assurance. Required for ACSC E8 ML3.' } 'x509Certificate' = @{ Name = 'Certificate-Based Auth (CBA)'; Rating = 'Phishing-Resistant'; Recommendation = 'Enable if PKI infrastructure exists -- phishing-resistant, meets E8 ML3.' } 'temporaryAccessPass' = @{ Name = 'Temporary Access Pass (TAP)'; Rating = 'Strong (Temporary)'; Recommendation = 'Enable -- time-limited bootstrap token. Restrict to admins via group target.' } 'softwareOath' = @{ Name = 'Software OATH Tokens'; Rating = 'Moderate'; Recommendation = 'Acceptable -- TOTP is stronger than SMS but weaker than Authenticator push.' } 'hardwareOath' = @{ Name = 'Hardware OATH Tokens'; Rating = 'Moderate'; Recommendation = 'Acceptable -- physical TOTP token. Consider FIDO2 keys as a superior alternative.' } 'email' = @{ Name = 'Email OTP'; Rating = 'WEAK -- Disable'; Recommendation = 'DISABLE -- email is not a secure second factor. Email accounts are frequently compromised.' } 'sms' = @{ Name = 'SMS (Text Message)'; Rating = 'WEAK -- Disable'; Recommendation = 'DISABLE -- susceptible to SIM-swap and SS7 interception attacks.' } 'voice' = @{ Name = 'Voice Call'; Rating = 'WEAK -- Disable'; Recommendation = 'DISABLE -- same attack surface as SMS; voice calls can be intercepted or social-engineered.' } 'qrCodePin' = @{ Name = 'QR Code + PIN'; Rating = 'Moderate'; Recommendation = 'Limited availability -- designed for frontline workers without personal devices. Restrict scope carefully.' } 'windowsHelloForBusiness'= @{ Name = 'Windows Hello for Business'; Rating = 'Phishing-Resistant'; Recommendation = 'Enable -- phishing-resistant biometric/PIN tied to device. Meets E8 ML3.' } } $PolicyObj = [System.Collections.ArrayList]::new() foreach ($Method in ($AuthPolicy.AuthenticationMethodConfigurations | Sort-Object Id)) { $State = $Method.State $Meta = if ($MethodMeta.ContainsKey($Method.Id)) { $MethodMeta[$Method.Id] } else { $null } $MethodName = if ($Meta) { $Meta.Name } else { $Method.Id } $SecurityRating = if ($Meta) { $Meta.Rating } else { 'Unknown' } $Recommendation = if ($Meta) { $Meta.Recommendation } else { 'Review this method and assess necessity.' } # Determine target scope $IncludeTargets = if ($Method.AdditionalProperties.includeTargets) { $TargetList = $Method.AdditionalProperties.includeTargets if ($TargetList | Where-Object { $_.id -eq 'all_users' }) { 'All Users' } else { "$(@($TargetList).Count) group(s)" } } else { 'N/A' } $policyInObj = [ordered] @{ 'Authentication Method' = $MethodName 'State' = if ($State -eq 'enabled') { 'Enabled' } else { 'Disabled' } 'Security Rating' = $SecurityRating 'Scope / Targets' = $IncludeTargets 'Recommendation' = $Recommendation } $PolicyObj.Add([pscustomobject]$policyInObj) | Out-Null } $null = (& { if ($HealthCheck.EntraID.MFA) { # Weak methods enabled = Warning (orange) $null = ($PolicyObj | Where-Object { $_.State -eq 'Enabled' -and $_.'Security Rating' -like 'WEAK*' } | Set-Style -Style Warning | Out-Null) # Strong/phishing-resistant methods disabled = Warning $null = ($PolicyObj | Where-Object { $_.State -eq 'Disabled' -and $_.'Security Rating' -in @('Strong','Phishing-Resistant') } | Set-Style -Style Warning | Out-Null) # Phishing-resistant methods enabled = OK (green) $null = ($PolicyObj | Where-Object { $_.State -eq 'Enabled' -and $_.'Security Rating' -eq 'Phishing-Resistant' } | Set-Style -Style OK | Out-Null) } }) # ColumnWidths: Method(22) + State(10) + Rating(18) + Scope(12) + Recommendation(38) = 100 $PolicyTableParams = @{ Name = "Authentication Methods Policy - $TenantId"; List = $false; ColumnWidths = 22, 10, 18, 12, 38 } if ($Report.ShowTableCaptions) { $PolicyTableParams['Caption'] = "- $($PolicyTableParams.Name)" } $PolicyObj | Table @PolicyTableParams # Store for Excel export $null = ($script:ExcelSheets['Auth Methods Policy'] = $PolicyObj) #region ACSC E8 + CIS Authentication Methods Assessment $null = (& { if ($script:IncludeACSCe8) { BlankLine Paragraph "ACSC Essential Eight Maturity Level Assessment -- Authentication Methods:" BlankLine }}) $null = (& { if ($script:IncludeCISBaseline) { BlankLine Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment -- Authentication Methods:" BlankLine }}) # Derive compliance check variables from $PolicyObj $StrongMethodCount = if ($PolicyObj) { @($PolicyObj | Where-Object { $_.'Security Rating' -in @('Strong','Strong (Temporary)','Phishing-Resistant') -and $_.State -eq 'Enabled' }).Count } else { 0 } $AuthenticatorEnabled = ($PolicyObj | Where-Object { $_.'Authentication Method' -eq 'Microsoft Authenticator' -and $_.State -eq 'Enabled' }) -ne $null $AuthenticatorState = if ($AuthenticatorEnabled) { 'Enabled' } else { 'Disabled' } $SmsEnabled = ($PolicyObj | Where-Object { $_.'Authentication Method' -eq 'SMS (Text Message)' -and $_.State -eq 'Enabled' }) -ne $null $VoiceEnabled = ($PolicyObj | Where-Object { $_.'Authentication Method' -eq 'Voice Call' -and $_.State -eq 'Enabled' }) -ne $null $EmailOtpEnabled = ($PolicyObj | Where-Object { $_.'Authentication Method' -eq 'Email OTP' -and $_.State -eq 'Enabled' }) -ne $null $Fido2Enabled = ($PolicyObj | Where-Object { $_.'Authentication Method' -eq 'FIDO2 Security Keys' -and $_.State -eq 'Enabled' }) -ne $null $CbaEnabled = ($PolicyObj | Where-Object { $_.'Authentication Method' -eq 'Certificate-Based Auth (CBA)' -and $_.State -eq 'Enabled' }) -ne $null $SmsState = if ($SmsEnabled) { 'Enabled' } else { 'Disabled' } $VoiceState = if ($VoiceEnabled) { 'Enabled' } else { 'Disabled' } $EmailOtpState = if ($EmailOtpEnabled) { 'Enabled' } else { 'Disabled' } $Fido2State = if ($Fido2Enabled) { 'Enabled' } else { 'Disabled' } $CbaState = if ($CbaEnabled) { 'Enabled' } else { 'Disabled' } # Number matching $NumberMatchEnabled = $false; $NumberMatchState = 'Not Configured' try { $AuthPol2 = Get-MgPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration ` -AuthenticationMethodConfigurationId 'microsoftAuthenticator' -ErrorAction SilentlyContinue if ($AuthPol2) { $nmState = $AuthPol2.AdditionalProperties.featureSettings.numberMatchingRequiredState.state $NumberMatchEnabled = ($nmState -eq 'enabled') $NumberMatchState = if ($NumberMatchEnabled) { 'Enabled' } else { if ($nmState) { $nmState } else { 'Not Configured' } } } } catch {} # System-preferred MFA $SystemPreferredMfaEnabled = $false; $SystemPreferredMfaState = 'Not Configured' try { $RegEnf = $AuthPolicy.AdditionalProperties.registrationEnforcement if ($RegEnf) { $sp = $RegEnf.authenticationMethodsRegistrationCampaign.state $SystemPreferredMfaEnabled = ($sp -eq 'enabled') $SystemPreferredMfaState = if ($SystemPreferredMfaEnabled) { 'Enabled [OK]' } else { 'Disabled [WARN]' } } } catch {} $_ComplianceVars = @{ 'StrongMethodCount' = $StrongMethodCount 'AuthenticatorEnabled' = $AuthenticatorEnabled 'AuthenticatorState' = $AuthenticatorState 'SmsEnabled' = $SmsEnabled 'VoiceEnabled' = $VoiceEnabled 'EmailOtpEnabled' = $EmailOtpEnabled 'SmsState' = $SmsState 'VoiceState' = $VoiceState 'EmailOtpState' = $EmailOtpState 'NumberMatchEnabled' = $NumberMatchEnabled 'NumberMatchState' = $NumberMatchState 'Fido2Enabled' = $Fido2Enabled 'CbaEnabled' = $CbaEnabled 'Fido2State' = $Fido2State 'CbaState' = $CbaState 'SystemPreferredMfaEnabled' = $SystemPreferredMfaEnabled 'SystemPreferredMfaState' = $SystemPreferredMfaState } $null = (& { if ($script:IncludeACSCe8) { $E8AuthChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrE8Checks -Section 'AuthMethods') ` -Framework E8 ` -CallerVariables $_ComplianceVars New-AbrE8AssessmentTable -Checks $E8AuthChecks -Name 'Authentication Methods' -TenantId $TenantId if ($E8AuthChecks) { $null = $script:E8AllChecks.AddRange([object[]](@($E8AuthChecks | Select-Object @{N='Section';E={'Authentication Methods'}}, ML, Control, Status, Detail ))) } }}) $null = (& { if ($script:IncludeCISBaseline) { $CISAuthChecks = Build-AbrComplianceChecks ` -Definitions (Get-AbrCISChecks -Section 'AuthMethods') ` -Framework CIS ` -CallerVariables $_ComplianceVars New-AbrCISAssessmentTable -Checks $CISAuthChecks -Name 'Authentication Methods' -TenantId $TenantId if ($CISAuthChecks) { $null = $script:CISAllChecks.AddRange([object[]](@($CISAuthChecks | Select-Object @{N='Section';E={'Authentication Methods'}}, CISControl, Level, Status, Detail ))) } }}) #endregion } } catch { Write-AbrSectionError -Section 'Authentication Methods Policy' -Message "$($_.Exception.Message)" Paragraph "Authentication Methods Policy could not be retrieved. Ensure Policy.Read.All permission is granted." } #endregion #region Microsoft Authenticator Detail if ($InfoLevel.AuthenticationMethods -ge 2) { try { $AuthenticatorPolicy = Get-MgPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration ` -AuthenticationMethodConfigurationId 'microsoftAuthenticator' ` -ErrorAction SilentlyContinue if ($AuthenticatorPolicy) { Section -Style Heading3 'Microsoft Authenticator Configuration' { Paragraph "Detailed configuration of the Microsoft Authenticator method in tenant $TenantId." BlankLine $AuthApp = $AuthenticatorPolicy.AdditionalProperties $AuthObj = [System.Collections.ArrayList]::new() $authInObj = [ordered] @{ 'State' = $AuthenticatorPolicy.State 'Number Matching' = if ($AuthApp.featureSettings.numberMatchingRequiredState.state) { $AuthApp.featureSettings.numberMatchingRequiredState.state } else { 'Not Configured' } 'Show App Name (Additional Context)' = if ($AuthApp.featureSettings.displayAppInformationRequiredState.state) { $AuthApp.featureSettings.displayAppInformationRequiredState.state } else { 'Not Configured' } 'Show Geographic Location' = if ($AuthApp.featureSettings.displayLocationInformationRequiredState.state) { $AuthApp.featureSettings.displayLocationInformationRequiredState.state } else { 'Not Configured' } } $AuthObj.Add([pscustomobject]$authInObj) | Out-Null $null = (& { if ($HealthCheck.EntraID.MFA) { $null = ($AuthObj | Where-Object { $_.'Number Matching' -in @('disabled', 'Not Configured') } | Set-Style -Style Warning | Out-Null) } }) $AuthTableParams = @{ Name = "Microsoft Authenticator Settings - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($Report.ShowTableCaptions) { $AuthTableParams['Caption'] = "- $($AuthTableParams.Name)" } $AuthObj | Table @AuthTableParams } } } catch { Write-AbrSectionError -Section 'Authenticator Policy Detail' -Message "$($_.Exception.Message)" } #region FIDO2 Detail try { $Fido2Policy = Get-MgPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration ` -AuthenticationMethodConfigurationId 'fido2' ` -ErrorAction SilentlyContinue if ($Fido2Policy) { Section -Style Heading3 'FIDO2 Security Key Configuration' { Paragraph "Detailed configuration of FIDO2 security keys in tenant $TenantId." BlankLine $Fido2Props = $Fido2Policy.AdditionalProperties $Fido2Obj = [System.Collections.ArrayList]::new() $fido2InObj = [ordered] @{ 'State' = $Fido2Policy.State 'Self-Service Setup Allowed' = if ($null -ne $Fido2Props.isSelfServiceRegistrationAllowed) { $Fido2Props.isSelfServiceRegistrationAllowed } else { '--' } 'Enforce Attestation' = if ($null -ne $Fido2Props.isAttestationEnforced) { $Fido2Props.isAttestationEnforced } else { '--' } 'Key Restriction Enforced' = if ($null -ne $Fido2Props.keyRestrictions.isEnforced) { $Fido2Props.keyRestrictions.isEnforced } else { '--' } 'Restriction Enforcement Type' = if ($Fido2Props.keyRestrictions.enforcementType) { $Fido2Props.keyRestrictions.enforcementType } else { 'N/A' } 'Allowed / Blocked AAGUIDs' = if ($Fido2Props.keyRestrictions.aaGuids) { ($Fido2Props.keyRestrictions.aaGuids -join ', ') } else { 'None' } } $Fido2Obj.Add([pscustomobject](ConvertTo-HashToYN $fido2InObj)) | Out-Null $Fido2TableParams = @{ Name = "FIDO2 Policy Settings - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($Report.ShowTableCaptions) { $Fido2TableParams['Caption'] = "- $($Fido2TableParams.Name)" } $Fido2Obj | Table @Fido2TableParams } } } catch { Write-AbrSectionError -Section 'FIDO2 Policy Detail' -Message "$($_.Exception.Message)" } #endregion #region Temporary Access Pass Detail try { $TapPolicy = Get-MgPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration ` -AuthenticationMethodConfigurationId 'temporaryAccessPass' ` -ErrorAction SilentlyContinue if ($TapPolicy) { Section -Style Heading3 'Temporary Access Pass (TAP) Configuration' { Paragraph "Configuration of Temporary Access Pass in tenant $TenantId." BlankLine $TapProps = $TapPolicy.AdditionalProperties $TapObj = [System.Collections.ArrayList]::new() $tapInObj = [ordered] @{ 'State' = $TapPolicy.State 'Default Lifetime (minutes)' = if ($TapProps.defaultLifetimeInMinutes) { $TapProps.defaultLifetimeInMinutes } else { '--' } 'Minimum Lifetime (minutes)' = if ($TapProps.minimumLifetimeInMinutes) { $TapProps.minimumLifetimeInMinutes } else { '--' } 'Maximum Lifetime (minutes)' = if ($TapProps.maximumLifetimeInMinutes) { $TapProps.maximumLifetimeInMinutes } else { '--' } 'Default One-Time Use' = if ($null -ne $TapProps.isUsableOnce) { $TapProps.isUsableOnce } else { '--' } } $TapObj.Add([pscustomobject](ConvertTo-HashToYN $tapInObj)) | Out-Null $TapTableParams = @{ Name = "TAP Policy Settings - $TenantId"; List = $true; ColumnWidths = 55, 45 } if ($Report.ShowTableCaptions) { $TapTableParams['Caption'] = "- $($TapTableParams.Name)" } $TapObj | Table @TapTableParams } } } catch { Write-AbrSectionError -Section 'TAP Policy Detail' -Message "$($_.Exception.Message)" } #endregion } #endregion #region Per-User Registered Authentication Methods if ($InfoLevel.AuthenticationMethods -ge 2) { try { Write-Host " - Retrieving per-user registered authentication methods (may take time for large tenants)..." $AllUsers = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,UserType,AccountEnabled -ErrorAction Stop | Where-Object { $_.UserType -eq 'Member' -and $_.AccountEnabled -eq $true } $UserMethodObj = [System.Collections.ArrayList]::new() foreach ($User in ($AllUsers | Sort-Object DisplayName)) { try { $UserMethods = Get-MgUserAuthenticationMethod -UserId $User.Id -ErrorAction SilentlyContinue $MethodNames = [System.Collections.ArrayList]::new() foreach ($Method in $UserMethods) { $OdataType = $Method.AdditionalProperties['@odata.type'] $FriendlyName = switch ($OdataType) { '#microsoft.graph.microsoftAuthenticatorAuthenticationMethod' { 'Microsoft Authenticator' } '#microsoft.graph.phoneAuthenticationMethod' { 'Phone (SMS/Voice)' } '#microsoft.graph.fido2AuthenticationMethod' { 'FIDO2 Security Key' } '#microsoft.graph.passwordAuthenticationMethod' { 'Password' } '#microsoft.graph.softwareOathAuthenticationMethod' { 'Software OATH Token' } '#microsoft.graph.temporaryAccessPassAuthenticationMethod' { 'Temporary Access Pass' } '#microsoft.graph.windowsHelloForBusinessAuthenticationMethod' { 'Windows Hello for Business' } '#microsoft.graph.emailAuthenticationMethod' { 'Email OTP' } default { $OdataType } } $null = $MethodNames.Add($FriendlyName) } # Remove 'Password' from method list for cleaner output -- it's always present $MfaMethods = $MethodNames | Where-Object { $_ -ne 'Password' } $userMethodInObj = [ordered] @{ 'Display Name' = $User.DisplayName 'UPN' = $User.UserPrincipalName 'Registered Methods' = if ($MfaMethods) { ($MfaMethods | Select-Object -Unique) -join ', ' } else { 'None' } 'Method Count' = @($MfaMethods | Select-Object -Unique).Count 'Has MFA Method' = ($MfaMethods -and @($MfaMethods | Select-Object -Unique).Count -gt 0) } $UserMethodObj.Add([pscustomobject](ConvertTo-HashToYN $userMethodInObj)) | Out-Null } catch { Write-PScriboMessage -IsWarning -Message "Auth methods for '$($User.DisplayName)': $($_.Exception.Message)" } } if ($UserMethodObj.Count -gt 0) { Section -Style Heading3 'Registered Authentication Methods per User' { Paragraph "The following table lists the authentication methods registered by each enabled member user in tenant $TenantId." BlankLine $null = (& { if ($HealthCheck.EntraID.MFA) { $null = ($UserMethodObj | Where-Object { $_.'Has MFA Method' -eq 'No' } | Set-Style -Style Critical | Out-Null) $null = ($UserMethodObj | Where-Object { $_.'Method Count' -eq '1' } | Set-Style -Style Warning | Out-Null) } }) $UserMethodTableParams = @{ Name = "Per-User Authentication Methods - $TenantId"; List = $false; ColumnWidths = 22, 32, 30, 10, 6 } if ($Report.ShowTableCaptions) { $UserMethodTableParams['Caption'] = "- $($UserMethodTableParams.Name)" } $UserMethodObj | Table @UserMethodTableParams # Store for Excel export $null = ($script:ExcelSheets['Per-User Auth Methods'] = $UserMethodObj) } } } catch { Write-AbrSectionError -Section 'Per-User Authentication Methods' -Message "$($_.Exception.Message)" } } #endregion } # end Section Authentication Methods #endregion } end { Show-AbrDebugExecutionTime -End -TitleMessage 'Authentication Methods' } } |