Query Details

Analytics Authentication Methods Changes

Query

// This query can help you to check accounts that changed their MFA or Authentication Methods in Entra ID
//
// Click "Save as function", in Parameters write in the fields:
// "bool" "check_signinlogs" "false"
//
// If you name the function "AuthenticationMethodsChanges", you can check the function with queries like the following:
//
// AuthenticationMethodsChanges()
//
// AuthenticationMethodsChanges(true)
//
// let check_signinlogs = false;
//let Function = (check_signinlogs = false){
let _NotCoreEvents =
    AuditLogs
    | where case(
        LoggedByService == "Device Registration Service" and Category == "UserManagement", true,
        LoggedByService == "Authentication Methods" and Category == "UserManagement", true,
        OperationName has_any ("Strong Authentication"), true,
        false
        )
    | where Result == "success"
    | where not(LoggedByService == "Authentication Methods" and Category == "UserManagement" and OperationName has_any (
        "cancelled",
        "started security info registration",
        "reviewed security info",
        "Restore multifactor authentication on all remembered devices",
        "Get passkey creation options",
        "password change",
        "password reset"
        ))
    | where not(OperationName == "User started security info registration" and Result == "failure" and ResultDescription == "A system error has occurred.")
    | mv-expand TargetUserAuxiliar = pack_array(tostring(TargetResources[0]["id"]), tostring(TargetResources[0]["userPrincipalName"])) to typeof(string)
    | where isnotempty(TargetUserAuxiliar)
    | join kind=leftouter (
        AuditLogs
        | where OperationName == "Update user"
        // | where Result == "success"
        // Remove empty updates
        | mv-apply ModifiedProperty = TargetResources[0]["modifiedProperties"] on (
            summarize BagToUnpack = make_bag(bag_pack(tostring(ModifiedProperty["displayName"]), translate(@'["\]', "", tostring(ModifiedProperty["newValue"]))))
        )
        | where not(BagToUnpack["Included Updated Properties"] in ("", "ProxyAddresses") or BagToUnpack["Included Updated Properties"] has_any ("LastDirSyncTime", "StsRefreshTokensValidFrom"))
        // | where BagToUnpack["Included Updated Properties"] contains "StrongAuthentication" or BagToUnpack["Included Updated Properties"] contains "DeviceKey" or BagToUnpack["Included Updated Properties"] contains "SecuredAccess" or BagToUnpack["Included Updated Properties"] contains "PassData" or BagToUnpack["Included Updated Properties"] contains "SecurityId"
        | mv-expand TargetUserAuxiliar = pack_array(tostring(TargetResources[0]["id"]), tostring(TargetResources[0]["userPrincipalName"])) to typeof(string)
        | where isnotempty(TargetUserAuxiliar)
        | project
            UpdateUser_TimeGenerated = TimeGenerated,
            UpdateUser_TargetResources = TargetResources,
            UpdateUser_CorrelationId = CorrelationId,
            TargetUserAuxiliar
        ) on TargetUserAuxiliar
    | project-away TargetUserAuxiliar*
    // Remove update user event info if it is not related
    | extend
        UpdateUser_TargetResources = iff(UpdateUser_TimeGenerated between ((TimeGenerated-5m) .. TimeGenerated), UpdateUser_TargetResources, dynamic(null)),
        UpdateUser_CorrelationId = iff(UpdateUser_TimeGenerated between ((TimeGenerated-5m) .. TimeGenerated), UpdateUser_CorrelationId, "")
    | extend
        UpdateUser_TimeGenerated = iff(UpdateUser_TimeGenerated between ((TimeGenerated-5m) .. TimeGenerated), UpdateUser_TimeGenerated, datetime(null))
    // Take the most "recent" update user event
    | summarize arg_max(UpdateUser_TimeGenerated, *) by CorrelationId, OperationName
    | summarize arg_max(TimeGenerated, *) by CorrelationId // One event from "Device Registration Service" and another from "Authentication Methods" can have the same CorrelationId
    | extend
        Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
        InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
        IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"]),
        TargetUserPrincipalName = coalesce(tostring(TargetResources[0]["userPrincipalName"]), tostring(UpdateUser_TargetResources[0]["userPrincipalName"])),
        TargetId = coalesce(tostring(TargetResources[0]["id"]), tostring(UpdateUser_TargetResources[0]["id"]))
    | project
        TimeGenerated,
        LoggedByService,
        Category,
        AADOperationType,
        Initiator,
        IPAddress,// Might just show a Microsoft address for some ServiceApi operation types
        OperationName,
        Result,
        ResultDescription,
        TargetUserPrincipalName,
        TargetId,
        AdditionalDetails,
        InitiatorId,
        InitiatedBy,
        TargetResources,
        CorrelationId,
        UpdateUser_TargetResources,
        UpdateUser_CorrelationId
;
let _CoreEvents =
    AuditLogs
    | where OperationName == "Update user" //and LoggedByService == "Core Directory" and Category == "UserManagement" and Identity in ("Azure MFA StrongAuthenticationService", "Azure Credential Configuration Endpoint Service", "Azure ESTS Service")
    // | where Result == "success"
    | where not(CorrelationId in (toscalar(_NotCoreEvents | summarize make_set_if(UpdateUser_CorrelationId, isnotempty(UpdateUser_CorrelationId)))))
    | mv-expand ModifiedProperty = TargetResources[0]["modifiedProperties"]
    | where ModifiedProperty["displayName"] contains "StrongAuthentication" or ModifiedProperty["displayName"] contains "DeviceKey" or ModifiedProperty["displayName"] contains "SecuredAccess" or ModifiedProperty["displayName"] contains "PassData" or (ModifiedProperty["displayName"] contains "SecurityId" and not(AdditionalDetails has "Guest" or TargetResources has_all ("AcceptedAs", "AcceptedOn")))
    | where not(ModifiedProperty["displayName"] == "StrongAuthenticationPhoneAppDetail"
        and tostring(array_sort_asc(extract_all(@'\"Id\"\:\"([^\"]+)\"', tostring(ModifiedProperty["newValue"])))) == tostring(array_sort_asc(extract_all(@'\"Id\"\:\"([^\"]+)\"', tostring(ModifiedProperty["oldValue"])))))
    | project-away ModifiedProperty
    // We will assume there is only one unique "Update user" by CorrelationId (you might receive duplicated events)
    | summarize take_any(*) by CorrelationId
    | extend
        TargetUserPrincipalName = tostring(TargetResources[0]["userPrincipalName"]),
        TargetId = tolower(TargetResources[0]["id"]),
        UpdateUser_CorrelationId = CorrelationId,
        UpdateUser_TargetResources = TargetResources
    | as _AuxiliarEvents
    | join kind=leftouter (
        AADNonInteractiveUserSignInLogs
        | where check_signinlogs
        | where (
                ResourceIdentity in ("1f5530b3-261a-47a9-b357-ded261e17918")// Azure Multi-Factor Auth Connector
                or (AppId == "0000000c-0000-0000-c000-000000000000"//   Microsoft App Access Panel
                    and ResourceIdentity in (
                        "93625bc8-bfe2-437a-97e0-3d0060024faa",//       Microsoft password reset service
                        "65d91a3d-ab74-42e6-8a2f-0add61688c74",//       Microsoft Approval Management
                        "00000003-0000-0000-c000-000000000000",//       Microsoft Graph
                        "19db86c3-b2b9-44cc-b339-36da233a3be2",//       My Signins
                        "00000002-0000-0000-c000-000000000000"))//      Windows Azure Active Directory
                or (AppId == "19db86c3-b2b9-44cc-b339-36da233a3be2"//   My Signins
                    and ResourceIdentity in (
                        "0000000c-0000-0000-c000-000000000000",//       Microsoft App Access Panel
                        "00000003-0000-0000-c000-000000000000"))//      Microsoft Graph
                )
            and ResultType == 0
            and UserId in (toscalar(_AuxiliarEvents | summarize make_set(TargetId)))
        | project
            CreatedDateTime,
            IPAddress,
            UserId
        ) on $left.TargetId == $right.UserId
    // Remove update user event info if it is not related
    | extend
        IPAddress = iff(CreatedDateTime between ((TimeGenerated-5m) .. TimeGenerated), IPAddress, ""),
        UserId = iff(CreatedDateTime between ((TimeGenerated-5m) .. TimeGenerated), UserId, "")
    | extend
        CreatedDateTime = iff(CreatedDateTime between ((TimeGenerated-5m) .. TimeGenerated), CreatedDateTime, datetime(null))
    // Take the most "recent" update user event
    | summarize arg_max(CreatedDateTime, *) by CorrelationId
    | extend
        Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
        InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"]))
    | project
        TimeGenerated,
        LoggedByService,
        Category,
        AADOperationType,
        Initiator,
        IPAddress,// Might just show a Microsoft address for some ServiceApi operation types
        OperationName,
        Result,
        ResultDescription,
        TargetUserPrincipalName,
        TargetId,
        AdditionalDetails,
        InitiatorId,
        InitiatedBy,
        TargetResources,
        CorrelationId,
        UpdateUser_TargetResources,
        UpdateUser_CorrelationId
;
union _NotCoreEvents, _CoreEvents
| mv-apply ModifiedProperty = UpdateUser_TargetResources[0]["modifiedProperties"] on (
    summarize BagToUnpack = make_bag(bag_pack(tostring(ModifiedProperty["displayName"]), bag_pack("oldValue", ModifiedProperty["oldValue"], "newValue", ModifiedProperty["newValue"])))
    )
| extend BagToUnpack = bag_remove_keys(BagToUnpack, set_difference(bag_keys(BagToUnpack), split(trim(@'\"', tostring(BagToUnpack["Included Updated Properties"]["newValue"])), ", ")))
| evaluate bag_unpack(BagToUnpack, columnsConflict = "keep_source")
//};
//Function(check_signinlogs)

Explanation

This query is designed to identify accounts in Entra ID (formerly Azure Active Directory) that have undergone changes in their Multi-Factor Authentication (MFA) or authentication methods. Here's a simplified breakdown of what the query does:

  1. Function Setup: The query can be saved as a function named AuthenticationMethodsChanges. It accepts a boolean parameter check_signinlogs which defaults to false. This parameter determines whether to check non-interactive user sign-in logs.

  2. Event Filtering:

    • The query first identifies relevant audit log events that indicate changes in authentication methods or MFA settings. It filters out events that are not successful or are irrelevant, such as those related to password changes or system errors.
    • It distinguishes between "core" and "non-core" events based on the service that logged the event and the operation name.
  3. Data Joining and Enrichment:

    • The query joins these filtered events with user update events to gather additional context, such as the user involved and the specific changes made.
    • It also optionally joins with non-interactive sign-in logs if check_signinlogs is set to true, to provide more information about the user's sign-in activity around the time of the change.
  4. Data Processing:

    • The query processes the data to ensure that only the most relevant and recent events are considered.
    • It extracts and organizes information about the changes, such as the initiator of the change, the target user, and the specific properties that were modified.
  5. Output:

    • The final output includes details such as the time of the event, the service that logged it, the operation performed, the result, the initiator's identity, the target user, and any additional details about the change.

This query is useful for security and compliance purposes, as it helps administrators monitor and audit changes to authentication settings in their organization.

Details

Jose Sebastián Canós profile picture

Jose Sebastián Canós

Released: July 29, 2025

Tables

AuditLogs AADNonInteractiveUserSignInLogs

Keywords

AccountsAuthenticationMethodsEntraIDUserDevicesSecurityInfoMultifactorUserManagementAzureMFAMicrosoftGraph

Operators

letcasehas_anynotmv-expandisnotemptyjoinkind=leftoutersummarizearg_maxiifcoalesceprojectiffdatetimetoscalarmake_set_ifmv-applymake_bagbag_packincontainsarray_sort_ascextract_alltolowerasbetweenunionbag_remove_keysset_differencebag_keyssplittrimevaluatebag_unpack

Actions

GitHub