The primary detection field
Every successful device code authorization writes a row to SigninLogs in your Sentinel workspace. The field that distinguishes it from every other sign-in type is AuthenticationProtocol. For device code flow completions, the value is deviceCode.
Normal interactive sign-ins write oAuth2. Passwordless writes passwordless. Device code writes deviceCode. This is not an attacker artifact — it is a standard Microsoft telemetry field that accurately describes the authentication grant type used. An attacker cannot suppress it.
The detection gap that existed before 2024 was that most SOC teams built their alert logic around credential indicators (password spray, impossible travel, unfamiliar location) and session indicators (cookie replay, token anomalies). Device code phishing clears all of those filters because the authentication is legitimate. The AuthenticationProtocol field was always there. It just was not being watched.
Rule 01 — Successful device code sign-in (analytics rule)
// Rule: Successful Device Code Sign-Inchr(10)// Fires on any completed device code authorization against your tenant.chr(10)// Schedule: every 1 hour, lookback 1 hour.chr(10)// Severity: Medium (High if combined with anomalous IP or first-seen app).chr(10)chr(10)SigninLogschr(10)| where TimeGenerated > ago(1h)chr(10)| where AuthenticationProtocol == "deviceCode"chr(10)| where ResultType == 0chr(10)| extendchr(10)AppName = tostring(AppDisplayName),chr(10)SourceIP = tostring(IPAddress),chr(10)Country = tostring(LocationDetails.countryOrRegion),chr(10)City = tostring(LocationDetails.city),chr(10)ASN = tostring(AutonomousSystemNumber),chr(10)DeviceId = tostring(DeviceDetail.deviceId),chr(10)OS = tostring(DeviceDetail.operatingSystem),chr(10)MFADetail = tostring(AuthenticationDetails)chr(10)| projectchr(10)TimeGenerated,chr(10)UserPrincipalName,chr(10)AppName,chr(10)AppId,chr(10)SourceIP,chr(10)Country,chr(10)City,chr(10)ASN,chr(10)DeviceId,chr(10)OS,chr(10)MFADetail,chr(10)CorrelationIdchr(10)| sort by TimeGenerated descSet this as a scheduled analytics rule firing every hour with a 1-hour lookback. Alert on any result. The baseline for most tenants is zero or near-zero — device code sign-ins almost never happen for normal users.
Rule 02 — First-time device code sign-in for a user (higher fidelity)
// Rule: First-ever device code sign-in for a given user.chr(10)// Compares the last 24 hours against the prior 14 days.chr(10)// Near-zero false positives for tenants that have blocked the flow in CAchr(10)// but want to catch anything that slips through.chr(10)chr(10)let lookback = 14d;chr(10)let detection = 1h;chr(10)chr(10)let historical =chr(10)SigninLogschr(10)| where TimeGenerated between (ago(lookback) .. ago(detection))chr(10)| where AuthenticationProtocol == "deviceCode"chr(10)| where ResultType == 0chr(10)| summarize HistoricalApps = make_set(AppId) by UserPrincipalName;chr(10)chr(10)SigninLogschr(10)| where TimeGenerated > ago(detection)chr(10)| where AuthenticationProtocol == "deviceCode"chr(10)| where ResultType == 0chr(10)| join kind=leftanti historical on UserPrincipalNamechr(10)| extendchr(10)AppName = tostring(AppDisplayName),chr(10)SourceIP = tostring(IPAddress),chr(10)Country = tostring(LocationDetails.countryOrRegion)chr(10)| project TimeGenerated, UserPrincipalName, AppName, AppId, SourceIP, Country, CorrelationIdThis rule fires only on users who have never completed a device code flow in the prior 14 days. Once you deploy the CA block policy (see mitigations post), this rule becomes your canary: any hit means the CA policy has a gap.
Rule 03 — Device code sign-in from an unusual ASN
// Enrichment layer: flag device code sign-ins from ASNs not seenchr(10)// in normal interactive sign-ins for the same user in the last 30 days.chr(10)// Useful for detecting attacker-side polling from cloud or VPS infrastructure.chr(10)chr(10)let normal_asns =chr(10)SigninLogschr(10)| where TimeGenerated > ago(30d)chr(10)| where AuthenticationProtocol != "deviceCode"chr(10)| where ResultType == 0chr(10)| summarize KnownASNs = make_set(AutonomousSystemNumber) by UserPrincipalName;chr(10)chr(10)SigninLogschr(10)| where TimeGenerated > ago(1h)chr(10)| where AuthenticationProtocol == "deviceCode"chr(10)| where ResultType == 0chr(10)| join kind=leftouter normal_asns on UserPrincipalNamechr(10)| where not(set_has_element(KnownASNs, AutonomousSystemNumber))chr(10)| extendchr(10)AppName = tostring(AppDisplayName),chr(10)SourceIP = tostring(IPAddress),chr(10)Country = tostring(LocationDetails.countryOrRegion),chr(10)ASN = tostring(AutonomousSystemNumber)chr(10)| project TimeGenerated, UserPrincipalName, AppName, SourceIP, Country, ASN, CorrelationIdWhy this matters: the SigninLogs row for a device code sign-in reflects where the victim authenticated, not where the attacker's polling loop is running. If the victim authenticated from their office IP but the ASN in the row is a cloud provider or Tor exit, that is the attacker's infrastructure on the polling side leaking into the log. This happens when the attacker and victim are on different networks and the sign-in row picks up the final token exchange endpoint.
What the attacker event looks like in SigninLogs
From our lab run against a Microsoft 365 developer tenant using the Microsoft Office first-party client:
AuthenticationProtocol—deviceCodeAppDisplayName—Microsoft OfficeAppId—d3590ed6-52b3-4102-aeff-aad2292ab01c(Microsoft Office first-party)ResultType—0(success)AuthenticationRequirement—multiFactorAuthentication(MFA was satisfied)ConditionalAccessStatus—success(no CA policy blocked the flow)ResourceDisplayName—Microsoft GraphTokenIssuerType—AzureAD
Note what is not anomalous: the IP is the victim's real IP, the location is the victim's real location, MFA is marked satisfied, CA is marked passed. Every indicator that AiTM-focused detections watch is clean. The only indicator that a device code phishing event occurred is AuthenticationProtocol == "deviceCode" on an app the user has never previously authorized through that flow.
False-positive breakdown
AuthenticationProtocol == "deviceCode" with ResultType == 0 generates false positives from:
- Azure CLI users.
az loginwith--use-device-codeflag. AppId04b07795-8ddb-461a-bbee-02f9e1bf7b46(Azure CLI). Legitimate developer workflow. Allowlist this AppId in Rule 01 or tune Rule 02 lookback to catch novel apps only. - Azure PowerShell.
Connect-AzAccount -UseDeviceAuthentication. AppId1950a258-227b-4e31-a9cf-717495945fc2. Same pattern, same allowlist approach. - VSCode Azure extensions. Some authenticate via device code when browser-redirect auth is not available. AppId varies by extension.
- IoT or line-of-business devices. Any device configured to use device code flow. Known in advance, should be in your CA allowlist (see mitigations post).
If you have deployed the CA block policy correctly, you will see failed attempts (ResultType != 0) for users attempting device code from policy-blocked scenarios, and successful completions only from your explicitly allowlisted apps or device groups. At that point Rule 01 becomes high-fidelity with near-zero FP: every success is either an allowlisted legitimate use or a CA gap.
What ResultType to watch for
ResultType == 0— completed authorization. Token issued. This is the one you alert on.ResultType == 50097— "Device authentication required" — normal friction, not an attack signal.ResultType == 70043or70044— device code expired or polling timeout. Could indicate victim refused to complete the flow, or the lure page was inspected without being executed.
ResultType 70043/70044 with an unusual ASN is worth flagging separately: it may indicate a probe of your tenant's device code availability without a successful capture.