LoFP LoFP / legitimate use of device code flow where a user authenticates via browser for a cli tool or headless application. common legitimate scenarios include azure cli, azure powershell, or vs code remote development. review the user agent combinations - browser + known cli tool from the same user may be expected behavior.

Techniques

Sample rules

Entra ID OAuth Device Code Flow with Concurrent Sign-ins

Description

Identifies Entra ID device code authentication flows where multiple user agents are observed within the same session. This pattern is indicative of device code phishing, where an attacker’s polling client (e.g., Python script) and the victim’s browser both appear in the same authentication session. In legitimate device code flows, the user authenticates via browser while the requesting application polls for tokens - when these have distinctly different user agents (e.g., Python Requests vs Chrome), it may indicate the code was phished and redeemed by an attacker.

Detection logic

from logs-azure.signinlogs-* metadata _id, _version, _index

| where event.category == "authentication" and event.dataset == "azure.signinlogs" and
        azure.signinlogs.properties.original_transfer_method == "deviceCodeFlow"

// Track events with deviceCode authentication protocol (browser auth) vs polling client
| eval is_device_code_auth = case(azure.signinlogs.properties.authentication_protocol == "deviceCode", 1, 0)

| stats Esql.count_logon = count(*),
        Esql.device_code_auth_count = sum(is_device_code_auth),
        Esql.timestamp_values = values(@timestamp),
        Esql.source_ip_count_distinct = count_distinct(source.ip),
        Esql.user_agent_count_distinct = count_distinct(user_agent.original),
        Esql.user_agent_values = values(user_agent.original),
        Esql.authentication_protocol_values = values(azure.signinlogs.properties.authentication_protocol),
        Esql.azure_signinlogs_properties_client_app_values = values(azure.signinlogs.properties.app_display_name),
        Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id),
        Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),
        Esql.azure_signinlogs_properties_auth_requirement_values = values(azure.signinlogs.properties.authentication_requirement),
        Esql.azure_signinlogs_properties_tenant_id = values(azure.tenant_id),
        Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code),
        Esql.message_values = values(message),
        Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id),
        Esql.source_ip_values = values(source.ip)
        by azure.signinlogs.properties.session_id, azure.signinlogs.identity

// Require: 2+ events, at least one deviceCode auth protocol event, and either 2+ IPs or 2+ user agents
| where Esql.count_logon >= 2 and Esql.device_code_auth_count >= 1 and (Esql.source_ip_count_distinct >= 2 or Esql.user_agent_count_distinct >= 2)
| keep
       Esql.*,
       azure.signinlogs.properties.session_id,
       azure.signinlogs.identity