Skip to main content

Review update (September 26, 2026): The merged source preserves an already-disabled user-consent policy and refuses unrecognized custom self-consent policy replacement for manual review. Reports, manifest staging and request-body files are owner-only at creation. The four rule excerpts use immutable identity/freshness and exact-host redirect checks, including current ngrok and loca.lt domains. Fifty-four offline tests and semantic analysis of all 13 rule, hunt and workbook queries passed; no new tenant mutation or live query is claimed.

Source maintenance reviewed September 26, 2026: this updated source passed its offline and CI checks. Historical tenant evidence in the companion article retains its original dates; no fresh tenant deployment or detection result is asserted here.

A hands-on lab deploying detection and hardening for OAuth redirect abuse โ€” the technique Microsoft warned about in their March 2026 advisory.

Cost: Uses an existing Sentinel workspace; ingestion, retention, and licensing charges still apply. Optional hardening changes shared tenant policy. Cleanup: Use the deterministic-ID and ownership-marker destroy path for lab-owned Sentinel objects. If hardening was applied, use the owner-only local manifest to restore the exact captured consent-policy collection and delete only the exact CA policy ID that the script created.

Blog Post: For detailed explanations of the attack technique and detection logic, see Detecting OAuth Redirect Abuse with Microsoft Sentinel and Entra ID.

GitHub: j-dahl7/oauth-redirect-abuse-sentinel

Validation boundary (August 13, 2026): the source reviewed then passed 20 offline contract tests, including mocked apply, rerun, foreign-policy, partial-failure, consent-race, drift, rollback, and KQL fixture checks. No live tenant mutation, Sentinel query, or incident was validated for this revision.


What Gets Deployed

ResourceTypeDetails
4 Analytics RulesSentinel ScheduledOAuth consent after risky sign-in, suspicious redirect URI, OAuth error patterns, bulk consent
1 WorkbookAzure WorkbookOAuth Security Dashboard (consent timeline, error patterns, URI changes, top apps)
Optional consent policy changeEntra IDTenant authorization-policy update; applied only with -ApplyHardening
Optional CA PolicyEntra IDNewly created report-only step-up policy; same-named policies are never adopted or updated
5 Hunting QueriesKQL filesGrant-event review, non-corporate IPs, new high-priv apps, URI change history, possible token-relay correlation
1 Audit ScriptPowerShellEnumerate all OAuth apps for suspicious redirect URIs and overprivileged permissions

Prerequisites

  • Azure subscription with an existing Microsoft Sentinel workspace
  • Azure CLI configured (az login)
  • PowerShell 7.6+ (pwsh) for stable native-command error handling
  • Microsoft Sentinel Contributor or equivalent rule/workbook write access on the workspace
  • Directory.Read.All delegated Graph permission and a supported Entra role such as Directory Readers for the OAuth audit (skip with -SkipAudit)
  • For -ApplyHardening, Privileged Role Administrator plus Policy.ReadWrite.Authorization for the tenant consent-policy update
  • For its report-only Conditional Access policy, Conditional Access Administrator (or Security Administrator) plus Policy.Read.All and Policy.ReadWrite.ConditionalAccess
  • At least one reviewed emergency-access user object GUID supplied through -ExcludedUserIds, plus the exact active tenant GUID supplied through -ConfirmTenantId; actual hardening fails closed without either

Quick Start

1. Clone the Repository

git clone https://github.com/j-dahl7/oauth-redirect-abuse-sentinel.git
cd oauth-redirect-abuse-sentinel
git checkout 21fbb382560dedabcca24c4d6327d02fa32b817e

2. Deploy

./scripts/Deploy-Lab.ps1 -ResourceGroup "rg-sentinel-lab" -WorkspaceName "law-sentinel-lab"

Optional tenant hardening:

./scripts/Deploy-Lab.ps1 `
  -ResourceGroup "rg-sentinel-lab" `
  -WorkspaceName "law-sentinel-lab" `
  -ApplyHardening `
  -ConfirmTenantId "<verified-tenant-guid>" `
  -ExcludedUserIds @("<break-glass-user-object-guid>")

Preview the complete deployment without cloud writes, temporary request-body files, or a local audit CSV:

./scripts/Deploy-Lab.ps1 `
  -ResourceGroup "rg-sentinel-lab" `
  -WorkspaceName "law-sentinel-lab" `
  -ApplyHardening `
  -ConfirmTenantId "<verified-tenant-guid>" `
  -ExcludedUserIds @("<break-glass-user-object-guid>") `
  -WhatIf

The script will:

  1. Verify the Sentinel workspace exists and Sentinel is enabled
  2. Deploy 4 scheduled analytics rules via the Sentinel REST API
  3. Deploy the OAuth Security Dashboard workbook
  4. Skip tenant hardening unless -ApplyHardening is present
  5. Run the OAuth app audit and save a CSV report

During -WhatIf, the summary labels resources as planned, skips the audit file, ownership manifest, and temporary request bodies, and applies no local or tenant changes.

3. Verify Deployment

Open Microsoft Defender portal > Microsoft Sentinel > Configuration > Analytics:

  • You should see 4 new rules prefixed with “LAB -”
  • All rules should show as Enabled with Scheduled type

Open Workbooks:

  • Find “OAuth Security Dashboard” in the list

Analytics Rules

Correlates SigninLogs risk indicators with AuditLogs consent events within a 15-minute window.

MITRE: T1566.002 (Spearphishing Link)

Rule 2: Suspicious OAuth Redirect URI Registered (Medium)

Watches for app registrations adding redirect URIs to tunneling services, free hosting, URL shorteners, or non-HTTPS endpoints. It normalizes normal AppAddress objects and legacy bare-string records, compares oldValue with newValue, and evaluates newly added addresses only. It exempts the exact localhost and 127.0.0.1 HTTP loopback hosts that Microsoft supports for local application development; other HTTP hosts remain suspicious.

MITRE: T1098 (Account Manipulation)

Rule 3: OAuth Error Cluster by Application (Medium)

Groups repeated consent, scope, app-registration, grant, and client-authentication failures by application. These errors can appear during redirect-abuse investigations, but they can also reflect ordinary consent state or broken application configuration. Treat the result as a triage lead and correlate it with redirect-URI changes, consent events, application ownership, and sign-in risk. The error cluster alone does not prove a redirect.

The rule suppresses only immutable AppId values that you explicitly add to ApprovedAppIds; display names are attacker-controlled and are never trusted as an allowlist.

MITRE: None assigned. SigninLogs error codes alone do not establish phishing-link delivery or user execution.

Fires when 3+ distinct, nonempty Entra user object IDs consent to the same app within 1 hour. Repeated events from one account remain in the event count but do not satisfy the distinct-user threshold.

MITRE: T1566.002 (Spearphishing Link)


Hunting Queries

Import the queries from detection/hunting-queries.kql into Sentinel Hunting:

HuntPurposeLookback
1. Review OAuth Grant EventsRetained consent and grant events; not a current permission inventory90 days
2. Non-Corporate IP Sign-insOAuth app auth from unexpected locations30 days
3. New High-Privilege AppsRecently registered apps with sensitive scopes14 days
4. Redirect URI ChangesRetained redirect URI change events; not a complete current registration inventory90 days
5. Possible Token Relay After ErrorOAuth error followed by successful auth from a different IP; does not prove a redirect or relay7 days

Hunt 2 requires customization โ€” replace the CorporateNetworks variable with your organization’s IP ranges.


Hardening Policies

The Set-OAuthHardening.ps1 script restricts user consent to:

  • Low-risk permissions only (e.g., User.Read, openid, profile)
  • Apps from verified publishers and trusted tenant-owned workflows
  • Everything else requires admin approval
  • Existing managePermissionGrantsForOwnedResource.* entries are preserved when the policy is updated

Before its first Graph mutation, the script persists an owner-only manifest with the exact tenant, original and intended consent-policy collections, intended CA content hash, and reviewed exclusions. Immediately after Graph creates the CA policy, the script atomically records its server-assigned immutable ID before changing the authorization policy. It verifies the server-created policy by ID and hash, then re-reads consent immediately before and after its guarded update. The manifest and temporary Graph request bodies are locked to the current owner before sensitive content is written.

Conditional Access Policy

Creates a new report-only lab CA policy that applies when:

  • Sign-in risk is Medium or High
  • Grant controls require MFA
  • Session sign-in frequency is set to Every time
  • Emergency-access users supplied through -ExcludedUserIds are excluded

Supply break-glass exclusions before creating the policy, review report-only results for at least 7 days, and validate impact before considering enforcement. The script never adopts or updates a policy by display name. Same-name foreign policies, tenant mismatches, changed exclusions, an uncertain prior create, and consent or CA content drift all stop the operation before another write. Declining an interactive confirmation aborts without reporting false success.

OAuth App Audit

Run the audit independently:

./hardening/Audit-OAuthApps.ps1 -OutputPath "./oauth-audit-report.csv"

The PowerShell 7.6+ audit includes local registrations and tenant service principals, including external enterprise apps and managed identities without a local registration. It pages through delegated grants and each principal’s outbound app-role assignments to check:

  • Suspicious redirect URI domains (ngrok, herokuapp, workers.dev, etc.)
  • HTTP redirects except exact parsed loopback hosts localhost, 127.0.0.1, and [::1]
  • High-privilege delegated permissions (Mail.Read, Files.ReadWrite.All, etc.)
  • User-consented vs admin-consented permissions
  • Multi-tenant audience declarations
  • Granted application permissions, resolved against the resource principal’s app roles; custom/default roles and unresolved definitions remain review candidates

Output is a CSV sorted by the count of heuristic flags, with separate delegated/app-role permission columns and resource/object identity context. Formula-like exported text is prefixed with an apostrophe. The audit uses only GET requests under the existing Directory.Read.All read permission and an endpoint-supported Entra role; it needs no additional Graph write permission.

Incomplete or malformed required pages stop before export. A failed run leaves any prior CSV unchanged; do not interpret that older file as new results. Empty findings do not establish safe effective access. Protect the report as tenant security inventory and read the pinned README’s replication, throttling, and coverage limits before use.


File Structure

The main operational files are shown below. Offline contracts and KQL fixtures are under tests/ in the pinned repository.

oauth-redirect-abuse-sentinel/
โ”œโ”€โ”€ README.md                             # Lab documentation
โ”œโ”€โ”€ detection/
โ”‚   โ”œโ”€โ”€ analytics-rules.kql            # 4 Sentinel analytics rules (full KQL)
โ”‚   โ””โ”€โ”€ hunting-queries.kql            # 5 proactive hunting queries
โ”œโ”€โ”€ hardening/
โ”‚   โ”œโ”€โ”€ Set-OAuthHardening.ps1         # Consent restriction + CA policy
โ”‚   โ””โ”€โ”€ Audit-OAuthApps.ps1            # OAuth app security audit
โ””โ”€โ”€ scripts/
    โ””โ”€โ”€ Deploy-Lab.ps1                 # Main deployment orchestrator

Cleanup

Remove Sentinel Resources

Preview cleanup of the four deterministic, ownership-marked rules and workbook:

./scripts/Deploy-Lab.ps1 `
  -ResourceGroup "rg-sentinel-lab" `
  -WorkspaceName "law-sentinel-lab" `
  -Destroy `
  -WhatIf

Then run the same ownership checks and delete only the exact owned objects:

./scripts/Deploy-Lab.ps1 `
  -ResourceGroup "rg-sentinel-lab" `
  -WorkspaceName "law-sentinel-lab" `
  -Destroy

Cleanup refuses same-title foreign objects and mismatched IDs or ownership markers. Do not bulk-delete every rule prefixed LAB -.

Remove Hardening (if applied)

Preview the manifest-backed, drift-aware rollback:

./hardening/Set-OAuthHardening.ps1 `
  -ConfirmTenantId "<verified-tenant-guid>" `
  -Rollback `
  -WhatIf

Then omit -WhatIf. The script restores only the exact captured consent collection and deletes only the manifest-owned CA ID after verifying its full managed content hash. It re-reads consent immediately before restoration and re-reads/re-hashes the exact CA object immediately before deletion. It refuses drift and never deletes by name; a completed rollback can be rerun safely with no additional cloud mutation. The Sentinel destroy path intentionally does not guess or revert tenant-wide Graph state, and it does not delete the local oauth-audit-report.csv.


Troubleshooting

Rules Don’t Fire

Analytics rules need matching data in SigninLogs and AuditLogs. If you don’t have OAuth consent events or risky sign-ins in your tenant, the rules will be silent. Test by:

  1. Registering a test app with a redirect URI containing webhook.site (triggers Rule 2)
  2. Checking that AuditLogs contains “Add application” events

Workbook Shows No Data

Ensure the workspace has AuditLogs and SigninLogs data connectors enabled. Check:

AuditLogs | take 1
SigninLogs | take 1

Hardening Script Fails

The hardening script changes two policy surfaces. Updating the tenant authorization policy requires Privileged Role Administrator and Policy.ReadWrite.Authorization. Creating its report-only Conditional Access policy requires Conditional Access Administrator (or Security Administrator) plus Policy.Read.All and Policy.ReadWrite.ConditionalAccess. Run with -WhatIf to preview changes:

./hardening/Set-OAuthHardening.ps1 `
  -ConfirmTenantId "<verified-tenant-guid>" `
  -ExcludedUserIds @("<break-glass-user-object-guid>") `
  -WhatIf

This is a true preview: it performs the read-only discovery needed to build the plan, but does not write Graph changes or temporary request-body files.


Resources