Private/Conditions/Test-AuthenticationStrength.ps1

function Test-AuthenticationStrength {
    <#
    .SYNOPSIS
        Tests if a user's authentication methods satisfy an authentication strength policy.

    .DESCRIPTION
        This function evaluates if a user's authentication methods satisfy the authentication
        strength policy specified in a Conditional Access policy's grant controls.

        It supports:
        - Combination configurations for authentication methods
        - Authentication method verification against user's available methods
        - Compatible with different authentication strength policies

    .PARAMETER AuthStrength
        The authentication strength policy object from a policy's grant controls.

    .PARAMETER UserContext
        The user context containing authentication methods information.

    .EXAMPLE
        Test-AuthenticationStrength -AuthStrength $Policy.GrantControls.AuthenticationStrength -UserContext $UserContext
    #>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [object]$AuthStrength,

        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [object]$UserContext
    )

    # If no authentication strength specified, consider it satisfied
    if (-not $AuthStrength) {
        Write-Verbose "No authentication strength specified, considered satisfied"
        return @{
            Satisfied        = $true
            Reason           = "No authentication strength required"
            RequiredStrength = $null
        }
    }

    # Extract strength policy details
    $strengthId = $AuthStrength.id
    $displayName = $AuthStrength.displayName

    Write-Verbose "Evaluating authentication strength: $displayName (ID: $strengthId)"

    # If user has no authentication methods, strength cannot be satisfied
    if (-not $UserContext -or -not $UserContext.AuthenticationMethods -or $UserContext.AuthenticationMethods.Count -eq 0) {
        Write-Verbose "User has no authentication methods, strength cannot be satisfied"
        return @{
            Satisfied        = $false
            Reason           = "User has no available authentication methods"
            RequiredStrength = $displayName
        }
    }

    # Try to get detailed strength policy if available
    $strengthPolicy = Get-AuthenticationStrengthPolicy -AuthenticationStrengthPolicyId $strengthId

    # If we couldn't get the detailed policy, do basic evaluation
    if (-not $strengthPolicy) {
        Write-Verbose "Could not retrieve detailed strength policy, using basic evaluation"

        # Check if user has MFA method
        $hasMfaMethod = $UserContext.AuthenticationMethods | Where-Object {
            $_.Type -in @("microsoftAuthenticator", "fido2SecurityKey", "softwareOath", "hardwareOath", "phoneAuthentication")
        }

        $satisfied = ($hasMfaMethod -ne $null)

        return @{
            Satisfied        = $satisfied
            Reason           = if ($satisfied) { "User has basic MFA method" } else { "User lacks required MFA method" }
            RequiredStrength = $displayName
        }
    }

    # Get user's authentication methods
    $userMethods = $UserContext.AuthenticationMethods | ForEach-Object { $_.Type }

    Write-Verbose "User authentication methods: $($userMethods -join ', ')"
    Write-Verbose "Required combinations: $($strengthPolicy.CombinationConfigurations.Count) configuration(s)"

    # Check if any combination is satisfied
    $satisfiedCombination = $false
    $combinationDetails = ""

    foreach ($combination in $strengthPolicy.CombinationConfigurations) {
        Write-Verbose "Checking combination: $($combination.DisplayName)"

        # Get the required method combinations for this configuration
        $requiredMethodCombinations = $combination.RequiredAuthenticationMethodCombinations

        foreach ($methodCombination in $requiredMethodCombinations) {
            Write-Verbose "Required methods for this combination: $($methodCombination -join ' AND ')"

            # Check if all methods in this combination are available to the user
            $allMethodsAvailable = $true
            foreach ($requiredMethod in $methodCombination) {
                if ($userMethods -notcontains $requiredMethod) {
                    $allMethodsAvailable = $false
                    Write-Verbose "User is missing required method: $requiredMethod"
                    break
                }
            }

            if ($allMethodsAvailable) {
                $satisfiedCombination = $true
                $combinationDetails = "$($combination.DisplayName) ($($methodCombination -join ' AND '))"
                Write-Verbose "Combination satisfied: $combinationDetails"
                break
            }
        }

        if ($satisfiedCombination) { break }
    }

    return @{
        Satisfied            = $satisfiedCombination
        Reason               = if ($satisfiedCombination) { "User satisfies authentication strength: $combinationDetails" } else { "User does not satisfy any authentication strength combination" }
        RequiredStrength     = $displayName
        RequiredCombinations = $strengthPolicy.CombinationConfigurations
    }
}

function Get-AuthenticationStrengthPolicy {
    <#
    .SYNOPSIS
        Retrieves an authentication strength policy by ID.

    .DESCRIPTION
        This function retrieves an authentication strength policy from Microsoft Graph API
        or from a cache. It returns the detailed policy with combination configurations.

    .PARAMETER AuthenticationStrengthPolicyId
        The ID of the authentication strength policy to retrieve.

    .EXAMPLE
        Get-AuthenticationStrengthPolicy -AuthenticationStrengthPolicyId "00000000-0000-0000-0000-000000000000"
    #>

    [CmdletBinding()]
    [OutputType([System.Object])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$AuthenticationStrengthPolicyId
    )

    # Check if policy is in cache
    if ($script:AuthStrengthPoliciesCache -and $script:AuthStrengthPoliciesCache.ContainsKey($AuthenticationStrengthPolicyId)) {
        Write-Verbose "Retrieved authentication strength policy '$AuthenticationStrengthPolicyId' from cache"
        return $script:AuthStrengthPoliciesCache[$AuthenticationStrengthPolicyId]
    }

    try {
        # Retrieve authentication strength policy from Graph API
        $policy = Get-MgPolicyAuthenticationStrengthPolicy -AuthenticationStrengthPolicyId $AuthenticationStrengthPolicyId

        # Initialize cache if not exists
        if (-not $script:AuthStrengthPoliciesCache) {
            $script:AuthStrengthPoliciesCache = @{}
        }

        # Add to cache
        $script:AuthStrengthPoliciesCache[$AuthenticationStrengthPolicyId] = $policy

        return $policy
    }
    catch {
        Write-Warning "Error retrieving authentication strength policy '$AuthenticationStrengthPolicyId': $_"

        # Return a built-in policy for well-known IDs
        $builtInPolicy = Get-BuiltInAuthenticationStrengthPolicy -AuthenticationStrengthPolicyId $AuthenticationStrengthPolicyId
        if ($builtInPolicy) {
            return $builtInPolicy
        }

        return $null
    }
}

function Get-BuiltInAuthenticationStrengthPolicy {
    <#
    .SYNOPSIS
        Gets a built-in authentication strength policy based on its ID.

    .DESCRIPTION
        This function returns a built-in definition for well-known authentication
        strength policies when the Graph API is not available or returns an error.

    .PARAMETER AuthenticationStrengthPolicyId
        The ID of the authentication strength policy to retrieve.

    .EXAMPLE
        Get-BuiltInAuthenticationStrengthPolicy -AuthenticationStrengthPolicyId "00000000-0000-0000-0000-000000000001"
    #>

    [CmdletBinding()]
    [OutputType([System.Object])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$AuthenticationStrengthPolicyId
    )

    # Map of built-in authentication strength policies with their IDs
    $builtInPolicies = @{
        # Microsoft Entra multifactor authentication
        "c1a4e2d5-a209-4524-bb83-b2ab066404a6" = @{
            Id                        = "c1a4e2d5-a209-4524-bb83-b2ab066404a6"
            DisplayName               = "Microsoft Entra multifactor authentication"
            Description               = "Require Microsoft Entra multifactor authentication"
            CombinationConfigurations = @(
                @{
                    DisplayName                              = "Microsoft Entra MFA"
                    RequiredAuthenticationMethodCombinations = @(
                        @("microsoftAuthenticator"),
                        @("softwareOath"),
                        @("hardwareOath"),
                        @("phoneAuthentication"),
                        @("fido2SecurityKey")
                    )
                }
            )
        }

        # Passwordless MFA
        "be5aa28e-21ea-4f11-9db4-0f6982c16fb6" = @{
            Id                        = "be5aa28e-21ea-4f11-9db4-0f6982c16fb6"
            DisplayName               = "Passwordless MFA"
            Description               = "Require passwordless MFA"
            CombinationConfigurations = @(
                @{
                    DisplayName                              = "Passwordless methods"
                    RequiredAuthenticationMethodCombinations = @(
                        @("windowsHelloForBusiness"),
                        @("fido2SecurityKey"),
                        @("microsoftAuthenticator")
                    )
                }
            )
        }

        # Phishing-resistant MFA
        "00000000-0000-0000-0000-000000000004" = @{
            Id                        = "00000000-0000-0000-0000-000000000004"
            DisplayName               = "Phishing-resistant MFA"
            Description               = "Require phishing-resistant MFA"
            CombinationConfigurations = @(
                @{
                    DisplayName                              = "Phishing-resistant methods"
                    RequiredAuthenticationMethodCombinations = @(
                        @("windowsHelloForBusiness"),
                        @("fido2SecurityKey")
                    )
                }
            )
        }
    }

    # Return the built-in policy if found
    if ($builtInPolicies.ContainsKey($AuthenticationStrengthPolicyId)) {
        return $builtInPolicies[$AuthenticationStrengthPolicyId]
    }

    # Return null if not found
    return $null
}