On this page
Device code phishing is nasty because the user does not hand over a password. They hand over a session.
The lure sends the victim to a legitimate Microsoft device sign-in page. The victim enters a short code. Entra ID issues tokens to the attacker’s waiting client. MFA can still be satisfied because the victim completed the approval ceremony at Microsoft, not on a fake login page.
Microsoft’s April 6, 2026 write-up on an AI-enabled device code phishing campaign makes the shift clear. This is no longer a novelty flow abused by hand. Microsoft described AI-driven infrastructure and end-to-end automation, plus Defender XDR detections for suspicious device code authentication, token replay, and post-compromise activity.
The wrong answer is “block device code flow everywhere tomorrow.” Some tenants still have legitimate device-code dependencies, including Azure CLI, PowerShell, Teams Rooms, shared devices, lab environments, and emergency workflows. The better answer is inventory first, carve out the few workflows that truly need it, then detect the abuse chain before a token turns into mailbox or payroll access.
Hands-on lab. The companion lab lives at Lab: Entra Device Code Phishing Detection. It includes Sentinel KQL, Defender XDR hunts, sample replay data, and a triage checklist.
The lab now includes a safe PowerShell 7+ device-code telemetry generator. It signs in a lab-owned, non-privileged user through a no-secret public client app, polls first to produce the 50199 interrupt pattern, then discards the issued device-code tokens immediately without using them to access mail, Graph, or any workload resource.
cd labs/entra-device-code-phishing
.\scripts\run-device-code-telemetry-test.ps1 -Confirm
.\scripts\check-device-code-telemetry.ps1 -RunId "<run-id>"This is a telemetry generator, not a phishing simulator. Use a lab account only.
Real Microsoft Page. Wrong Session.
The user approves a legitimate code. Entra ID issues the token to the attacker's waiting client.
Waiting OAuth client
The adversary starts the device authorization flow and waits for the victim to complete it.
Enter code
The page is real, so URL reputation alone can miss the trick. The session context is the attack.
What the token can touch next
What Makes Device Code Phishing Different
Classic credential phishing tries to steal a username and password. Adversary-in-the-middle phishing tries to proxy a full browser session. Device code phishing is quieter. The attacker starts an OAuth device authorization flow and sends the victim the code.
The victim sees a real Microsoft page.
https://microsoft.com/devicelogin
The victim enters the code and approves the sign-in. The attacker never needs to know the password. The token is issued to the attacker’s client.
That creates a different detection problem. You are not looking only for failed passwords or impossible travel. You are looking for the sequence around the device code flow and the behavior after token issuance.
Microsoft’s campaign write-up calls out several high-value signals.
- A
50199interrupt followed by success in a short window, especially when correlated with device-code protocol or session context - Device code authentication from suspicious infrastructure
- URL clicks that precede device code authentication
- Device registration after compromise
- Inbox rule or mail access activity after token issuance
- Token replay patterns where the user-agent or client behavior changes after authentication
One important nuance is that 50199 is useful, but it is not device-code proof by itself. Microsoft documents AADSTS50199 as CmsiInterrupt, a user-confirmation interrupt. In this post, 50199 -> 0 is treated as a device-code-phishing signal only when it is correlated with AuthenticationProtocol == deviceCode, app/client identity, URL-click timing, session or correlation context, or post-token behavior.
The Legitimate Workflow Problem
Device code flow exists for a reason. It helps devices and tools sign in when a full browser flow is awkward or unavailable.
Legitimate examples include the following.
| Workflow | Why It Uses Device Code | Recommended Handling |
|---|---|---|
| Azure CLI or Azure PowerShell | Developer/admin shell login | Allow only from managed devices or trusted locations |
| Teams Rooms and shared devices | Device provisioning and reauth | Use dedicated room/resource accounts, trusted locations, managed/compliant device conditions or device filters where applicable, documented app/resource IDs, and explicit monitoring |
| Lab or break-fix environments | Temporary admin access | Time-box and monitor with alerting |
| Legacy command-line tools | No embedded browser support | Replace where possible; exception otherwise |
The security goal is not to pretend those do not exist. The goal is to make every device-code use explainable.
Your first hunt should answer one question.
SigninLogs
| where TimeGenerated > ago(30d)
| extend AuthProtocol = tostring(column_ifexists("AuthenticationProtocol", ""))
| extend Result = tostring(ResultType)
| where AuthProtocol =~ "deviceCode"
or ResultDescription has "device code"
or Result == "50199"
| summarize Events=count(), Users=dcount(UserPrincipalName), IPs=dcount(IPAddress)
by AppDisplayName, AppId
| order by Events desc
If this query shows applications nobody can explain, you have your first cleanup list.
Detection Architecture
Data-source requirements matter. SigninLogs requires Entra ID diagnostic logs flowing into Log Analytics or Sentinel. UrlClickEvents requires Defender for Office 365 Safe Links telemetry. CloudAppEvents requires Defender for Cloud Apps and connected Microsoft 365/SaaS activity. EntraIdSignInEvents requires Microsoft Entra ID P2 data in Defender XDR advanced hunting.
Correlate the Session Chain
Device-code abuse is weak as a single signal. It gets loud when sign-ins, clicks, app identity, and post-token actions line up.
50199 -> successFind the interrupted device-code ceremony and the follow-on success.
risk + app + sessionBring Defender XDR sign-in context into the chain.
lure -> auth windowConnect the phishing delivery moment to the sign-in.
mailbox + SaaS actionsWatch what the issued token does after authentication.
User + session + app + time windowRule 1: 50199 -> successPromote after tuning known device-code apps.
allowlist drivenTurn inventory into policy without breaking Teams Rooms or admin tools.
mailbox, device, SaaSEscalate when the token starts touching sensitive workflows.
Native Browser Telemetry Plus Two Live Sentinel Rules
Rendered from SigninLogs and az sentinel alert-rule list against the lab workspace after authenticating the lab account in the in-app browser.
LAB - Nine Lives Device Code Telemetry
The lab app produced a real 50199 -> 0 sequence for [email protected].
- 50199
- 2026-04-26 02:51:39Z
- Success
- 2026-04-26 02:52:53Z
- Correlation
9456e1ee...daba57
LAB - Device Code - 50199 Followed by Success
- Severity
- High
- Frequency
- 15 minutes
- Tactics
- Initial Access + Defense Evasion
LAB - Device Code - Unapproved Client
- Severity
- Medium
- Frequency
- 1 hour
- Tactics
- Initial Access + Credential Access
In the live lab run, the in-app browser ceremony produced one defensible shape the first Sentinel rule hunts for. The event sequence was 50199 at 2026-04-26T02:51:39Z, then a successful sign-in at 2026-04-26T02:52:53Z, both for [email protected] against the lab public client app and the same correlation ID.

device-code-50199-to-success rule.This lab uses five detection layers.
- Device code interrupt followed by success
- Device code authentication by an unapproved app or client
- URL click followed by suspicious device-code sign-in
- Post-token mailbox access or inbox rule creation
- Device registration or persistence after device-code auth
The strongest alerts come from correlation. A single device-code sign-in might be benign. A rare sender URL click, followed by 50199 -> success, followed by inbox rule creation is not benign.
Rule 1 - Device Code Interrupt Followed by Success
Microsoft’s Storm-2372 and device-code guidance repeatedly points at the same shape. An interrupt or failure appears, then a successful sign-in follows shortly after. In Defender XDR, Microsoft shows this as ErrorCode values 50199 and 0 grouped by user, session, or correlation context. The 50199 value still needs context; do not treat it as a device-code-only error.
For Sentinel workspaces using Entra diagnostic tables, start with SigninLogs.
let Lookback = 15m;
let Window = 5m;
let ApprovedDeviceCodeApps = dynamic([
"Microsoft Azure CLI",
"Azure CLI",
"Microsoft Azure PowerShell",
"Microsoft Teams Rooms"
]);
let ApprovedDeviceCodeAppIds = dynamic([
"04b07795-8ddb-461a-bbee-02f9e1bf7b46", // Microsoft Azure CLI
"1950a258-227b-4e31-a9cf-717495945fc2" // Microsoft Azure PowerShell
]);
let Interrupts =
SigninLogs
| where TimeGenerated > ago(Lookback)
| extend Result = tostring(ResultType)
| where Result == "50199" or ResultDescription has "50199"
| extend SessionId = tostring(column_ifexists("SessionId", ""))
| project InterruptTime = TimeGenerated, UserPrincipalName,
CorrelationId, SessionId, IPAddress, AppDisplayName, AppId, UserAgent;
let Successes =
SigninLogs
| where TimeGenerated > ago(Lookback)
| extend Result = tostring(ResultType)
| where Result == "0"
| extend SessionId = tostring(column_ifexists("SessionId", ""))
| project SuccessTime = TimeGenerated, UserPrincipalName,
CorrelationId, SessionId, SuccessIP = IPAddress,
SuccessApp = AppDisplayName, SuccessAppId = AppId, SuccessUserAgent = UserAgent;
Interrupts
| join kind=inner Successes on UserPrincipalName, CorrelationId
| where SuccessTime between (InterruptTime .. InterruptTime + Window)
| where AppDisplayName !in~ (ApprovedDeviceCodeApps)
and SuccessApp !in~ (ApprovedDeviceCodeApps)
and AppId !in~ (ApprovedDeviceCodeAppIds)
and SuccessAppId !in~ (ApprovedDeviceCodeAppIds)
| project TimeGenerated = SuccessTime, InterruptTime, UserPrincipalName,
AppDisplayName, AppId, IPAddress, SuccessIP, UserAgent, SuccessUserAgent,
CorrelationId, SessionId
Use this as a high-signal hunting query first. Keep the allowlist close to your tenant reality; legitimate Azure CLI or PowerShell device-code logins can produce the same 50199 -> 0 ceremony.
One deliberate choice matters. Microsoft Authentication Broker is not in the default allowlist. It can be legitimate in brokered authentication and device-registration scenarios, but Microsoft has reported Storm-2372 abusing the Microsoft Authentication Broker client ID in device-code flows. Treat it as a sensitive exception, not a universal safe entry; require documented use cases, enrollment restrictions, trusted locations, managed or compliant device requirements where possible, dedicated accounts, and device-registration monitoring before suppressing it.
Rule 2 - Device Code Flow From an Unapproved Client
This one is inventory-driven. Build an allowlist of device-code clients your tenant actually uses, then alert on everything else.
let ApprovedDeviceCodeApps = dynamic([
"Microsoft Azure CLI",
"Azure CLI",
"Microsoft Azure PowerShell",
"Microsoft Teams Rooms"
]);
let ApprovedDeviceCodeAppIds = dynamic([
"04b07795-8ddb-461a-bbee-02f9e1bf7b46", // Microsoft Azure CLI
"1950a258-227b-4e31-a9cf-717495945fc2" // Microsoft Azure PowerShell
]);
let Lookback = 1h;
let Window = 5m;
let DirectDeviceCodeEvents =
SigninLogs
| where TimeGenerated > ago(Lookback)
| extend AuthProtocol = tostring(column_ifexists("AuthenticationProtocol", ""))
| where AuthProtocol =~ "deviceCode" or ResultDescription has "device code"
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, AppId;
let Interrupts =
SigninLogs
| where TimeGenerated > ago(Lookback)
| extend Result = tostring(ResultType)
| where Result == "50199" or ResultDescription has "50199"
| project InterruptTime = TimeGenerated, UserPrincipalName, CorrelationId;
let CorrelatedSuccesses =
SigninLogs
| where TimeGenerated > ago(Lookback)
| extend Result = tostring(ResultType)
| where Result == "0"
| project SuccessTime = TimeGenerated, UserPrincipalName, CorrelationId,
IPAddress, AppDisplayName, AppId;
let DeviceCodeLikeEvents =
Interrupts
| join kind=inner CorrelatedSuccesses on UserPrincipalName, CorrelationId
| where SuccessTime between (InterruptTime .. InterruptTime + Window)
| project TimeGenerated = SuccessTime, UserPrincipalName, IPAddress, AppDisplayName, AppId;
union DirectDeviceCodeEvents, DeviceCodeLikeEvents
| where AppDisplayName !in~ (ApprovedDeviceCodeApps)
and AppId !in~ (ApprovedDeviceCodeAppIds)
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated),
Attempts=count(), Users=dcount(UserPrincipalName), IPs=dcount(IPAddress),
SampleUsers=make_set(UserPrincipalName, 10), SampleIPs=make_set(IPAddress, 10)
by AppDisplayName, AppId
| project TimeGenerated = LastSeen, FirstSeen, LastSeen,
AppDisplayName, AppId, Attempts, Users, IPs, SampleUsers, SampleIPs
| order by Attempts desc
This rule is boring in the best way. It forces the tenant to document exceptions.
Rule 3 - URL Click Followed by Device Code Auth
This is a Defender XDR advanced hunting pattern because the valuable correlation crosses email click telemetry and Entra sign-in telemetry.
let Window = 7m;
let SuspiciousClicks =
UrlClickEvents
| where Timestamp > ago(24h)
| extend ClickedThrough = tolower(tostring(IsClickedThrough)) in ("true", "1")
| where ActionType in ("ClickAllowed", "UrlScanInProgress", "UrlErrorPage")
or ClickedThrough
or UrlChain has_any ("microsoft.com/devicelogin", "login.microsoftonline.com/common/oauth2/deviceauth")
or Url has_any ("microsoft.com/devicelogin", "login.microsoftonline.com/common/oauth2/deviceauth")
| project ClickTime = Timestamp, AccountUpn = tolower(AccountUpn),
Url, UrlChain, NetworkMessageId, ActionType, ClickedThrough;
let DeviceCodeSignins =
EntraIdSignInEvents
| where Timestamp > ago(24h)
| where ErrorCode in (0, 50199)
| summarize ErrorCodes=make_set(ErrorCode), FirstSignin=min(Timestamp), LastSignin=max(Timestamp),
IPs=make_set(IPAddress, 10), Apps=make_set(Application, 10), RiskLevels=make_set(RiskLevelAggregated, 10)
by AccountUpn=tolower(AccountUpn), SessionId
| where ErrorCodes has_all (0, 50199);
SuspiciousClicks
| join kind=inner DeviceCodeSignins on AccountUpn
| where FirstSignin between (ClickTime .. ClickTime + Window)
| project FirstSignin, ClickTime, AccountUpn, Url, UrlChain, ActionType, ClickedThrough,
ErrorCodes, IPs, Apps, RiskLevels, NetworkMessageId, SessionId
The point is not that every click is malicious. The point is sequence. A mail/web click appears, then a device-code-style auth interrupt, then success.
Rule 4 - Post-Token Mailbox Abuse
Attackers do not stop at sign-in. They read mail, create forwarding or inbox rules, and search for payroll or invoice context.
Microsoft’s April 6 guidance includes CloudAppEvents hunting examples for Exchange activity. This query is a Defender XDR advanced hunting pattern unless you export CloudAppEvents into Sentinel; either way, it looks for mailbox control changes shortly after suspicious device-code activity.
One join caveat matters. CloudAppEvents.AccountId is not guaranteed to be a UPN in every tenant or connected app. Prefer AccountObjectId when it is present on both sides of your join, or validate how AccountId is populated before relying on the UPN-style join below.
let Window = 60m;
let DeviceCodeInterrupts =
EntraIdSignInEvents
| where Timestamp > ago(24h)
| where ErrorCode == 50199
| project AccountUpn = tolower(AccountUpn), SessionId,
InterruptTime = Timestamp;
let DeviceCodeSuccesses =
EntraIdSignInEvents
| where Timestamp > ago(24h)
| where ErrorCode == 0
| project AccountUpn = tolower(AccountUpn), SessionId,
DeviceCodeTime = Timestamp, SignInIP = IPAddress, Application;
let SuspiciousDeviceCodeSessions =
DeviceCodeInterrupts
| join kind=inner DeviceCodeSuccesses on AccountUpn, SessionId
| where DeviceCodeTime between (InterruptTime .. InterruptTime + 5m)
| summarize DeviceCodeTime=max(DeviceCodeTime),
FirstInterrupt=min(InterruptTime),
SignInIPs=make_set(SignInIP, 10),
Apps=make_set(Application, 10)
by AccountUpn, SessionId;
CloudAppEvents
| where Timestamp > ago(24h)
| where ApplicationId == 20893 or Application == "Microsoft Exchange Online"
| where ActionType in (
"New-InboxRule", "Set-InboxRule", "Enable-InboxRule",
"Set-Mailbox", "New-TransportRule", "Set-TransportRule",
"UpdateInboxRules", "MailItemsAccessed"
)
| extend AccountUpn = tolower(AccountId)
| join kind=inner SuspiciousDeviceCodeSessions on AccountUpn
| where Timestamp between (DeviceCodeTime .. DeviceCodeTime + Window)
| project Timestamp, DeviceCodeTime, FirstInterrupt, AccountUpn, SessionId,
ActionType, IPAddress, UserAgent, RawEventData
| order by Timestamp desc
If your tenant has payroll systems like Workday behind SSO, add CloudAppEvents for those apps too. That is where device-code auth becomes business impact.
Rule 5 - Device Registration After Device Code Compromise
Some campaigns register devices after token compromise. Treat device registration as a persistence signal when it follows a suspicious authentication sequence. Like the mailbox hunt, this version uses Defender XDR CloudAppEvents.
let Window = 2h;
let DeviceCodeInterrupts =
EntraIdSignInEvents
| where Timestamp > ago(24h)
| where ErrorCode == 50199
| project AccountUpn = tolower(AccountUpn), SessionId,
InterruptTime = Timestamp;
let DeviceCodeSuccesses =
EntraIdSignInEvents
| where Timestamp > ago(24h)
| where ErrorCode == 0
| project AccountUpn = tolower(AccountUpn), SessionId,
DeviceCodeTime = Timestamp, SignInIP = IPAddress, Application;
let SuspiciousDeviceCodeSessions =
DeviceCodeInterrupts
| join kind=inner DeviceCodeSuccesses on AccountUpn, SessionId
| where DeviceCodeTime between (InterruptTime .. InterruptTime + 5m)
| summarize DeviceCodeTime=max(DeviceCodeTime),
FirstInterrupt=min(InterruptTime),
SignInIPs=make_set(SignInIP, 10),
Apps=make_set(Application, 10)
by AccountUpn, SessionId;
CloudAppEvents
| where Timestamp > ago(24h)
| where AccountDisplayName == "Device Registration Service"
| extend AccountUpn = tolower(tostring(RawEventData.ObjectId))
| join kind=inner SuspiciousDeviceCodeSessions on AccountUpn
| where Timestamp between (DeviceCodeTime .. DeviceCodeTime + Window)
| extend DeviceName = tostring(parse_json(tostring(RawEventData.ModifiedProperties))[1].NewValue)
| project Timestamp, DeviceCodeTime, FirstInterrupt, AccountUpn, SessionId,
DeviceName, ActionType, IPAddress, RawEventData
This is not a volume rule. One unexpected device registration after suspicious auth is enough to page someone.
Response Playbook
When the alert fires, do not only reset the password. The attacker may already hold refresh tokens.
- Revoke sign-in sessions for the user.
- Confirm risky sign-in state and user risk in Entra ID Protection.
- Review mailbox rules, forwarding, delegates, and transport rules.
- Search CloudAppEvents for Workday, payroll, finance, HR, and file access after the device-code event.
- Remove unapproved registered devices and authentication methods.
- Review OAuth grants and app consent.
- Convert the device-code allowlist finding into a Conditional Access policy change.
Conditional Access Strategy
Start in report-only mode. Block broad device code flow only after you know what it will break.
Suggested rollout
| Phase | Action | Outcome |
|---|---|---|
| 1 | Inventory device-code sign-ins for 30 days | Know real dependencies |
| 2 | Tag allowed apps/devices/users | Document exceptions |
| 3 | Create report-only CA policy blocking device code flow | Measure blast radius |
| 4 | Alert on non-allowlisted device-code auth | Catch abuse while tuning |
| 5 | Enforce for users and apps with no dependency | Reduce attack surface |
For high-risk users, prefer enforcement sooner. For shared devices and admin workflows, make the exception explicit and auditable.
Conditional Access authentication-flow policies also use protocol tracking. A session originally created through device-code flow can remain protocol-tracked through refreshes, so later access that does not visibly start as device code may still be affected by a device-code-flow policy. That matters when you troubleshoot blocks after enforcement.
One more scope nuance matters. The client-app allowlists in this post are detection and tuning lists, not Conditional Access enforcement objects. Conditional Access enforcement should be scoped through users, target resources and cloud apps, authentication-flow controls, locations, device state or filters, and report-only testing before enforcement.
What This Lab Proves
This lab demonstrates one reproducible, defensible detection chain in a controlled tenant.
- A native browser ceremony can emit the
50199 -> 0interrupt/success sign-in pattern inSigninLogs - A small set of legitimate clients that can be inventoried
- Post-token behavior in mailbox, device registration, or SaaS logs
- A Conditional Access path that can be tested without breaking the tenant blindly
It does not prove that every tenant will emit identical telemetry, that screenshots prove third-party reproducibility, or that one KQL rule can catch every device-code campaign. The reliable defense layers are inventory, Conditional Access, Defender XDR, Sentinel correlation, and post-auth behavior hunting.
Why This Is Worth Publishing Now
This is where identity security is heading. The password is less often the prize. The session is the prize.
Device code phishing gives attackers a clean way to make users approve that session on real Microsoft infrastructure. The organizations that win are the ones that already know where device code flow is allowed, where it is blocked, and what post-auth behavior means trouble.
The SOC question is simple.
Can you explain every device-code sign-in in your tenant?
If the answer is no, this is a good weekend lab.

Jerrad Dahlager, CISSP, CCSP
Cloud Security Architect ยท Adjunct Instructor
Marine Corps veteran and firm believer that the best security survives contact with reality.
Have thoughts on this post? I'd love to hear from you.



