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.
Validation boundary (August 13, 2026): the pinned source 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
| Resource | Type | Details |
|---|---|---|
| 4 Analytics Rules | Sentinel Scheduled | OAuth consent after risky sign-in, suspicious redirect URI, OAuth error patterns, bulk consent |
| 1 Workbook | Azure Workbook | OAuth Security Dashboard (consent timeline, error patterns, URI changes, top apps) |
| Optional consent policy change | Entra ID | Tenant authorization-policy update; applied only with -ApplyHardening |
| Optional CA Policy | Entra ID | Newly created report-only step-up policy; same-named policies are never adopted or updated |
| 5 Hunting Queries | KQL files | Delegated permissions audit, non-corporate IPs, new high-priv apps, URI inventory, token replay |
| 1 Audit Script | PowerShell | Enumerate 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.3+ (
pwsh) - 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 17cda5d320dd4fe1922c310be4e553d086d9109b
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:
- Verify the Sentinel workspace exists and Sentinel is enabled
- Deploy 4 scheduled analytics rules via the Sentinel REST API
- Deploy the OAuth Security Dashboard workbook
- Skip tenant hardening unless
-ApplyHardeningis present - 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 > 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
Rule 1: OAuth Consent After Risky Sign-in (High)
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.
Rule 4: Bulk OAuth Consent to Single App (High)
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:
| Hunt | Purpose | Lookback |
|---|---|---|
| 1. Enumerate Delegated Permissions | Baseline audit of all user-granted permissions | 90 days |
| 2. Non-Corporate IP Sign-ins | OAuth app auth from unexpected locations | 30 days |
| 3. New High-Privilege Apps | Recently registered apps with sensitive scopes | 14 days |
| 4. Redirect URI Inventory | Full audit trail of redirect URI changes | 90 days |
| 5. Token Replay After Error | Error redirect followed by successful auth from different IP | 7 days |
Hunt 2 requires customization โ replace the CorporateNetworks variable with your organization’s IP ranges.
Hardening Policies
User Consent Restriction
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
-ExcludedUserIdsare 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 audit checks every app registration for:
- Suspicious redirect URI domains (ngrok, herokuapp, workers.dev, etc.)
- Non-HTTPS redirect URIs (excluding localhost)
- High-privilege delegated permissions (Mail.Read, Files.ReadWrite.All, etc.)
- User-consented vs admin-consented permissions
- Multi-tenant app registrations
Output is a CSV sorted by risk score.
File Structure
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:
- Registering a test app with a redirect URI containing
webhook.site(triggers Rule 2) - Checking that
AuditLogscontains “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.
