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:

  1. The recipient receives a message containing a short code (e.g. ABCD-1234) and a URL (e.g. https://microsoft.com/devicelogin).
  2. The recipient visits the URL, enters the code, and signs in with their Microsoft account including completing any MFA prompt.
  3. Microsoft issues OAuth tokens to the application that originally requested the device code.
  4. 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.

  1. When a campaign message is sent or a landing page is rendered, Phishing Club calls Microsoft's /oauth2/v2.0/devicecode endpoint to request a device code for the campaign and recipient.
  2. 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.
  3. The code and URL are embedded into the email body or landing page HTML using the {{MicrosoftDeviceCode}} and {{MicrosoftDeviceCodeURL}} template functions.
  4. A background task polls Microsoft's /oauth2/v2.0/token endpoint for every active, non-expired device code. When the recipient authenticates, Microsoft returns tokens and Phishing Club captures them.
  5. Captured tokens are saved as a Data Submitted campaign event marked with a 🔑 key emoji. The campaign recipient's notable event is updated accordingly.
  6. 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.

Device Code Template Functions
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.

Optional Keyword Arguments
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:

  1. The recipient loads the page. {{.DeviceCodeCaptured}} is false so the code and URL are shown. The <meta refresh> reloads the page every 5 seconds.
  2. The background task polls Microsoft and captures the tokens. On the next page reload, {{.DeviceCodeCaptured}} is true.
  3. 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:

Captured Token Event Data
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 capturedOnce setting controls the flow. When {{MicrosoftDeviceCode}} is used, the first triggered occurance in a campaign decides whether the code is captured once or multiple times. Subsequent capturedOnce will be ignored. If no capturedOnce is 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 different capturedOnce settings, to avoid this, specify capturedOnce explicitly.
  • 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.