Your user didn't type a password into a fake site. They didn't get an MFA push they didn't recognise. They went to a real Microsoft page, typed a real code, and clicked "Continue." Two days later their mailbox has forwarding rules to an external address and someone has been reading their SharePoint files. This is device code phishing, and it's spreading fast through Microsoft 365 tenants because it doesn't look like phishing at all.
Watch on YouTube · Subscribe at @EndpointWeekly
The problem: a real Microsoft page, a real code, no password stolen
A user reports something that sounds harmless: a Teams meeting invite, a "verify your device" prompt, or an IT support message telling them to go to microsoft.com/devicelogin and enter a short code like ABCD-8197. They do it. The page is genuinely hosted by Microsoft. There's no certificate warning, no wrong domain, no obvious red flag. A few hours later, one or more of these show up:
- New inbox rules that auto-forward or auto-delete mail (often named to blend in, or hidden entirely)
- Sign-ins from IP ranges belonging to cloud hosting providers, not the user's normal ISP
- Unusual Microsoft Graph activity - mass mailbox reads, bulk searches across SharePoint or OneDrive
- A sign-in in the Entra logs where Authentication Protocol reads
deviceCode
The user's password is untouched. MFA was satisfied. Nothing about the sign-in itself looks broken - because it isn't. This looks, at first glance, like a leaked password, a session cookie theft, or a Conditional Access gap. It's none of those. This is the defining trait of device code phishing: the attacker never steals a credential, they get the victim to hand over a token.
Why it happens: a legitimate OAuth flow with no built-in way to tell who's asking
Device code flow is a real, legitimate part of OAuth 2.0 (RFC 8628), built for devices that can't easily open a browser or accept keyboard input - think Teams Rooms panels, smart TVs, or a PowerShell/Azure CLI session on a headless server. The flow works like this:
- A client (legitimate app, or attacker's script) calls Entra ID's
/devicecodeendpoint with aclient_id. - Entra ID returns a
user_codeand averification_uri- that's themicrosoft.com/deviceloginpage and the short code. - The attacker packages that code and URL into a lure - a fake Teams invite, an "urgent collaboration" email, a fake IT verification request - and sends it to the victim.
- The victim opens
microsoft.com/deviceloginthemselves, enters the code, signs in normally, and approves the request. - Meanwhile the attacker's client has been silently polling Entra ID's
/tokenendpoint every few seconds. The moment the victim approves, that poll succeeds - and the attacker receives the access token and, often, a refresh token.
Nothing about steps 3-5 requires the attacker to know a password or intercept traffic. The victim completes real authentication on Microsoft's real domain. Conditional Access policies built around device compliance or "familiar browser" signals frequently don't fire, because from Entra ID's point of view this looks like an ordinary device sign-in - because it is one, just not the device you think.
How to verify: filter the sign-in logs on Authentication Protocol, not Client App
Start in the Entra ID sign-in logs - Kusto/Sentinel if you have it, the portal if you don't. The field you want is Authentication Protocol, not just "Client App," because device code sign-ins can otherwise blend into normal traffic.
Two fields matter most when you're triaging a hit: Original transfer method should say deviceCodeFlow, and the IP/location should not match where that user normally signs in. Here is a real shape of what that query returns - six rows, four routine and two worth chasing:
| UserPrincipalName | AppDisplayName | IPAddress | Location | SignInCount |
|---|---|---|---|---|
| j.mitchell@contoso.com | Microsoft Authentication Broker | 185.220.101.x | Netherlands | 3 |
| a.reyes@contoso.com | Azure CLI | 51.132.44.x | West US 2 | 14 |
| s.omar@contoso.com | Microsoft Authentication Broker | 45.153.160.x | Romania | 1 |
| t.nguyen@contoso.com | Teams Devices | 20.190.12.x | East US | 27 |
Illustrative example (organisation, usernames and IPs are fictional). Two of these four rows are the finding: the Netherlands and Romania sign-ins show a single device-code sign-in from unfamiliar European IP ranges, on an app neither user has a documented reason to use. The Teams Devices and Azure CLI rows are recurring, high-count, from expected regions - that's the baseline you'd expect to see and leave alone.
If you also see event code AADSTS50199 (CmsiInterrupt) immediately followed by a success entry for the same session in a short window, treat that sequence as a strong device-code-phishing signal - 50199 alone is just a user-confirmation interrupt, but 50199 followed by success, correlated to deviceCode, is the pattern attackers leave behind.
appId 29d9ed98-a469-4536-ade2-f981bc1d605e) shows up constantly in legitimate device-registration traffic - but Storm-2372 has been observed abusing that exact client ID. Don't filter it out entirely; review it with tighter scrutiny instead.No Sentinel? The same query works directly against Entra admin center › Monitoring › Sign-in logs - add "Authentication Protocol" as a column and filter for Device Code.
Investigating scope: which users and devices were affected
Once you suspect or confirm a device code phishing attempt, the first question is always: how far did it go? One user? Ten? Did the attacker only phish credentials or did they also poll tokens on multiple accounts? The answers are in Entra ID sign-in logs — if you know what to query.
Tenant-wide investigation with Graph API
The tenant-wide script queries Get-MgAuditLogSignIn filtered to authenticationProtocol eq 'deviceCode'. For each hit it looks up the device that completed the sign-in using the Azure AD Device ID from the log, and cross-references Intune to pull the managed device name, compliance state, and last sync time.
Run it with your global admin or security reader account — it will prompt you to authenticate and then export a CSV to your desktop.
# Query last 30 days — all device code sign-ins (attempts + successes) .\Get-DeviceCodePhishingReport.ps1 -LookbackDays 30 # Export only confirmed compromises (token was issued — ErrorCode = 0) .\Get-DeviceCodePhishingReport.ps1 -LookbackDays 30 -SuccessfulOnly # Save to a specific path instead of the desktop default .\Get-DeviceCodePhishingReport.ps1 -LookbackDays 30 -ExportPath C:\IR\DeviceCodeReport # CSV only — skip the HTML report .\\Get-DeviceCodePhishingReport.ps1 -LookbackDays 30 -NoHtml
The script handles module installation automatically, caches Intune device lookups to avoid repeated Graph calls for the same device ID, and prints a summary to the console when it finishes:
What the CSV report contains
Each row is one sign-in event. The key columns to look at first:
| Column | What to look for |
|---|---|
| SignInResult | Success = token was issued. The attacker has access. Prioritise these rows. |
| AttackerIP / Country | This is the IP that polled the /token endpoint — typically outside your country. It is the attacker’s IP, not the victim’s. |
| VictimDeviceName | The device that completed the sign-in (the user’s machine). Cross-reference with IntuneDeviceName to confirm it’s the same device in Intune. |
| AppGranted / AppId | Which app received the token. Microsoft Authentication Broker (29d9ed98-...) is commonly abused. Graph Explorer, Azure PowerShell, and Teams are also frequent targets. |
| IntuneCompliance | Tells you whether the device was compliant at time of sign-in. A compliant device being successfully phished means your Conditional Access policy wasn’t blocking device code flow. |
| RiskLevelDuringSignIn | If this is none on a successful device code sign-in, Entra ID Risk Policies did not flag it in real time. This is expected — device code flow typically bypasses standard impossible-travel signals. |
HTML investigation report
Alongside the CSV, the script generates a standalone HTML file you can open in any browser — no Excel, no SIEM, no setup required. It is designed for the first 30 minutes of an incident when you need to triage fast and share findings with a manager or the security team without asking them to filter a spreadsheet.
Every row is colour-coded by risk: red = unknown or suspicious app (investigate immediately), amber = service/room accounts (verify), green = known Microsoft admin tools. You can filter by risk level, country, and app, and search by username, IP, or device name.
.html file to your manager or CISO without needing to share a CSV or give access to Entra. It opens in any browser, no server or internet connection required./token — not the victim’s IP. On successful compromises, the attacker’s IP will often be in a different country. If Country is unexpected (RU, NG, CN, etc.) and SignInResult = Success, treat that user as fully compromised until you complete the IR steps.Investigating on the local device
After identifying affected users from the tenant report, run Invoke-DeviceCodeTriage.ps1 on the specific device to collect local artifacts: the Windows AAD Operational event log (Event IDs 1006/1007/1098), WAM Token Broker cache, dsregcmd /status output, and browser history timestamps. This gives you a timeline of when the device code exchange happened at the device level, and confirms whether the same device shows the code in its local auth log.
Invoke-DeviceCodeTriage.ps1 as the affected user (not as admin) — the WAM Token Broker cache is stored per-user and requires the user context to read. The script is read-only and makes no changes to the device.The fix: block the flow where nobody actually needs it
Context: device code flow is on by default in most tenants and almost nobody actively uses it outside a handful of kiosk, Teams Rooms, or CLI scenarios. You don't fix this by training users harder - the lure is legitimate-looking by design. You fix it by removing the flow where nobody actually needs it.
Prefer scripting this over clicking through the portal - it's faster to review, faster to re-run in report-only across multiple tenants, and it leaves you an artifact you can version:
Change state to enabled once your report-only review is clean.
If you'd rather click through the portal, or want to sanity-check what the script above produced, the steps are:
- Check who's using device code flow today before you block anything blind. Run the KQL query above against 90 days of history and get a list of accounts and apps.
- Go to Entra admin center › Protection › Conditional Access › Policies › New policy.
- Under Conditions, select Authentication flows, and set Configure to Yes. Choose Device code flow.
- Scope Users to All users, then add an exclusion group for the handful of accounts that legitimately need it (kiosk devices, Teams Rooms service accounts, break-glass admin scripts).
- Under Access controls › Grant, select Block access.
- Set the policy to Report-only first, run it for a week, review the Conditional Access report tab for anything that would break, then flip it to On.
If you've already found a suspicious sign-in, contain it immediately, in this order:
- Revoke the user's sessions: Entra admin center › Users › [user] › Revoke sessions. This kills the access and refresh tokens the attacker is holding.
- Reset the user's password and force MFA re-registration.
- Review and remove any inbox rules created around the incident window (Exchange admin center › mail flow, or
Get-InboxRule -Mailbox [user]). - Check for new OAuth app consents granted by that user around the same time - revoke anything unrecognised.
Local endpoint artifacts worth checking
The authoritative record for this attack is server-side - the Entra ID sign-in log. But if a user reports clicking a device-code lure, there are local artifacts on their machine that help you build a timeline and corroborate (or rule out) what happened, without waiting on log ingestion delays.
| Artifact | Where | What it tells you |
|---|---|---|
| AAD Operational event log | Microsoft-Windows-AAD/Operational, Event IDs 1006 / 1007 / 1098 | Token broker activity and failures on this device around the reported time |
| WAM Token Broker cache | %LOCALAPPDATA%\Microsoft\TokenBroker\Cache | Timestamps of token cache writes - do not decrypt, files are DPAPI-protected |
| AAD Broker Plugin package | %LOCALAPPDATA%\Packages\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy | Broker plugin cache folder + last-modified time |
| WAM registry key | HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\SystemAppData\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy\PSR | Broker plumbing health - also the key behind the common Event 1098 "token broker operation failed" fix |
| Services | Web Account Manager (TokenBroker), Microsoft Account Sign-in Assistant (wlidsvc) | Confirms the local WAM stack is running as expected |
dsregcmd /status | Command output | Device join state and AzureAdPrtUpdateTime - when this device's PRT last refreshed |
| Browser history | Edge/Chrome History SQLite file, Default profile | Confirms the user actually visited microsoft.com/devicelogin |
Troubleshooting script: local triage
Context: a user has reported clicking a device-code lure, or you've spotted a hit in the KQL query above and want the local timeline before the Entra logs finish correlating. Prerequisites: PowerShell 5.1+, run as the affected user (not elevated-only, or %LOCALAPPDATA% and HKCU won't resolve to their profile), Edge or Chrome closed if you want the browser-history check to succeed on the first pass.
To pull all of the local artifacts from the table above in one pass instead of chasing each one by hand, run this read-only triage script. It makes no changes to the system - it only reads and reports.
Real output from this series' test device (hostname, username, DeviceId and TenantId redacted). This device shows no devicelogin hits and no matching AAD Operational 1006/1007 pair - clean. The 1098 errors are a real, separate WS-Trust/ADFS discovery issue on this device (worth its own investigation) and not a device-code-phishing signal by themselves; that's exactly why the script tells you to correlate against the Entra sign-in log rather than trust local noise alone.
Get-WinEvent comes back empty for the AAD Operational log, it may simply not be enabled on that build. Turn it on with wevtutil sl Microsoft-Windows-AAD/Operational /e:true and have the user reproduce a normal sign-in to confirm it's now logging before you rely on it.Proof it worked: the flow dead-ends at the Conditional Access grant step
Confirm the block two ways. First, use Entra ID's built-in simulator before you fully trust the policy:
Second, check the sign-in logs after the policy goes live. A blocked attempt shows up as a failure tied to your Conditional Access policy, not a silent drop - the sign-in record exists, it's just denied at the grant stage:
| Date | User | Application | Status | Conditional Access |
|---|---|---|---|---|
| 8/9/2026, 3:41 PM | j.mitchell@contoso.com | Azure CLI | Failure | Failure |
| 8/9/2026, 11:02 AM | s.omar@contoso.com | Microsoft Authentication Broker | Failure | Failure |
Illustrative example (organisation and usernames are fictional).
That's proof the control is live: the flow that used to hand out tokens quietly now dead-ends at the Conditional Access grant step, and it shows up in your logs when someone - attacker or curious employee - tries it.
References
- Microsoft Learn - Conditional Access: Block authentication flows
- Microsoft Security Blog - Storm-2372 conducts device code phishing campaign
- Microsoft Security Blog - Inside an AI-enabled device code phishing campaign
- Microsoft Tech Community - Device Code Flow: The Gift That Keeps on Giving, to Attackers
- Microsoft Learn - Event 1098, Error 0xCAA5001C: Token broker operation failed
Both scripts for this post are in Daily-Tasks / device-code-phishing-microsoft-365-entra.