Blocking Device-Code Phishing in Microsoft Entra ID: Usage Audit, Report-Only Conditional Access, Enforcement and Hunting for Stolen Sessions

Measure device code flow usage, block it with a Conditional Access authentication-flows policy, and hunt down tokens that device-code phishing kits already stole.
Device-code phishing has become one of the busiest ways to steal Microsoft 365 accounts this autumn. Kits like SkyW4lker send the victim to Microsoft's real sign-in page and ask them to type a short code. The victim completes MFA themselves, and the attacker's device receives the tokens. No password is stolen and no fake page appears, so the usual phishing advice doesn't help. The fix is to stop the flow everywhere it isn't needed. This tutorial shows how to measure device code usage in your tenant, roll out a Conditional Access block in report-only mode, enforce it with narrow exceptions, and hunt for sessions that were already stolen.
Prerequisites
- A Microsoft Entra ID tenant with Entra ID P1 or higher (Conditional Access needs it).
- The Conditional Access Administrator or Security Administrator role.
- Optional but recommended: sign-in logs sent to a Log Analytics workspace or Microsoft Sentinel, so you can query with KQL.
- PowerShell 7 with the Microsoft Graph SDK if you want to script the policy:
Install-Module Microsoft.Graph -Scope CurrentUser
Connect-MgGraph -Scopes "Policy.Read.All","Policy.ReadWrite.ConditionalAccess","Application.Read.All"
- At least one break-glass (emergency access) account that you will exclude from every Conditional Access policy.
Step 1: Find out who actually uses device code flow
Device code flow is meant for devices without a keyboard or browser, such as conference-room systems, digital signage and some CLI tools. In most tenants only a handful of accounts need it. Find out who they are before you block anything.
In the Entra admin center, go to Monitoring & health > Sign-in logs, select Add filters > Authentication Protocol and choose Device code. Check both the User sign-ins (interactive) and User sign-ins (non-interactive) tabs and look back as far as your retention allows.
If your logs go to Log Analytics, this query gives a quick inventory:
SigninLogs
| where TimeGenerated > ago(30d)
| where AuthenticationProtocol == "deviceCode"
| summarize Count = count(), Apps = make_set(AppDisplayName, 10),
IPs = make_set(IPAddress, 10), LastSeen = max(TimeGenerated)
by UserPrincipalName, ResultType
| order by Count desc
Write down every legitimate use you find, for example Teams Rooms accounts, a specific admin running Azure CLI with --use-device-code, or kiosk devices. These become your exclusions.
Step 2: Create the policy in report-only mode
Go to Entra ID > Conditional Access > Policies > New policy, then configure:
- Name:
CA-BLOCK-DeviceCodeFlow - Users: Include All users. Exclude your break-glass accounts and a group such as
CA-Exclude-DeviceCodeholding the accounts you found in Step 1. - Target resources: All resources. Under Exclude, choose Select excluded cloud apps and add Device Registration Service if your organisation registers devices with device code flow. Microsoft enforces authentication flows policies on that service when a policy targets all resources.
- Conditions > Authentication flows: set Configure to Yes and tick Device code flow. Tick Authentication transfer too unless you rely on users scanning a QR code in desktop Outlook to sign in on mobile.
- Grant: Block access.
- Enable policy: Report-only.
To script it instead, the Graph property is conditions.authenticationFlows.transferMethods:
$params = @{
displayName = "CA-BLOCK-DeviceCodeFlow"
state = "enabledForReportingButNotEnforced"
conditions = @{
users = @{
includeUsers = @("All")
excludeUsers = @("<break-glass-object-id>")
excludeGroups = @("<CA-Exclude-DeviceCode-group-id>")
}
applications = @{
includeApplications = @("All")
excludeApplications = @("01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9") # Device Registration Service
}
authenticationFlows = @{ transferMethods = "deviceCodeFlow,authenticationTransfer" }
}
grantControls = @{ operator = "OR"; builtInControls = @("block") }
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
Step 3: Review report-only results
Leave the policy in report-only mode for one to two weeks. Then open Conditional Access > Insights and reporting, pick the policy, and look at the Failure results, which are sign-ins the policy would have blocked. You can also open any sign-in and check the Report-only tab.
With Log Analytics:
SigninLogs
| where TimeGenerated > ago(14d)
| mv-expand CAP = ConditionalAccessPolicies
| where CAP.displayName == "CA-BLOCK-DeviceCodeFlow"
| where CAP.result == "reportOnlyFailure"
| summarize Hits = count() by UserPrincipalName, AppDisplayName
| order by Hits desc
Every hit should be either a legitimate account that belongs in your exclusion group or something suspicious that needs looking into (see Step 5).
Step 4: Enforce the block and tighten the exceptions
When the report-only data looks clean, switch Enable policy to On. Make the exclusions as narrow as you can:
- Use a separate, tightly governed group for device-code exceptions and review it every quarter.
- For shared devices such as Teams Rooms, create a second policy that allows device code flow only from your corporate network locations or device platform, rather than excluding those accounts from all controls.
- Never exclude administrators. If an admin needs device code for a CLI, give them a different login method. Azure CLI and Azure PowerShell both use an interactive browser login by default.
Step 5: Hunt for tokens that were already stolen
A block stops new device-code sign-ins, but it doesn't remove access an attacker already has. Look for device-code sign-ins that are new for the user, that come from unusual IPs or countries, or that are followed by mailbox access from unfamiliar infrastructure:
let lookback = 30d;
SigninLogs
| where TimeGenerated > ago(lookback)
| where AuthenticationProtocol == "deviceCode" and ResultType == 0
| project TimeGenerated, UserPrincipalName, IPAddress,
Country = tostring(LocationDetails.countryOrRegion),
AppDisplayName, UserAgent, CorrelationId
| join kind=leftanti (
SigninLogs
| where TimeGenerated between (ago(90d) .. ago(lookback))
| where AuthenticationProtocol == "deviceCode"
| distinct UserPrincipalName
) on UserPrincipalName
| order by TimeGenerated desc
This lists users whose first device-code sign-in happened in the last 30 days. Check each one with the user. If a sign-in was not theirs:
# Invalidate all refresh tokens and session cookies for the user
Revoke-MgUserSignInSession -UserId "user@contoso.com"
Then reset the password, check for new MFA methods and inbox rules, review OAuth consents, and look for devices registered in the account's name. With protocol tracking, Entra also marks sessions that began with device code flow, and later refreshes in those sessions are blocked once the policy is on.
Verification
- Test from a non-excluded account. On a test machine, run
az login --use-device-codeand enter the code at the sign-in page. The sign-in should fail with a Conditional Access block. - In Sign-in logs, open the failed attempt, go to the Conditional Access tab and confirm
CA-BLOCK-DeviceCodeFlowshows Failure. - Confirm that an excluded account can still sign in from an approved device.
Troubleshooting
- Users signed out of devices after enforcement. That's protocol tracking: refresh tokens that came from device code flow are rejected. Look for
AADSTS530036and the Original transfer method field under the sign-in's Basic info. - Device enrollment broke. You probably didn't exclude Device Registration Service (app ID
01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9). - Teams Rooms or signage stopped working. Add the resource accounts to the exception group or the location-scoped allow policy. Don't switch the main policy off.
- No results in KQL. Non-interactive sign-ins live in
AADNonInteractiveUserSignInLogs. Run the same queries against that table.
Wrapping Up
Device code phishing works because nothing on the page looks fake, so training users to spot bad pages won't stop it. Removing the flow does. Measure first, run report-only, enforce with a small, reviewed exception list, then hunt back through the last month for tokens that were already taken. In most tenants this takes under an hour of work and leaves current device-code phishing kits with nothing to use.