Why This Technique Keeps Working

Device code phishing is one of the few credential attacks where every defender heuristic looks clean. The link in the lure points at a real Microsoft domain. The TLS chain is valid. The sign-in page is the one users were trained to trust. The multi-factor prompt is genuine, performed on a known device, and approved by the legitimate user. The only thing that is wrong is the device polling for the token on the other end of the flow.

That asymmetry is why Storm-2372 and adjacent clusters spent most of 2025 leaning into the technique against government, NGO, defense, and energy targets, and why detection teams who only watch for credential pages and lookalike domains miss it entirely. The artifact that gives the attack away does not live in mail flow. It lives in the sign-in logs.

The Legitimate Flow in One Paragraph

The OAuth 2.0 Device Authorization Grant, defined in RFC 8628, exists for input-constrained devices like smart TVs and CLI tools. The device asks the identity provider for a device code and a short user code, then displays instructions like "go to microsoft.com/devicelogin and enter ABC-XYZ-123." A user authenticates on a richer device, types the user code, consents, and the original device polls a token endpoint until it receives an access token and a refresh token. Nothing about that exchange is unusual when the device asking for the code is the device the user is sitting in front of.

The Phishing Inversion

The attack inverts step two. The operator initiates the device code request themselves against Entra ID, typically using a high-value first-party client identifier such as the Microsoft Office or Azure CLI client, which inherits broad default scopes. The identity provider returns a user code and verification URL. The operator then phishes the victim with a message that delivers those exact strings, framed as a Teams join, a secure document, or a help desk verification step. The victim opens the genuine Microsoft sign-in page, enters their credentials, completes MFA on their registered device, and approves the prompt. The attacker's polling client receives tokens within seconds.

The refresh token is the prize. Default Entra ID refresh tokens are long-lived and rolling, so a single successful phish often produces persistence that survives password resets unless an administrator explicitly revokes sessions or refresh tokens for the user.

End-to-End Walkthrough

A representative campaign opens with a Teams chat invitation or external email impersonating a partner organization, often timed to a real meeting on the target's calendar to add legitimacy. The body says something like "to join the secure briefing, sign in at microsoft.com/devicelogin and enter code FXZK-RPQT" with a short window of urgency. The user code is freshly minted by the operator's client and is valid for roughly fifteen minutes.

When the victim signs in, the Entra sign-in event is recorded with an authentication protocol of deviceCode and a client app that reflects the first-party identifier the operator chose. The IP address on that sign-in is the victim's, not the attacker's. From a perimeter logging standpoint, this is the trap: the sign-in looks normal because the user really did authenticate.

The next event is the one defenders should care about. Once the access and refresh tokens are issued, the attacker uses them from their own infrastructure. That produces a second wave of activity, often within minutes, in which the same user identity appears in non-interactive token redemption events from a different IP, a different ASN, and a different user agent. Mailbox enumeration, OneDrive exfiltration, or new app consents typically follow inside the first hour.

What the Telemetry Looks Like

In Entra ID sign-in logs, the highest-confidence field is AuthenticationProtocol equal to deviceCode. In normal enterprises this value should be rare and tied to a small set of expected clients. Pair it with the AppDisplayName field to see which first-party client was abused. The ResourceDisplayName tells you which downstream API the token will hit, which helps prioritize: a deviceCode sign-in resolving to Microsoft Graph or Exchange Online warrants faster action than one resolving to a developer tooling endpoint.

The follow-on token use shows up in non-interactive sign-in logs. A legitimate device code redemption is followed by token refreshes from the same device fingerprint and IP. A phished one shows the redemption from one location and immediate refresh activity from a second location, often a hosting provider ASN or residential proxy network. Correlation across the interactive and non-interactive logs is what separates a real-world detection from a noisy one.

Microsoft Sentinel Detection Patterns

The simplest useful rule is a scheduled analytics rule over SigninLogs that filters on the device code authentication protocol and projects the fields a triage analyst needs first. In environments where device code is genuinely never used, this runs as a near real-time alert with no further enrichment. In environments with legitimate developer use, allow-list the AppDisplayName values your engineering teams actually use and alert on everything else.

SigninLogs
| where TimeGenerated > ago(1h)
| where AuthenticationProtocol == "deviceCode"
| where ResultType == 0
| where AppDisplayName !in ("Visual Studio Code", "Azure CLI")
| project TimeGenerated, UserPrincipalName, AppDisplayName,
          ResourceDisplayName, IPAddress, Location, ClientAppUsed,
          UserAgent, CorrelationId

The higher-fidelity rule joins SigninLogs against AADNonInteractiveUserSignInLogs on UserPrincipalName, looking for a device code interactive sign-in followed within fifteen minutes by a non-interactive token use from a different country or ASN. That join is the closest thing to a smoking gun, because it captures the moment the token leaves the victim's device and arrives on the attacker's.

let window = 15m;
let deviceCodeSignins =
    SigninLogs
    | where TimeGenerated > ago(2h)
    | where AuthenticationProtocol == "deviceCode" and ResultType == 0
    | project VictimTime = TimeGenerated, UserPrincipalName,
              VictimIP = IPAddress, VictimCountry = LocationDetails.countryOrRegion,
              VictimUA = UserAgent, AppDisplayName;
let tokenReuse =
    AADNonInteractiveUserSignInLogs
    | where TimeGenerated > ago(2h)
    | where ResultType == 0
    | project ReuseTime = TimeGenerated, UserPrincipalName,
              ReuseIP = IPAddress, ReuseCountry = LocationDetails.countryOrRegion,
              ReuseUA = UserAgent, ReuseApp = AppDisplayName;
deviceCodeSignins
| join kind=inner tokenReuse on UserPrincipalName
| where ReuseTime between (VictimTime .. VictimTime + window)
| where VictimIP != ReuseIP and VictimCountry != ReuseCountry
| project VictimTime, ReuseTime, UserPrincipalName, AppDisplayName,
          VictimIP, VictimCountry, ReuseIP, ReuseCountry, ReuseApp

A third rule worth running is consent and registration adjacency. Catch operators who chain device code phishing into persistent OAuth app consent or rogue device registration, which is the standard escalation path.

let dc =
    SigninLogs
    | where TimeGenerated > ago(2h)
    | where AuthenticationProtocol == "deviceCode" and ResultType == 0
    | project SigninTime = TimeGenerated, UserPrincipalName, IPAddress;
AuditLogs
| where TimeGenerated > ago(2h)
| where OperationName in ("Consent to application", "Add device",
                          "Add delegated permission grant",
                          "Add service principal")
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| join kind=inner dc on $left.Actor == $right.UserPrincipalName
| where TimeGenerated between (SigninTime .. SigninTime + 1h)
| project TimeGenerated, SigninTime, OperationName, Actor,
          TargetResources, IPAddress

Defender XDR and Defender for Cloud Apps Hooks

Defender for Cloud Apps surfaces several of these patterns out of the box, but two policies are worth tightening. The "Unusual ISP for an OAuth app" alert fires on the ASN shift between victim and attacker and should be configured to auto-suspend the user's sessions for high-severity hits. The "Activity from infrequent country" policy is a useful secondary signal, especially when paired with mailbox-rule-creation alerts, since exfiltration almost always begins with an inbox rule.

In Defender XDR advanced hunting, the equivalent of the Sentinel join lives in the IdentityLogonEvents and CloudAppEvents tables. The benefit of doing it in XDR is that the response actions, including session revocation and user disablement, are one click from the alert.

let dc =
    IdentityLogonEvents
    | where Timestamp > ago(2h)
    | where LogonType == "Interactive" and Protocol == "deviceCode"
    | where ActionType == "LogonSuccess"
    | project DcTime = Timestamp, AccountObjectId, AccountUpn,
              DcIP = IPAddress, DcLocation = Location;
CloudAppEvents
| where Timestamp > ago(2h)
| where ActionType in ("MailItemsAccessed", "FileDownloaded",
                       "Add-MailboxPermission", "New-InboxRule",
                       "Update-InboxRule")
| join kind=inner dc on AccountObjectId
| where Timestamp between (DcTime .. DcTime + 15m)
| where IPAddress != DcIP
| project Timestamp, DcTime, AccountUpn, ActionType, Application,
          IPAddress, DcIP, ISP, CountryCode

Conditional Access offers the only real preventive control. A policy that targets the device code authentication flow and blocks it for all users except a named group of engineers and service identities removes the attack surface for the population that should never see the flow in the first place. This is a 2024-and-later capability in Entra Conditional Access, and it is the single highest-leverage change a defender can make after deploying detection.

Response Playbook

When a deviceCode alert fires with a non-interactive follow-on from a second location, the response order matters. Revoke the user's refresh tokens before disabling the account, because account disable alone does not invalidate already-issued tokens. Then audit AuditLogs for new app consents, new device registrations, and new mailbox rules in the prior twenty-four hours, and reverse any that were created. Only then move to user notification and password reset. Reversing the order leaves the attacker with a valid refresh token even after the password change, which is the failure mode that has produced most of the public incidents tied to this technique.

Beyond Detection

Phishing-resistant authentication is the long answer. FIDO2 security keys and platform passkeys do not change the device code flow itself, but they make the broader credential theft economy expensive enough that operators move on. In the meantime, the combination of a Conditional Access block on the device code grant for non-engineering users, a Sentinel rule on the deviceCode protocol with cross-log correlation, and a revocation-first response playbook is the package that materially reduces risk today. None of the three is novel. The reason this attack still works is that most environments have deployed zero of them.