Monitoring with PowerShell: Monitoring MFA Usage

So I’ve blogged about this before too, but times change and monitoring MFA usage is becoming a little more difficult . Microsoft allows per-user MFA, Security Defaults, and Conditional Access all to be used concurrently. I’ve created this monitoring script that returns which users seem to fall out of any Multi-factor authentication scope, and also reports what type of authentication is currently active on the tenant. Using the normal PowerShell methods you can only find if a user has per-user MFA enabled, if a user uses Conditional Access or Security Defaults it shows the per-user MFA state as disabled, which is a little annoying.

So the script checks the following to make sure we know if all users have MFA or not;

  • Do users have per-user MFA enabled?
  • Is Security Defaults enabled?
  • Is there a CA policy that enforces MFA for all users?

This should give you a slightly better report of exactly where there are gaps in your security settings. If users don’t have multi-factor authentication enabled you can help them enable it as soon as possible. I’ve included two scripts, one for all tenants, and one for a specific tenant. Of course as always I’m not saying the script is 100% complete, but feel free edit it to your own needs and policies. 🙂

For the script you’ll need the secure application model, I’ve also posted a new video about the Secure Application Model right here, in case you’ve never set that up.

All Tenants Script

######### Secrets #########
$ApplicationId = 'ApplicationID'
$ApplicationSecret = 'ApplicationSecret' | ConvertTo-SecureString -Force -AsPlainText
$TenantID = 'TenantID'
$RefreshToken = 'LongRefreshToken'
######### Secrets #########
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)

$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 

Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken

$customers = Get-MsolPartnerContract -All

$Baseuri = "https://graph.microsoft.com/beta"
$MFAState = foreach ($customer in $customers) {
    $users = Get-MsolUser -TenantId $customer.tenantid -all
    $PerUserMFA = foreach ($user in $users) {
        $MFAStatus = if ($null -ne $user.StrongAuthenticationUserDetails) { ($user.StrongAuthenticationMethods | Where-Object { $_.IsDefault -eq $true }).methodType } else { "Disabled" } 
        [PSCustomObject]@{
            "DisplayName" = $user.DisplayName
            "UPN"         = $user.UserPrincipalName
            "MFA Type"    = $MFAStatus
        }
    }
    try {
        $CustGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes "https://graph.microsoft.com/.default" -ServicePrincipal -Tenant $customer.TenantId
        $Header = @{ Authorization = "Bearer $($CustGraphToken.AccessToken)" }
        $SecureDefaultsState = (Invoke-RestMethod -Uri "$baseuri/policies/identitySecurityDefaultsEnforcementPolicy" -Headers $Header -Method get -ContentType "application/json").IsEnabled
        $CAPolicies = (Invoke-RestMethod -Uri "$baseuri/identity/conditionalAccess/policies" -Headers $Header -Method get -ContentType "application/json").value
    }
    catch {
        $CAPolicies = $false
    }
    $EnforcedForUsers = foreach ($Policy in $CAPolicies) {
        if ($policy.grantControls.builtincontrols -ne 'mfa') { continue }
        if ($Policy.conditions.applications) {
            [PSCustomObject]@{
                Name   = $policy.displayName
                Target = 'Specific Applications'
            }
            continue 
        }

        if ($Policy.conditions.users.includeUsers -eq "All") {
            [PSCustomObject]@{
                Name   = $policy.displayName
                Target = 'All Users'
            } 
        }
            
    }

    $enforced = if ($EnforcedForUsers | Where-Object -Property Target -eq "All Users") { $True } else { $false }
    [PSCustomObject]@{
        TenantName                        = $customer.DefaultDomainName
        UserList                          = $PerUserMFA
        'Secure Defaults Enabled'         = $SecureDefaultsState
        'Conditional Access'              = $CAPolicies
        'Conditional Access Enforced MFA' = $Enforced
    }

}

if ($MFAState.'Security Defaults Enabled' -eq $false -or $MFAState.'Conditional Access Enforced MFA') {
    $MFAState.userlist | Where-Object -Property "MFA Type" -eq "Disabled"
}

Single Tenant script

######### Secrets #########
$ApplicationId = 'ApplicationID'
$ApplicationSecret = 'ApplicationSecret' | ConvertTo-SecureString -Force -AsPlainText
$TenantID = 'TenantID'
$RefreshToken = 'LongRefreshToken'
$clienttenantID = "clientname.onmicrosoft.com"
######### Secrets #########
$credential = New-Object System.Management.Automation.PSCredential($ApplicationId, $ApplicationSecret)

$aadGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.windows.net/.default' -ServicePrincipal -Tenant $tenantID 
$graphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes 'https://graph.microsoft.com/.default' -ServicePrincipal -Tenant $tenantID 

Connect-MsolService -AdGraphAccessToken $aadGraphToken.AccessToken -MsGraphAccessToken $graphToken.AccessToken
$Customer = Get-MsolPartnerContract -All | Where-Object -property DefaultDomainName -eq $clienttenantID
$users = Get-MsolUser -all -TenantId $Customer.TenantId
$PerUserMFA = foreach ($user in $users) {
    $MFAStatus = if ($null -ne $user.StrongAuthenticationUserDetails) { ($user.StrongAuthenticationMethods | Where-Object { $_.IsDefault -eq $true }).methodType } else { "Disabled" } 
    [PSCustomObject]@{
        "DisplayName" = $user.DisplayName
        "UPN"         = $user.UserPrincipalName
        "MFA Type"    = $MFAStatus
    }
}
try{
$CustGraphToken = New-PartnerAccessToken -ApplicationId $ApplicationId -Credential $credential -RefreshToken $refreshToken -Scopes "https://graph.microsoft.com/.default" -ServicePrincipal -Tenant $clienttenantID
$Header = @{ Authorization = "Bearer $($CustGraphToken.AccessToken)" }
$SecureDefaultsState = (Invoke-RestMethod -Uri "$baseuri/policies/identitySecurityDefaultsEnforcementPolicy" -Headers $Header -Method get -ContentType "application/json").IsEnabled
$CAPolicies = (Invoke-RestMethod -Uri "$baseuri/identity/conditionalAccess/policies" -Headers $Header -Method get -ContentType "application/json").value
} catch {
    $CAPolicies = $false
}
$EnforcedForUsers = foreach ($Policy in $CAPolicies) {
    if ($policy.grantControls.builtincontrols -ne 'mfa') { continue }
    if ($Policy.conditions.applications) {
        [PSCustomObject]@{
            Name   = $policy.displayName
            Target = 'Specific Applications'
        }
        continue 
    }

    if ($Policy.conditions.users.includeUsers -eq "All") {
        [PSCustomObject]@{
            Name   = $policy.displayName
            Target = 'All Users'
        } 
    }
            
}

$enforced = if ($EnforcedForUsers | Where-Object -Property Target -eq "All Users") { $True } else { $false }
$MFAState = [PSCustomObject]@{
    TenantName                        = $customer.DefaultDomainName
    UserList                          = $PerUserMFA
    'Secure Defaults Enabled'         = $SecureDefaultsState
    'Conditional Access'              = $CAPolicies
    'Conditional Access Enforced MFA' = $Enforced
}


if ($MFAState.'Security Defaults Enabled' -eq $false -or $MFAState.'Conditional Access Enforced MFA' -eq $false) {
    $MFAState.userlist | Where-Object -Property "MFA Type" -eq "Disabled"
}

Like I said, this should help your reporting a little and gives you the ability to alert better, at least until Microsoft gives us a programmatic MFA testing utility, or something like that.

As always, Happy PowerShelling!

1 Comment

  1. Hendrik May 5, 2021 at 11:48 am

    Hello,

    Thanks for the script i am testing it right now.
    Is it possible to get also the company name in the report result, so yes, how can i realize that?

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.