HomeNewsletterCommunityMVP FeedToolsArchiveBlogToday's NewsAboutServicesQuick Links Subscribe free
← Back to Blog
Security Entra IDPhishingConditional AccessOAuthSecurityIdentity

Device Code Phishing: How Attackers Steal Microsoft 365 Sessions Without a Password

IA
Imran Awan
9 August 2026

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.

Note: This isn't new - Microsoft first documented the Storm-2372 campaign abusing this technique in February 2025 - but activity picked up sharply through 2026, including an AI-automated version of the campaign Microsoft wrote up in April 2026.
Watch this post — YouTube walkthrough

Watch on YouTube · Subscribe at @EndpointWeekly

🎤 Podcast episode
Device Code Phishing — Block OAuth Token Theft in Microsoft 365

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:

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:

  1. A client (legitimate app, or attacker's script) calls Entra ID's /devicecode endpoint with a client_id.
  2. Entra ID returns a user_code and a verification_uri - that's the microsoft.com/devicelogin page and the short code.
  3. 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.
  4. The victim opens microsoft.com/devicelogin themselves, enters the code, signs in normally, and approves the request.
  5. Meanwhile the attacker's client has been silently polling Entra ID's /token endpoint 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.

Gotcha: A successful device code sign-in doesn't always mean the token stays valid indefinitely - but if a refresh token was issued, the attacker can keep minting new access tokens long after the original approval, without the user ever seeing another prompt.

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.

Microsoft Sentinel — Log Analytics (KQL)
// Successful sign-ins using device code flow, last 90 days SigninLogs | where TimeGenerated > ago(90d) | where AuthenticationProtocol == "deviceCode" | where ResultType == 0 // Microsoft Authentication Broker is noisy - review separately, don't blanket-exclude | summarize SignInCount = count(), LastSeen = max(TimeGenerated) by UserPrincipalName, AppDisplayName, AppId, IPAddress, Location | order by LastSeen desc

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:

UserPrincipalNameAppDisplayNameIPAddressLocationSignInCount
j.mitchell@contoso.comMicrosoft Authentication Broker185.220.101.xNetherlands3
a.reyes@contoso.comAzure CLI51.132.44.xWest US 214
s.omar@contoso.comMicrosoft Authentication Broker45.153.160.xRomania1
t.nguyen@contoso.comTeams Devices20.190.12.xEast US27

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.

Watch out: Microsoft Authentication Broker (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.

📋 Note: Entra ID retains interactive sign-in logs for 30 days on P1/P2 licences and 7 days on the free tier. If the attack was more than 30 days ago, you will need Microsoft Sentinel or a Log Analytics workspace to go further back.

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.

Get-DeviceCodePhishingReport.ps1
# 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:

Console output — example
==========================================
Device Code Phishing — Investigation Summary
==========================================
  Total events found:        127
  Successful sign-ins:       127
  Unique users:              46
  Unique devices:            26
  Intune-managed devices:     88
  Countries in sign-in IPs:   DE, US, IT, BR, JP, PH, PT, GB, IN, HK, CA, CO, NL, ES, AE, BE, FR, SA, CL
HIGH-RISK SIGN-INS - VERIFY OR REVOKE IMMEDIATELY:
  !! morgan.james@contoso.com
  !! suzuki.mark@contoso.com
  !! okafor.sarah@contoso.com
  !! harris.tyler@contoso.com
  !! patel.raj@contoso.com
  !! ali.adam@contoso.com
  !! chen.victor@contoso.com
  ... and 39 more users
ACTION REQUIRED for each user above:
  1. Revoke sessions: Entra admin center > Users > [user] > Revoke sessions
  2. Reset password and force MFA re-registration
  3. Review inbox rules: Get-InboxRule -Mailbox <UPN>
  4. Audit OAuth app consents granted around the incident window
Reports exported:
  CSV:  C:\Users\Admin\Desktop\DeviceCodePhishingReport_20260810_143022.csv
  HTML: C:\Users\Admin\Desktop\DeviceCodePhishingReport_20260810_143022.html
        (Open in any browser for the colour-coded investigation report)

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.

Device Code Phishing — Investigation Report
Scope: Whole tenant  ·  30-day lookback  ·  Generated 10 August 2026 13:40
All 127 events show Success — token was issued
127
Total Events
127
Successful
46
Unique Users
18
Countries
🔴
40
Investigate
Unknown or suspicious app
🟡
19
Verify
Service or room accounts
🟢
68
Likely Fine
Known Microsoft admin tools

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.

✅ Tip: The HTML report is self-contained — all data is embedded as a JavaScript array. Send the single .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.
⚠ Gotcha: The AttackerIP column is the IP that polled /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.

✅ Tip: Run 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:

PowerShell — Microsoft Graph
# Requires: Microsoft.Graph.Identity.SignIns module, Policy.ReadWrite.ConditionalAccess scope Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess" # Exclude the accounts that genuinely need device code flow - kiosk, Teams Rooms, break-glass $excludeGroupId = "<object-id-of-your-exclusion-group>" $params = @{ displayName = "Block device code flow" state = "enabledForReportingButNotEnforced" conditions = @{ users = @{ includeUsers = @("All"); excludeGroups = @($excludeGroupId) } applications = @{ includeApplications = @("All") } authenticationFlows = @{ transferMethods = "deviceCodeFlow" } } grantControls = @{ operator = "OR" builtInControls = @("block") } } New-MgIdentityConditionalAccessPolicy -BodyParameter $params

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:

  1. 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.
  2. Go to Entra admin center › Protection › Conditional Access › Policies › New policy.
  3. Under Conditions, select Authentication flows, and set Configure to Yes. Choose Device code flow.
  4. 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).
  5. Under Access controls › Grant, select Block access.
  6. 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.
entra.microsoft.comProtection › Conditional AccessBlock device code flowConditions › Authentication flows
Microsoft Entra Admin Center — Conditional Access › Authentication flows
Transfer methodsDevice code flow
Users excludedsvc-teamsrooms-kiosk@contoso.com, break-glass-admin@contoso.com
Enable policyReport-only
Tip: If you've never touched this setting, Microsoft has shipped a tenant-default policy blocking device code and PowerShell/CLI authentication flows since May 2025 for many tenants - check Conditional Access › Policies for one named along the lines of "Microsoft-managed: Block device code flow" before you assume you're unprotected.

If you've already found a suspicious sign-in, contain it immediately, in this order:

  1. Revoke the user's sessions: Entra admin center › Users › [user] › Revoke sessions. This kills the access and refresh tokens the attacker is holding.
  2. Reset the user's password and force MFA re-registration.
  3. Review and remove any inbox rules created around the incident window (Exchange admin center › mail flow, or Get-InboxRule -Mailbox [user]).
  4. 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.

ArtifactWhereWhat it tells you
AAD Operational event logMicrosoft-Windows-AAD/Operational, Event IDs 1006 / 1007 / 1098Token broker activity and failures on this device around the reported time
WAM Token Broker cache%LOCALAPPDATA%\Microsoft\TokenBroker\CacheTimestamps of token cache writes - do not decrypt, files are DPAPI-protected
AAD Broker Plugin package%LOCALAPPDATA%\Packages\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewyBroker plugin cache folder + last-modified time
WAM registry keyHKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\SystemAppData\Microsoft.AAD.BrokerPlugin_cw5n1h2txyewy\PSRBroker plumbing health - also the key behind the common Event 1098 "token broker operation failed" fix
ServicesWeb Account Manager (TokenBroker), Microsoft Account Sign-in Assistant (wlidsvc)Confirms the local WAM stack is running as expected
dsregcmd /statusCommand outputDevice join state and AzureAdPrtUpdateTime - when this device's PRT last refreshed
Browser historyEdge/Chrome History SQLite file, Default profileConfirms the user actually visited microsoft.com/devicelogin
Gotcha: None of this proves compromise on its own. The attacker's polling client and the resulting token live on their infrastructure, not the victim's PC - local artifacts here confirm the user completed the approval step and give you a timestamp, not a smoking gun. Always correlate against the Entra sign-in log.

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.

PowerShell — Invoke-DeviceCodeTriage.ps1 (real output, redacted)
PS C:\Users\jmitchell> .\Invoke-DeviceCodeTriage.ps1 -LookbackDays 7 Device Code Phishing - Local Endpoint Triage Host: DESKTOP-8K2N1Q User: jmitchell Run: 2026-08-09 11:39:25 Lookback window: 7 day(s) ====================================================================== 1. dsregcmd /status - device join and PRT state ====================================================================== AzureAdJoined : YES EnterpriseJoined : NO DomainJoined : YES DeviceId : aaaaaaaa-0b0b-1c1c-2d2d-333333333333 TenantId : aaaaaaaa-0b0b-1c1c-2d2d-333333333333 WorkplaceJoined : NO AzureAdPrt : YES AzureAdPrtUpdateTime : 2026-08-07 14:11:13.000 UTC ====================================================================== 2. Microsoft-Windows-AAD/Operational event log ====================================================================== Found 124 event(s). Showing Event IDs of interest: [8/9/2026 11:26:33 AM] EventID 1098 - Error: 0xCAA100D8 A login hint was sent that doesn't match any WebAccount in the system. [8/9/2026 11:24:21 AM] EventID 1098 - Error: 0xCAA90022 Could not discover endpoint for Integrate Windows Authentication. Check your ADFS settings. [8/9/2026 11:24:21 AM] EventID 1098 - Error: 0xCAA9002B WS-Trust metadata exchange request failed. ... 26 more 1098 entries in this window, all the same three error codes ====================================================================== 3. WAM Token Broker cache (timestamps only) ====================================================================== Path: C:\Users\jmitchell\AppData\Local\Microsoft\TokenBroker\Cache 8/9/2026 11:30:26 AM 21,674 bytes 9f3a2b71-....tbres 8/9/2026 11:30:24 AM 7,674 bytes 4c8e5d02-....tbres ... 13 more cache writes in this window ====================================================================== 5. Identity-related services ====================================================================== Web Account Manager Running StartType=Manual Microsoft Account Sign-in Assistant Running StartType=Manual ====================================================================== 6. Browser history - microsoft.com/devicelogin visits ====================================================================== Edge: no match for 'devicelogin' in history file. Chrome: no match for 'devicelogin' in history file. Report written to: C:\Users\jmitchell\Desktop\DeviceCodeTriage_20260809_113925.txt

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.

Watch out: If this needs to hold up as forensic evidence - HR case, legal action, law enforcement referral - stop and use a proper forensic imaging/chain-of-custody process instead of a live triage script. This tool is for fast operational IR, not evidentiary collection.
Tip: If 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:

entra.microsoft.comProtection › Conditional AccessWhat If
Microsoft Entra Admin Center — Conditional Access › What If
Authentication flowsDevice code flow
Evaluation result1 policy(s) will apply · Grant: Block access

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:

DateUserApplicationStatusConditional Access
8/9/2026, 3:41 PMj.mitchell@contoso.comAzure CLIFailureFailure
8/9/2026, 11:02 AMs.omar@contoso.comMicrosoft Authentication BrokerFailureFailure

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

PowerShell Scripts — Device Code Phishing Investigation

Both scripts for this post are in Daily-Tasks / device-code-phishing-microsoft-365-entra.

Get-DeviceCodePhishingReport.ps1
— tenant-wide: queries Entra ID sign-in logs via Graph API, correlates with Intune, exports a CSV and a colour-coded HTML investigation report (risk classification, search, filter, sortable table)
Invoke-DeviceCodeTriage.ps1
— local device: collects dsregcmd, AAD event log, WAM cache, and browser-history artifacts for on-device triage
View all scripts on GitHub
Was this post helpful?
React below — no account needed
Share this post
LinkedIn X / Twitter Reddit Bluesky

More from EndpointWeekly

Security
That AI Tool You Just Gave 'Read/Write All' to Entra and Intune…
A newly adopted AI ITSM platform just asked for Directory.ReadWrite.All. Most tenants…
Security
Microsoft Edge Now Lets Users Sign In With Google — Here's What…
Edge now shows a Google sign-in option for browser profiles. On managed endpoints, that…
Security
Windows LAPS vs Legacy LAPS: The Migration Drift Where Two…
You migrated from legacy Microsoft LAPS to Windows LAPS and the portal looks clean. But…