On this page

On March 2, 2026, Microsoft published an advisory on OAuth redirection abuse enabling phishing and malware delivery. Microsoft described phishing-led campaigns where attackers register OAuth apps with attacker-controlled redirect URIs, then send legitimate-looking Microsoft login links that intentionally drive the browser into an authorization error path and bounce victims to attacker infrastructure.

This isn’t credential theft or classic token theft. The user still touches real Microsoft infrastructure, but the attacker wins when Entra ID redirects the browser to the app’s registered URI, which points to a phishing page, malware dropper, or relay endpoint.

This post walks through building detection and hardening for this technique using Microsoft Sentinel and Entra ID Conditional Access.

Hands-on Lab: All KQL queries, PowerShell scripts, and deployment automation are in the companion lab.


How the Attack Works

The OAuth redirect abuse pattern exploits how Entra ID handles authentication errors and consent flows. As documented in RFC 9700 Section 4.11.2 (“Authorization Server as Open Redirector”), attackers can deliberately trigger OAuth errors to force redirects through the authorization server.

OAuth redirect abuse attack flow diagram showing 5 steps: attacker registers a malicious app, sends a crafted OAuth lure, victim authenticates at Microsoft login, Entra returns an OAuth error and redirects to the attacker's URI, and the attacker-controlled landing page takes over
OAuth redirect abuse attack flow โ€” the victim authenticates against legitimate Microsoft infrastructure but lands on an attacker-controlled page after the error redirect.
  1. App Registration โ€” Attacker registers an OAuth app and sets the redirect URI to an attacker-controlled domain (powerappsportals.com, github.io, surge.sh, and gitlab.io were cited by Microsoft)
  2. Phishing Link โ€” Victim receives a link that initiates an OAuth authorization flow with parameters designed to fail at the authorization step, such as prompt=none combined with an invalid or unapproved request
  3. Authorization Error โ€” Entra ID reaches an authorization error state such as interaction_required or access_denied
  4. Error Redirect โ€” Per the OAuth 2.0 spec, Entra ID redirects the victim’s browser to the app’s registered redirect_uri with error parameters appended
  5. Malicious Landing โ€” The victim lands on the attacker’s page, which auto-downloads a ZIP containing LNK files and HTML smuggling loaders, or redirects to an AiTM phishing framework like EvilProxy
  6. Data Exfiltration โ€” The state parameter is repurposed to carry the victim’s email address (encoded via Base64, hex, or custom schemes), so it auto-populates on the phishing page

The key insight: the redirect itself is the win. Microsoft noted the sign-in can fail and still hand the attacker a phishing or malware-delivery opportunity because the browser lands on a malicious page after touching legitimate Microsoft infrastructure.

Why This Works

  • The URL starts with login.microsoftonline.com โ€” it looks legitimate to users and URL filters
  • In the observed Entra flow, prompt=none suppresses the normal consent UI and drives the request down the error path
  • Even security-aware users who would decline consent still get redirected because the error itself triggers the redirect
  • The redirect URI can point to any domain registered in the app โ€” github.io, netlify.app, or free hosting services
  • Microsoft’s advisory confirmed multiple threat actors targeting government and public-sector organizations

Which Error Outcomes Matter Most?

Microsoft’s write-up and the OAuth authorization-code flow docs give us two high-confidence Entra hunt signals:

  • AADSTS65001 / interaction_required โ€” common when silent auth cannot complete because the app or requested permissions do not already have the required consent
  • AADSTS65004 / access_denied โ€” common when a user explicitly declines consent
Error codeCommon meaning in this patternHunt value
AADSTS65001Silent auth fails because prior consent is missing or interaction is requiredHigh-confidence redirect-abuse signal
AADSTS65004User explicitly declines consentHigh-confidence consent-phishing signal
AADSTS70011Invalid scope or malformed OAuth requestSupporting context only
AADSTS700016App not found in tenantSupporting context only
AADSTS70000Invalid grant or broken authorization flowSupporting context only
AADSTS7000218Missing client assertion / client auth issueSupporting context only

Other OAuth failures such as 70011, 700016, 70000, and 7000218 can still show up while attackers probe or misconfigure the flow, but Microsoft does not document one universal redirect behavior for those numeric codes across every endpoint and flow. Treat them as supporting context, not proof that a browser redirect occurred.

Detection implication: Seeing a burst of 65001 or 65004 errors against a single unfamiliar app is the strongest Entra-native signal. Broader OAuth error clusters are still worth triaging, but they need app registration and consent context.


Detection Strategy

We need detection at two layers:

  1. Proactive โ€” Find risky OAuth app registrations before they’re weaponized
  2. Reactive โ€” Detect active abuse patterns in sign-in and audit logs

MITRE ATT&CK Mapping

TechniqueIDDetection
Spearphishing LinkT1566.002Rules 1, 3, 4
Account ManipulationT1098Rule 2
User Execution: Malicious LinkT1204.001Rule 3

Sentinel Analytics Rules

Four scheduled analytics rules detect the core abuse patterns. Each runs hourly against the last 24 hours of data.

Microsoft Defender portal showing the Analytics page with 4 LAB OAuth redirect abuse detection rules filtered, severity chart showing 5 High and 2 Medium rules, and detail panel showing the OAuth Consent After Risky Sign-in rule configuration
Four OAuth redirect abuse detection rules deployed in Microsoft Sentinel via the Defender portal. The detail panel shows rule severity, MITRE ATT&CK mapping, and status.

Correlates SigninLogs risk indicators with AuditLogs consent events. If a user’s sign-in session shows phishing risk (unfamiliar features, anonymized IP, malicious IP, suspicious IP, malware-infected IP, or suspicious browser) and they grant OAuth consent within 15 minutes, something is wrong.

let PhishingWindow = 15m;
let RiskySignIns = SigninLogs
    | where RiskLevelDuringSignIn in ("high", "medium")
        or RiskEventTypes_V2 has_any (
            "unfamiliarFeatures", "anonymizedIPAddress",
            "maliciousIPAddress", "suspiciousIPAddress",
            "malwareInfectedIPAddress", "suspiciousBrowser")
    | project SignInTime = TimeGenerated,
        UserPrincipalName, IPAddress,
        RiskLevelDuringSignIn, RiskEventTypes_V2;
AuditLogs
| where OperationName == "Consent to application"
| extend ConsentUser = tostring(InitiatedBy.user.userPrincipalName)
| extend AppDisplayName = tostring(TargetResources[0].displayName)
| join kind=inner (RiskySignIns)
    on $left.ConsentUser == $right.UserPrincipalName
| where TimeGenerated between (SignInTime .. (SignInTime + PhishingWindow))
| project TimeGenerated, UserPrincipalName = ConsentUser,
    AppDisplayName, RiskLevel = RiskLevelDuringSignIn, SourceIP = IPAddress

Why this matters: Legitimate consent grants don’t happen during risky sessions. If Identity Protection flags the sign-in and the user grants consent, you’re likely looking at a consent phishing attack.

Rule 2: Suspicious OAuth Redirect URI Registered

Watches for app registrations or updates that add redirect URIs pointing to free hosting, tunneling services, URL shorteners, or non-HTTPS endpoints.

let SuspiciousDomains = dynamic([
    // Tunneling services
    "ngrok.io", "ngrok-free.app", "trycloudflare.com",
    "serveo.net", "localtunnel.me",
    // Free hosting / PaaS
    "workers.dev", "pages.dev", "herokuapp.com",
    "netlify.app", "vercel.app", "github.io",
    "gitlab.io", "surge.sh", "glitch.me", "replit.dev",
    "powerappsportals.com",
    // Webhook / request capture
    "webhook.site", "requestbin.com", "pipedream.com",
    // URL shorteners
    "bit.ly", "tinyurl.com", "t.co", "rebrand.ly"]);
AuditLogs
| where OperationName in ("Add application", "Update application")
| mv-expand ModifiedProperty = TargetResources[0].modifiedProperties
| where ModifiedProperty.displayName == "AppAddress"
| extend NewRedirectUris = tostring(ModifiedProperty.newValue)
| extend InitiatedBy_ = coalesce(
    tostring(InitiatedBy.user.userPrincipalName),
    tostring(InitiatedBy.app.displayName))
| extend AppName = tostring(TargetResources[0].displayName)
| where NewRedirectUris has_any (SuspiciousDomains)
    or NewRedirectUris has "http://"
| project TimeGenerated, AppName, NewRedirectUris, InitiatedBy_

Tuning tip: Add your organization’s legitimate development domains to an exclusion list. Developers using ngrok for local testing will generate false positives โ€” but you should know about those too.

Rule 3: OAuth Error-Based Redirect Pattern

Detects sign-in attempts that result in the Entra errors most closely associated with redirect abuse. The strongest signals are AADSTS65001 and AADSTS65004. The rule also carries a short list of secondary OAuth failures that often appear when attackers or broken apps probe the same flow.

SigninLogs
| where ResultType in (
    "65001",   // User hasn't consented (prompt=none attack vector)
    "65004",   // User declined consent
    "70011",   // Invalid scope or other OAuth parameter issue
    "700016",  // App not found in tenant
    "70000",   // Invalid grant
    "7000218", // Missing client assertion
    "AADSTS65001", "AADSTS65004",
    "AADSTS70011", "AADSTS700016")
| where AppDisplayName !in (
    "Microsoft Office", "Azure Portal",
    "Microsoft Teams", "Outlook Mobile")
| summarize ErrorCount = count(),
    DistinctUsers = dcount(UserPrincipalName),
    Users = make_set(UserPrincipalName, 10),
    ErrorCodes = make_set(ResultType),
    IPs = make_set(IPAddress, 10)
    by AppDisplayName, AppId, bin(TimeGenerated, 1h)
| where ErrorCount > 3 or DistinctUsers > 2
| project TimeGenerated, AppDisplayName, AppId,
    ErrorCount, DistinctUsers, Users, ErrorCodes, IPs

Why we include non-redirect error codes: 65001 and 65004 are the high-confidence redirect-abuse signals. The additional OAuth failures in the rule are secondary context that can strengthen a case when they cluster around the same app and time window.

When 3+ users consent to the same app within an hour, it strongly indicates a phishing campaign pushing users to authorize a malicious application.

AuditLogs
| where OperationName == "Consent to application"
| extend ConsentUser = tostring(InitiatedBy.user.userPrincipalName)
| extend AppName = tostring(TargetResources[0].displayName)
| extend AppId = tostring(TargetResources[0].id)
| summarize ConsentCount = count(),
    ConsentUsers = make_set(ConsentUser, 20),
    FirstConsent = min(TimeGenerated),
    LastConsent = max(TimeGenerated)
    by AppName, AppId, bin(TimeGenerated, 1h)
| where ConsentCount >= 3
| project TimeGenerated, AppName, AppId,
    ConsentCount, ConsentUsers

Hunting Queries

Beyond automated detection, five hunting queries support proactive threat hunting:

  1. Enumerate All OAuth Apps with Delegated Permissions โ€” Baseline audit of every app with user-granted permissions over the last 90 days
  2. OAuth Sign-ins from Non-Corporate IPs โ€” Find OAuth app authentications from unexpected locations (customize the corporate IP ranges)
  3. Recently Registered Apps with High-Privilege Permissions โ€” Apps created in the last 14 days requesting Mail.Read, Files.ReadWrite.All, Directory.ReadWrite.All, etc.
  4. OAuth Redirect URI Inventory โ€” Full audit trail of redirect URI changes across all app registrations
  5. Token Replay After OAuth Redirect Error โ€” Detect the pattern where an OAuth 65001 error redirect is followed by a successful token acquisition from a different IP within 30 minutes โ€” the signature of a token relay attack

The full KQL for all five hunting queries is in the companion lab. Import them into Sentinel Hunting > Queries to run proactive hunts against your OAuth telemetry.


OAuth Security Workbook

The lab deploys an Azure Workbook that provides a single-pane view of OAuth activity across four panels:

Azure Workbook showing the OAuth Security Dashboard with four panels: Consent Grants Over Time timechart, OAuth Error Patterns by Application table, Recent Redirect URI Changes table, and Top 10 Apps by Consent Count bar chart
OAuth Security Dashboard in a sandbox workspace with minimal test data. Production tenants with active OAuth traffic will show richer consent timelines, error clustering, and redirect URI change history across all four panels.
  • Consent Grants Over Time โ€” Timechart of OAuth consent events by application, showing spikes that indicate bulk consent campaigns
  • OAuth Error Patterns by Application โ€” Table of primary redirect-abuse indicators (65001, 65004) plus related OAuth failures grouped by app and error code
  • Recent Redirect URI Changes โ€” Audit trail of redirect URI modifications across all app registrations
  • Top 10 Apps by Consent Count โ€” Bar chart highlighting apps with the most user consents, surfacing outliers

The workbook uses the same KQL patterns as the analytics rules, giving SOC analysts a dashboard to investigate alerts in context. The time range parameter defaults to 7 days but can be adjusted for broader investigations.


Entra ID Hardening

Detection alone isn’t enough. The lab includes hardening scripts that reduce the attack surface:

The most impactful control: restrict which apps users can consent to.

$authPolicy = az rest --method GET `
    --url 'https://graph.microsoft.com/v1.0/policies/authorizationPolicy' `
    | ConvertFrom-Json

$currentPolicies = @($authPolicy.defaultUserRolePermissions.permissionGrantPoliciesAssigned)
$updatedPolicies = @(
    $currentPolicies | Where-Object { $_ -like 'managePermissionGrantsForOwnedResource.*' }
)
$updatedPolicies += 'managePermissionGrantsForSelf.microsoft-user-default-low'
$updatedPolicies = $updatedPolicies | Select-Object -Unique

$body = @{
    defaultUserRolePermissions = @{
        permissionGrantPoliciesAssigned = $updatedPolicies
    }
} | ConvertTo-Json -Depth 5

az rest --method PATCH `
    --url 'https://graph.microsoft.com/v1.0/policies/authorizationPolicy' `
    --body $body --headers 'Content-Type=application/json'

This policy blocks most user-driven consent phishing and materially reduces risky third-party app approvals. It does not revoke existing grants, and redirect-only lures can still succeed if the attacker only needs the browser bounce.

Conditional Access Policy

A CA policy adds step-up authentication to risky OAuth-related sign-ins:

  • Applies to: All users
  • Conditions: Sign-in risk = High or Medium
  • Grant controls: Require MFA
  • Session controls: Sign-in frequency = Every time
Entra admin center showing the LAB - Require MFA for Risky OAuth Sign-ins Conditional Access policy in report-only mode, with All users assigned, sign-in risk conditions configured, MFA grant control, and sign-in frequency set to Every time
The lab CA policy deploys in report-only mode. Review CA insights for 7 days, then enforce after excluding emergency accounts.

The policy deploys in report-only mode. Run it for 7 days, review the CA insights workbook for impact, exclude emergency accounts before enforcement, then switch it on.

OAuth App Audit

The Audit-OAuthApps.ps1 script enumerates all app registrations and service principals via Microsoft Graph to flag:

  • Apps with redirect URIs pointing to ngrok.io, herokuapp.com, workers.dev, etc.
  • Apps with non-HTTPS redirect URIs (excluding localhost)
  • Apps with high-privilege delegated permissions (Mail.Read, Files.ReadWrite.All, Directory.ReadWrite.All)
  • User-consented permissions (vs admin-consented)
  • Multi-tenant apps registered in your tenant

The audit outputs a CSV with risk scores, sorted by severity. Run it weekly.


Deployment

The entire lab deploys to an existing Microsoft Sentinel workspace:

git clone https://github.com/j-dahl7/oauth-redirect-abuse-sentinel.git
cd oauth-redirect-abuse-sentinel
# Deploy everything
./scripts/Deploy-Lab.ps1 -ResourceGroup "rg-sentinel-lab" -WorkspaceName "law-sentinel-lab"

# Detection only (skip tenant hardening)
./scripts/Deploy-Lab.ps1 -ResourceGroup "rg-sentinel-lab" -WorkspaceName "law-sentinel-lab" -SkipHardening

The script deploys:

  1. 4 Sentinel analytics rules (scheduled, hourly)
  2. 1 Sentinel workbook (OAuth Security Dashboard)
  3. OAuth hardening policies (consent restriction, CA policy)
  4. OAuth app audit report (CSV)

See the full lab documentation for prerequisites, testing steps, and cleanup.


Key Takeaways

  1. OAuth redirect abuse bypasses simple URL filtering โ€” The link starts on login.microsoftonline.com, which looks legitimate to users and many controls.
  2. AADSTS65001 is a primary hunting signal โ€” In Microsoft’s Entra example, the sign-in can fail and still redirect the browser to the attacker’s landing page.
  3. Not every OAuth error means a redirect โ€” Treat 65001 and 65004 as the strongest browser-side signals, and use the rest as supporting context.
  4. Restrict user consent now โ€” The low-risk verified-publisher policy meaningfully reduces consent phishing, but you still need to review existing grants.
  5. Deploy CA policies for risky sessions โ€” Step up risky sign-ins before the user reaches the malicious app flow.
  6. Hunt, don’t just detect โ€” The token replay hunting query (Hunt 5) catches attacks that no single-event rule will find

Resources

Jerrad Dahlager

Jerrad Dahlager, CISSP, CCSP

Cloud Security Architect ยท Adjunct Instructor

Marine Corps veteran and firm believer that the best security survives contact with reality.

Have thoughts on this post? I'd love to hear from you.