Detect Impact MFA Enforcement

You may have noticed that Microsoft will enforce MFA requirement per October 15, 2024 for Azure/Entra/Intune. If this is new to you – or you want to find out (potential) impacted accounts, keep reading 🙂

Content

Background

MFA should be something you are familiar with or have enforced in you enterprise, but unfortunately there are still many tenants or configurations, where security improvements are needed – maybe due to exceptions.

In November 2023, Microsoft launched the Secure Future Initiative (SFI) to prepare for the increasing scale of cyberattacks. SFI brings together every part of Microsoft to advance cybersecurity protection across the whole stack of existing and future products.

It is important to understand that SFI is not just technology but also includes processes and people skills; ultimately establishing a security culture.

As part of Microsoft’s Secure Future Initiative (SFI), a number of global security changes will happen, which will impact us all. Each of the initiatives are detailed here.

Identities and secrets – Impact Oct 15, 2024

As detailed link, Microsoft will enforce MFA requirement for accounts on October 15, 2024, that makes interactive sign-ins to specific portals:

Application NameApp IDEnforcement phase
Azure portalc44b4083-3bb0-49c1-b47d-974e53cbdf3c
October 15, 2024
Microsoft Entra admin centerc44b4083-3bb0-49c1-b47d-974e53cbdf3cOctober 15, 2024
Microsoft Intune admin centerc44b4083-3bb0-49c1-b47d-974e53cbdf3cOctober 15, 2024
Azure command-line interface (Azure CLI)04b07795-8ddb-461a-bbee-02f9e1bf7b46Early 2025
Azure PowerShell1950a258-227b-4e31-a9cf-717495945fc2Early 2025
Azure mobile app
0c1307d4-29d6-4389-a11c-5cbe7f65d7fa
Early 2025
Infrastructure as Code (IaC) tools
Use Azure CLI or Azure PowerShell IDs
Early 2025

Impacted Accounts (official statement Microsoft)

Statements from this article

All users who sign into the applications listed previously to perform any Create, Read, Update, or Delete (CRUD) operation will require MFA when the enforcement begins.

Workload identities, such as managed identities and service principals, aren’t impacted by MFA enforcement.

If user identities sign in as a service account to run automation (including scripts or other automated tasks), those user identities need to sign in with MFA once enforcement begins.

User identities aren’t recommended for automation. You should migrate those user identities to workload identities.

Break glass or emergency access accounts are also required to sign in with MFA once enforcement begins. I recommend updating these accounts to use passkey (FIDO2) or configure certificate-based authentication for MFA. Both methods satisfy the MFA requirement.

Find potential accounts with issues (script)

In order for us to find (potential) impacted accounts, I categorize into 3 checks:

  • Check 1: Accounts that will be impacted as sign-in activity is detected to portals
    • Active Account
    • Sign-ins during last 90 days in cloud
    • Accounts signed in and used one of the MS Admin Portals during last 90 days
    • MFA not registered
  • Check 2: Accounts that may be impacted – missing MFA reg.
    • Both Inactive or Active accounts.
      • We include Disabled accounts, as some companies tend to disable some admin accounts e.g. consultants and then re-enable them again.
    • Sign-ins during last 90 days in cloud
    • MFA not registered
  • Check 3: Accounts that may be impacted – MFA not capable, MFA not registered
    • Both Inactive or Active accounts.
      • We include Disabled accounts, as some companies tend to disable some admin accounts e.g. consultants and then re-enable them again.
    • Sign-ins during last 90 days in cloud
    • MFA not registered
    • MFA not capable

It is important to note, that we cannot conclude based solely on recent sign-in events against above admin portal (check 1), as some accounts are used rarely like service accounts with interactive logins and break-glass accounts. For example, if you haven’t logged in with your break-glass accounts during the last 90 days, you will face issues if they are only protected with userid/password and have no MFA registrations. Therefore we also have check 2 and 3. Feel free to modify the checks for your requirement. Happy to learn how you do the filtering ?

Entra ID Sign-in logs keeps 90 days of events. The provided script receives the data directly from Microsoft Graph and is therefore limited to last 90 days of sign-in events.

If you send your Sign-in logs into Sentinel, you can also consider to adjust the script to enumerate a larger dataset by looking up the Azure LogAnalytics SigninLogs table. Query can be done using Invoke-AzOperationalInsightsQuery

Correlation of data (script)

The script correlates information from Entra ID SignIn Log, Entra ID Authentication Methods and Entra ID User information. Script can easily be extended to include e.g. Active Directory sign-ins, tagging information, etc.

Currently, the following fields are available in the report (array):

Id
GivenName
SurName
UserPrincipalName
DisplayName
AccountEnabled
Mail
SignInsDetectedMSAdminPortals
IsAdmin
DefaultMfaMethod
MethodsRegistered
IsMfaCapable
IsMfaRegistered
IsPasswordlessCapable
IsSsprCapable
IsSsprEnabled
IsSsprRegistered
IsSystemPreferredAuthenticationMethodEnabled
AuthMethodsLastUpdatedDateTime
Cloud_LastSignInDateTime
Cloud_LastNonInteractiveSignInDateTime
Cloud_PasswordPolicies
ActiveDirectoryDistinguishedName
UserLicenseList

Output (script)

Output of the script is both shown on screen – but also exported to 2 Excel files.

File 1 (sample Identity_Overview.xlsx) includes an overview and observations

File 2 (sample Identity_Overview_Events.xlsx) includes actual events which can be used to investigate further.

Script

Script can be found here on Github

Bug Fix – “command not found” Get-MgAuditLogSignIn

If you ran the script on a machine with only the beta version of Microsoft Graph installed, you would before Sept 18, 2024 get an error with “command not found” referencing to the command Get-MgAuditLogSignIn

Script has now been updated to use the beta-version which will installed as part of the script.

$Events = Get-MgBetaAuditLogSignIn -Filter “(AppId eq ‘$($AppId)’) and (createdDateTime ge $SearchDateFrom)” -All

Variables

You can fine-tune the script scope and output by modifying the variables in the header.

    $LogDaysToSearch = 90
    $File_Overview = ".\Identity_Overview.xlsx"
    $File_Events   = ".\Identity_Overview_Events.xlsx"

Complete script

#Requires -Version 5.0
<#
    .SYNOPSIS
    
    Detect Accounts Impacted By The MFA Enforcement October 15, 2025 and Early 2025

    .MORE INFORMATION
    https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mandatory-multifactor-authentication

    .NOTES
    VERSION: 2409

    .COPYRIGHT
    @mortenknudsendk on Twitter | mok@mortenknudsen.net
    Blog: https://mortenknudsen.net
    
    .LICENSE
    Licensed under the MIT license.

    .WARRANTY
    Use at your own risk, no warranty given!
#>

#####################################################################
# Variables
#####################################################################

    $LogDaysToSearch = 90
    $File_Overview = ".\Identity_Overview.xlsx"
    $File_Events   = ".\Identity_Overview_Events.xlsx"

    $AppInScope = @(
                      [PSCustomObject]@{
                                          AppId = "c44b4083-3bb0-49c1-b47d-974e53cbdf3c"
                                          AppName = "Azure Portal"
                                          EnforceMent = "Oct 15, 2024"
                                       }
                      [PSCustomObject]@{
                                          AppId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
                                          AppName = "Azure CLI"
                                          EnforceMent = "Early 2025"
                                       }
                      [PSCustomObject]@{
                                          AppId = "1950a258-227b-4e31-a9cf-717495945fc2"
                                          AppName = "Azure Powershell"
                                          EnforceMent = "Early 2025"
                                       }
                      [PSCustomObject]@{
                                          AppId = "0c1307d4-29d6-4389-a11c-5cbe7f65d7fa"
                                          AppName = "Azure Mobile App"
                                          EnforceMent = "Early 2025"
                                       }
                   )


#####################################################################
# Microsoft Graph
#####################################################################

    write-host ""
    Write-host "Step 1/8: Checking Microsoft Powershell Modules"

    $InstalledModules = Get-InstalledModule
    If ("Microsoft.Graph.Beta" -notin $InstalledModules.Name)
        {
            # Beta-module contains more info than normal module - why ? Good question, but that is a fact !
            Install-module Microsoft.Graph.Beta
        }
    If ("ImportExcel" -notin $InstalledModules.Name)
        {
            # ImportExcel - Used for Excel file reporting
            #   Github: https://github.com/dfinke/ImportExcel
            #   Powershell Gallery: https://www.powershellgallery.com/packages/ImportExcel

            Install-module ImportExcel
        }

#####################################################################
# Microsoft Graph
#####################################################################

    write-host ""
    Write-host "Step 2/8: Connectivity to Microsoft Graph"

    Connect-MgGraph -scopes Directory.Read.All, AuditLog.Read.All, UserAuthenticationMethod.Read.All


#####################################################################
# Step 3: Get SignIn Events from Entra ID SignInLog
#####################################################################

    write-host ""
    Write-host "Step 3/8: Checking Sign-in logs for last $($LogDaysToSearch) day(s) - looking for interactive sign-ins"

    $SearchDateFrom = (Get-date) - (New-TimeSpan -Days $LogDaysToSearch)
    $SearchDateFrom = Get-date $SearchDateFrom -format yyyy-MM-ddTHH:mm:ssZ

    $LogEvents = [System.Collections.ArrayList]@()
    $TotalEvents = 0
    ForEach ($Entry in $AppInScope)
        {
            $Events = @()
            write-host ""
            Write-host "  Searching for events for app [ $($Entry.AppName) ] ... Please Wait !"

            $AppId = [guid]$Entry.AppId
            $Events = Get-MgBetaAuditLogSignIn -Filter "(AppId eq '$($AppId)') and (createdDateTime ge $SearchDateFrom)" -All

            $TotalEvents = $TotalEvents + $Events.Count

            write-host "  Found $($Events.Count) event(s)"
            $Add = $LogEvents.add($Events)
        }

    If ($LogEvents)
        {
            write-host ""
            write-host "  Found $($TotalEvents) total event(s)"

            $LogEvents_Users_unique = $LogEvents.UserPrincipalName | Sort-Object -Unique

            write-host ""
            write-host "  Found $($LogEvents_Users_unique.count) unique user(s) with sign-ins"
            write-host ""
        }

#########################################################################################################
# Step 4: Get AuthenticationMethods from the users doing interactive sign-ins
#########################################################################################################

    write-host ""
    write-host "Step 4/8: Getting Authentication Methods from Entra ID .... Please Wait !"

    $UsersAuthMethods = Get-MgBetaReportAuthenticationMethodUserRegistrationDetail -All

    If ($UsersAuthMethods)
        {
            write-host ""
            write-host "  Found $($UsersAuthMethods.count) authentication methods"
            write-host ""
            write-host "  Converting UserAuthenticationMethod array to hash-table for faster searching as hash-tables per definition must be unique"

            $UsersAuthMethods_Hash = [ordered]@{}
            $UsersAuthMethods | ForEach-Object { $UsersAuthMethods_Hash.add($_.UserPrincipalName,$_)}
        }

#####################################################################
# Step 5: Get user info
#####################################################################

    write-host ""
    write-host "Step 5/8: Getting User info from Entra ID .... Please Wait !"

    $Users = Get-MgBetaUser -All -property AccountEnabled, id, givenname, surname, userprincipalname, AssignedLicenses, AssignedPlans, Authentication, Devices, CreatedDateTime, Department, Identities, InvitedBy, IsResourceAccount, JoinedTeams, JoinedGroups, LastPasswordChangeDateTime, LicenseDetails, Mail, Manager, MobilePhone, OfficeLocation, PasswordPolicies, ProxyAddresses, UsageLocation, OnPremisesDistinguishedName, OnPremisesExtensionAttributes, OnPremisesSyncEnabled, displayname, signinactivity `
                            | select-object id, givenname, surname, userprincipalname, OnPremisesDistinguishedName, AccountEnabled, displayname, AssignedLicenses, AssignedPlans, Authentication, Devices, CreatedDateTime, Department, Identities, InvitedBy, IsResourceAccount, JoinedTeams, JoinedGroups, LastPasswordChangeDateTime, LicenseDetails, Mail, Manager, MobilePhone, OfficeLocation, PasswordPolicies, ProxyAddresses, UsageLocation, OnPremisesSyncEnabled, `
                            @{name='LastSignInDateTime'; expression = {$_.signinactivity.lastsignindatetime}}, `
                            @{name='LastNonInteractiveSignInDateTime'; expression = {$_.signinactivity.LastNonInteractiveSignInDateTime}}, `
                            @{name='AuthPhoneMethods'; expression = {$_.authentication.PhoneMethods}}, `
                            @{name='AuthMSAuthenticator'; expression = {$_.authentication.MicrosoftAuthenticatorMethods}}, `
                            @{name='AuthPassword'; expression = {$_.authentication.PasswordMethods}}

    If ($Users)
        {
            write-host ""
            write-host "  Found $($Users.count) users"
            write-host ""
            write-host "  Converting User array to hash-table for faster searching as hash-tables per definition must be unique"

            $Users_Hash = [ordered]@{}
            $Users | ForEach-Object { $Users_Hash.add($_.UserPrincipalName,$_)}
        }
    
#####################################################################
# Step 6: Correlate date - build array
#####################################################################

    Write-host ""
    Write-host "Step 6/8: Correlating data-sources into user array for validation & reporting purpose .... Please Wait !"
    Write-host ""

    # Get license details from Microsoft - https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference
    $LicenseTranslationTable = Invoke-WebRequest -Method Get -UseBasicParsing -Uri "https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv" | ConvertFrom-Csv

    $UserInfoArray = [System.Collections.ArrayList]@()
    $UsersTotal = $Users.count

    $Users | ForEach-Object -Begin  {
            $i = 0
    } -Process {
            
            # Default values
            $User = $_
            $SignInsDetected = $false

            write-host "  Processing $($User.DisplayName)"

            #------------------------------------------------------------------------------------------------
            # Sign-in Events
               If ($User.UserPrincipalName -in $LogEvents_Users_unique)
                    {
                        $SignInsDetected = $true
                    }

            #------------------------------------------------------------------------------------------------
            # Authentication Methods
                
                If ($UsersAuthMethods)
                    {
                        $AuthMethods = $UsersAuthMethods_Hash[$user.UserPrincipalName]
                    }

            #------------------------------------------------------------------------------------------------
            # Get Licenses

                # Get user licenses
                    $LicenseInfo = @()
                    ForEach ($License in $User.AssignedLicenses)
                        {
                            $LicenseInfo += $LicenseTranslationTable | where { $_.Guid -eq $License.SkuID }
                        }
                    If ($LicenseInfo)
                        {
                            $UserLicenseInfo_List = (($LicenseInfo."???Product_Display_Name" | Sort-Object -Unique) -join ",")
                        }

                    $LicenseInfo = $LicenseInfo.String_ID | Sort-Object -Unique
                    $UserAssignedPlans = $User.AssignedPlans

            #------------------------------------------------------------------------------------------------
            # Building array

            $Object = [PSCustomObject]@{
                                            Id                                           = $User.Id
                                            GivenName                                    = $User.GivenName
                                            SurName                                      = $User.Surname
                                            UserPrincipalName                            = $User.UserPrincipalName
                                            DisplayName                                  = $User.DisplayName
                                            AccountEnabled                               = $User.AccountEnabled
                                            Mail                                         = $User.Mail
                                            SignInsDetectedMSAdminPortals                = $SignInsDetected
                                            IsAdmin                                      = $AuthMethods.IsAdmin
                                            DefaultMfaMethod                             = $AuthMethods.DefaultMfaMethod
                                            MethodsRegistered                            = $AuthMethods.MethodsRegistered -join ','
                                            IsMfaCapable                                 = $AuthMethods.IsMfaCapable
                                            IsMfaRegistered                              = $AuthMethods.IsMfaRegistered
                                            IsPasswordlessCapable                        = $AuthMethods.IsPasswordlessCapable
                                            IsSsprCapable                                = $AuthMethods.IsSsprCapable
                                            IsSsprEnabled                                = $AuthMethods.IsSsprEnabled
                                            IsSsprRegistered                             = $AuthMethods.IsSsprRegistered
                                            IsSystemPreferredAuthenticationMethodEnabled = $AuthMethods.IsSystemPreferredAuthenticationMethodEnabled
                                            AuthMethodsLastUpdatedDateTime               = $AuthMethods.LastUpdatedDateTime
                                            Cloud_LastSignInDateTime                     = $User.LastSignInDateTime
                                            Cloud_LastNonInteractiveSignInDateTime       = $User.LastNonInteractiveSignInDateTime
                                            Cloud_PasswordPolicies                       = $User.PasswordPolicies
                                            ActiveDirectoryDistinguishedName             = $User.OnPremisesDistinguishedName
                                            UserLicenseList                              = $UserLicenseInfo_List
                                        }
            $Result = $UserInfoArray.add($object)

            # Increment the $i counter variable which is used to create the progress bar.
            $i = $i+1

            # Determine the completion percentage
            $Completed = ($i/$UsersTotal) * 100
            Write-Progress -Activity "Correlating User Info" -Status "Progress:" -PercentComplete $Completed
            } -End {
                
                Write-Progress -Activity "Correlating User Info" -Status "Ready" -Completed
            }


#####################################################################
# Step 7: Build conclusions
#####################################################################

    $DaysLastSignInCheck = (Get-date) - (New-TimeSpan -Days 90)

    Write-host ""
    Write-host "Step 7/8: Building conclusions .... Please Wait !"

    # Scoping
        $UserInfoArray_Scoped = $UserInfoArray | Where-Object { $_.DisplayName -notlike "On-Premises Directory Synchronization Service Account" }

    #--------------------------------------------------------------------------------------------------------
    # IMPACT: Accounts that will have impact, when Microsoft enforce MFA Requirement (sign-ins was detected)
        $Impact_MFA_Enforcement = $UserInfoArray_Scoped | Where-Object { ( (!($_.IsMfaRegistered)) -and ($_.SignInsDetectedMSAdminPortals) -and ($_.AccountEnabled) ) }

        write-host ""
        write-host "Check 1: Active account, MFA missing, cloud sign-ins during last 90 days, Sign-in events against MS admin portals detected last $($LogDaysToSearch) day(s)"

        If ($Impact_MFA_Enforcement)
            {
                write-host ""
                Write-host "Users ($(($Impact_MFA_Enforcement | measure-object).count)) that will be impacted by MFA enforcement (sign-ins detected):" -ForegroundColor Yellow

                $Impact_MFA_Enforcement | Select-Object DisplayName,UserPrincipalName | Out-Default
            }
        Else
            {
                write-host "No issues found !" -ForegroundColor Green
                write-host ""
            }

    #--------------------------------------------------------------------------------------------------------
    # INVESTIGATION NEEDED: Accounts that are missing MFA registration - SignIn during Last 90 days - Account Active
        $MissingMFA_ActiveAccount_RecentSignIns = $UserInfoArray_Scoped | Where-Object { ( (!($_.IsMFARegistered) -and (!($_.SignInsDetectedMSAdminPortals)) -and ($_.Cloud_LastSignInDateTime -gt $DaysLastSignInCheck)) ) }

        write-host ""
        write-host "Check 2: Active account, MFA missing, cloud sign-ins during last 90 days, no sign-in events against MS admin portals"

        If ($MissingMFA_ActiveAccount_RecentSignIns)
            {
                write-host ""
                Write-host "Users ($(($MissingMFA_ActiveAccount_RecentSignIns | measure-object).count)) that MAY be impacted by MFA enforcement if account may do interactive login against MS admin Portals (no events detected):" -ForegroundColor Yellow

                $MissingMFA_ActiveAccount_RecentSignIns | Select-object DisplayName, UserPrincipalName | Out-Default
            }
        Else
            {
                write-host "No issues found !" -ForegroundColor Green
                write-host ""
            }

    #--------------------------------------------------------------------------------------------------------
    # INVESTIGATION NEEDED: Accounts that are missing MFA registration - SignIn during Last 90 days - Account Active - MFA not capable
    # MFA NOT capable: Users registered and enabled for a strong authentication method in Microsoft Entra ID. Either a user or an admin may register an authentication method on behalf of a user. 
    # Authentication methods are enabled by authentication method policy or multifactor authentication service settings

        $MissingMFA_ActiveAccount_RecentSignIns_MfaNotCapable = $UserInfoArray_Scoped | Where-Object { ( (!($_.IsMfaRegistered) -and (!($_.SignInsDetectedMSAdminPortals)) -and (!($_.IsMfaCapable)) -and ($_.Cloud_LastSignInDateTime -gt $DaysLastSignInCheck) ) ) }

        write-host ""
        write-host "Check 3: Active account, MFA missing, MFA not capable, cloud sign-ins during last 90 days, no sign-in events against MS admin portals"
        
        If ($MissingMFA_ActiveAccount_RecentSignIns_MfaNotCapable)
            {
                write-host ""
                Write-host "Users ($(($MissingMFA_ActiveAccount_RecentSignIns_MfaNotCapable | measure-object).count)) that MAY be impacted by MFA enforcement if account may do interactive login against MS admin Portals:" -ForegroundColor Yellow

                $MissingMFA_ActiveAccount_RecentSignIns_MfaNotCapable | Select-object DisplayName, UserPrincipalName | Out-Default
            }
        Else
            {
                write-host "No issues found !" -ForegroundColor Green
                write-host ""
            }


#####################################################################
# Step 8: Export to Excel
#####################################################################

    If (Test-Path $File_Overview)
        {
            Remove-Item $File_Overview -Force
        }

    Write-host ""
    Write-host ""
    Write-host "Step 8/8: Exporting Overview, Observations & Events .... Please Wait !"
    write-host ""

    #-----------------------------------
    Write-host "  Exporting Overview to file ($($File_Overview))"

    $Target = "Users_Scoped"
    $UserInfoArray_Scoped | Export-Excel -Path $File_Overview -WorksheetName $Target -AutoFilter -AutoSize -BoldTopRow -tablename $Target -tablestyle medium9

    $Target = "Check1_Impact_MFA_Enforcement"
    $Impact_MFA_Enforcement | Export-Excel -Path $File_Overview -WorksheetName $Target -AutoFilter -AutoSize -BoldTopRow -tablename $Target -tablestyle medium9

    $Target = "Check2_Missing_MFA"
    $MissingMFA_ActiveAccount_RecentSignIns | Export-Excel -Path $File_Overview -WorksheetName $Target -AutoFilter -AutoSize -BoldTopRow -tablename $Target -tablestyle medium9

    $Target = "Check3_Missing_MFA_Not_Capable"
    $MissingMFA_ActiveAccount_RecentSignIns_MfaNotCapable | Export-Excel -Path $File_Overview -WorksheetName $Target -AutoFilter -AutoSize -BoldTopRow -tablename $Target -tablestyle medium9

    #-----------------------------------

    If (Test-Path $File_Events)
        {
            Remove-Item $File_Events -Force
        }

    write-host ""
    Write-host "  Exporting Sign-In events to file ($($File_Events))"
    write-host ""

    $Target = "Events"
    $LogEvents | Export-Excel -Path $File_Events -WorksheetName $Target -AutoFilter -AutoSize -BoldTopRow -tablename $Target -tablestyle medium9

2 thoughts on “Detect Impact MFA Enforcement”

Leave a Reply