Device Code Phishing
Device code phishing abuses Microsoft's OAuth 2.0 Device Authorization Grant flow to capture access tokens, refresh tokens, and ID tokens from recipients without ever asking for a password. The recipient authenticates directly on Microsoft's own login page, which means MFA is completed on the attacker's behalf and the login looks entirely legitimate.
Phishing Club handles the full lifecycle: requesting codes from Microsoft, embedding them in emails or landing pages, polling for successful authentication in the background, capturing the tokens, and recording everything in the campaign event log. The workflow is similar to cookie capture in proxy campaigns.
Overview
From the recipient's perspective the flow looks like this:
- The recipient receives a message containing a short code (e.g.
ABCD-1234) and a URL (e.g.https://microsoft.com/devicelogin). - The recipient visits the URL, enters the code, and signs in with their Microsoft account including completing any MFA prompt.
- Microsoft issues OAuth tokens to the application that originally requested the device code.
- Phishing Club polls Microsoft's token endpoint in the background and captures the tokens as soon as the recipient completes authentication.
The captured access token, refresh token, and ID token are stored as a campaign event. They can be copied and imported into Session Sushi to hijack the session, the same workflow as cookie capture.
How It Works
Phishing Club handles the device code lifecycle automatically.
- When a campaign message is sent or a landing page is rendered, Phishing Club calls
Microsoft's
/oauth2/v2.0/devicecodeendpoint to request a device code for the campaign and recipient. - Microsoft responds with a short user code (e.g.
ABCD-1234), a verification URI (https://microsoft.com/devicelogin), and an expiry time, typically 15 minutes. - The code and URL are embedded into the email body or landing page HTML using the
{{MicrosoftDeviceCode}}and{{MicrosoftDeviceCodeURL}}template functions. - A background task polls Microsoft's
/oauth2/v2.0/tokenendpoint for every active, non-expired device code. When the recipient authenticates, Microsoft returns tokens and Phishing Club captures them. - Captured tokens are saved as a Data Submitted campaign event marked with a 🔑 key emoji. The campaign recipient's notable event is updated accordingly.
- When the campaign is closed, all device code records are deleted from the database. The token data is already preserved in the campaign event log.
One Code Per Recipient
Each device code is scoped to a campaign and recipient pair. Phishing Club enforces a one-active-code-per-recipient policy so the same recipient always sees the same code regardless of how many times their email is opened or their landing page is loaded.
The behaviour differs depending on where the template function is evaluated.
In Emails
When a campaign email is sent, Phishing Club requests a device code from Microsoft and embeds it into the email body at send time. The code is generated once per recipient and baked into the message.
If the code expires before the recipient acts on it there is nothing Phishing Club can do for that email. The code shown in the message is no longer valid. Keep campaign send windows short relative to the Microsoft device code expiry, which is typically 15 minutes.
In Landing Pages
Landing pages are rendered on demand every time the recipient loads the page. The device code is resolved at request time rather than at send time.
Phishing Club applies a 5 minute threshold. If an existing device code for that campaign and recipient expires within the next 5 minutes, or has already expired, it is discarded and a fresh code is requested from Microsoft before the page is rendered. This ensures the recipient always sees a valid, usable code when they visit the landing page even if they took a while to click through.
Every time a new code is created, whether at email send time or on a landing page refresh, an Info event is logged with the user code and verification URI so you can track which code was shown to which recipient.
Behaviour After Capture
By default, once a device code has been captured it is never replaced
on subsequent page loads. If the recipient reloads the landing page after authenticating, Phishing
Club returns the already-captured entry as-is rather than deleting it and requesting a new code
from Microsoft. This is the capturedOnce behaviour and it is enabled by default.
This is important for landing pages that auto-redirect after capture (see
Auto-Redirect After Capture below). Without it, each page reload
would generate a new code and
{{DeviceCodeCaptured}}
would never become
true.
To opt out and always generate a fresh code even after capture, pass
"capturedOnce" "false" to the template function:
{{MicrosoftDeviceCode "capturedOnce" "false"}}Template Functions
Three template functions are available for use in email bodies, landing pages, and
attachments. All accept optional keyword arguments as alternating key and
value string pairs.
| Function | Returns | Example output |
|---|---|---|
{{MicrosoftDeviceCode}} | The short user code to display to the recipient | ABCD-1234 |
{{MicrosoftDeviceCodeURL}} | The verification URI the recipient must visit | https://microsoft.com/devicelogin |
{{DeviceCodeCaptured}} | true if the device code for this recipient has already been captured,
false otherwise | false |
Optional Parameters
All three functions accept the same optional keyword arguments to customise the device code request sent to Microsoft. When omitted, the defaults target Microsoft Graph.
| Key | Default | Description |
|---|---|---|
clientId | d3590ed6-52b3-4102-aeff-aad2292ab01c | The Azure AD application client ID used to request the device code. The default is the well-known Microsoft Office client ID. |
tenantId | organizations | The Azure AD tenant identifier. Accepts a tenant UUID, a verified domain name such as
contoso.com, or one of the well-known values common,
organizations, or consumers. Use a specific tenant to target a
particular organisation. |
scope | https://graph.microsoft.com/.default openid profile offline_access | Space-separated list of OAuth scopes to request. Include offline_access to receive
a refresh token. |
resource | https://graph.microsoft.com | The target resource URI. |
capturedOnce | true | When true, a captured entry is returned as-is on subsequent page loads
instead of being replaced with a new code. Set to false to always generate
a fresh code even after capture. See
Behaviour After Capture above. |
Usage Examples
Basic usage with default settings:
<p>To verify your identity, visit <a href="{{MicrosoftDeviceCodeURL}}">{{MicrosoftDeviceCodeURL}}</a>
and enter the code below:</p>
<h2 style="letter-spacing: 4px; font-family: monospace;">{{MicrosoftDeviceCode}}</h2> Targeting a specific tenant:
{{MicrosoftDeviceCode "tenantId" "contoso.com"}}
{{MicrosoftDeviceCodeURL "tenantId" "contoso.com"}} Custom client ID and tenant:
{{MicrosoftDeviceCode "clientId" "your-app-client-id-here" "tenantId" "contoso.com"}}
{{MicrosoftDeviceCodeURL "clientId" "your-app-client-id-here" "tenantId" "contoso.com"}} Both functions within the same template share the same underlying device code entry. Calling
{{MicrosoftDeviceCode}} and
{{MicrosoftDeviceCodeURL}} with the same arguments returns the user code and verification
URI from the same object. No separate codes are generated.
The template editor provides quick-insert buttons for all three functions under the
Templates menu and renders placeholder values (ABCD-1234,
https://microsoft.com/devicelogin, and
false) in the live preview so you can design your page without triggering real
Microsoft API calls.
Auto-Redirect After Capture
Landing pages can automatically redirect the recipient to another page once their tokens have
been captured. The recommended approach uses {{DeviceCodeCaptured}} combined
with a
<meta> refresh so the page periodically reloads itself until capture is confirmed
server-side, then redirects.
{{if DeviceCodeCaptured}}
<script>window.location.href = "{{.URL}}";</script>
{{else}}
<p>
Visit <strong>{{MicrosoftDeviceCodeURL}}</strong>
and enter the code: <strong>{{MicrosoftDeviceCode}}</strong>
</p>
<meta http-equiv="refresh" content="5">
{{end}} How this works:
- The recipient loads the page.
{{.DeviceCodeCaptured}}isfalseso the code and URL are shown. The<meta refresh>reloads the page every 5 seconds. - The background task polls Microsoft and captures the tokens. On the next page reload,
{{.DeviceCodeCaptured}}istrue. - The inline script fires immediately and redirects the recipient to
{{.URL}}.
Because capturedOnce defaults to true, each page reload returns the
same device code entry rather than requesting a new one from Microsoft. The recipient sees the
same code on every reload and the page never enters a broken state where a freshly generated
code replaces the one that was just captured.
You can adjust the reload interval by changing the content
value on the
<meta> tag. 5 seconds is a reasonable default — the background polling task
runs on every task runner tick so captures are detected quickly.
Token Capture
Once a recipient completes authentication, Phishing Club captures the OAuth tokens and records them as a Data Submitted event in the campaign event log. The event is marked with a 🔑 key emoji to distinguish it from regular form submissions and cookie captures.
The captured event data contains the following fields:
| Field | Description |
|---|---|
access_token | Short-lived bearer token for calling Microsoft APIs such as Microsoft Graph |
refresh_token | Long-lived token that can be exchanged for new access tokens. This is the most valuable artifact as it survives password resets and can be used to maintain persistent access. |
id_token | JWT containing the recipient's identity claims such as name, email, and tenant |
user_code | The short code that was shown to the recipient, for reference |
client_id | The client ID used to obtain the tokens |
If the campaign has Save Submitted Data disabled, the event is still recorded so the capture is counted, but the token values are replaced with an empty object.
Copying and Importing Tokens
Clicking a 🔑 event in the campaign view copies the token data to your clipboard and opens the Tokens captured modal. From there you can copy the tokens and import them into Session Sushi to hijack the captured session, the same workflow as cookie capture.
Info Events
Every time a device code is created for a recipient, whether at email send time or because the landing page refreshed an expiring code, an Info event is logged on the campaign timeline. The event data includes:
- user_code — the short code shown to the recipient
- verification_uri — the URL the recipient was directed to
This lets you track which code was active for each recipient and correlate it with the eventual token capture event.
Note that with capturedOnce enabled (the default), a captured entry is returned as-is
on page reloads rather than replaced. No new Info event is emitted for those reloads since no new
code is created.
Webhooks
Token captures fire a campaign_recipient_submitted_data webhook event, the same event
type used for regular form submissions. The webhook payload includes the captured tokens so you
can send them to external tooling or alerting pipelines in real time.
The webhook body contains:
{
"access_token": "eyJ...",
"refresh_token": "0.A...",
"id_token": "eyJ...",
"user_code": "ABCD-1234",
"client_id": "d3590ed6-52b3-4102-aeff-aad2292ab01c"
} See the Webhooks section for details on configuring webhook endpoints and verifying webhook signatures.
Limits and Scaling
The number of device codes that can be active and polled at the same time has not been tested at scale. Microsoft's device code endpoint and token polling endpoint may apply rate limits, and large campaigns with many simultaneous active codes could run into these limits.
If you are running a campaign with a large number of recipients you should be aware of this. Keep an eye on the campaign event log for any errors during code creation or polling. This is something that will be investigated and documented further.
Tips
- Combine with landing pages for best results. Landing pages auto-refresh expiring codes within 5 minutes of expiry so the recipient always has a valid code even if they take their time. Email-only campaigns risk the code expiring before the recipient acts.
- The first
capturedOncesetting controls the flow. When{{MicrosoftDeviceCode}}is used, the first triggered occurance in a campaign decides whether the code is captured once or multiple times. SubsequentcapturedOncewill be ignored. If nocapturedOnceis specified, the default is to capture once and not refresh device code after the first capture.
This can lead to confusion when mixing if{{MicrosoftDeviceCode}}is used in multiple campaigns with differentcapturedOncesettings, to avoid this, specifycapturedOnceexplicitly. - Target specific tenants when possible. Using
"tenantId" "contoso.com"restricts the device code to that organisation's users, which reduces noise and makes the login prompt look more targeted to the recipient.