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.
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
| 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 | Grant-event review, non-corporate IPs, new high-priv apps, URI change history, possible token-relay correlation |
| 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.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:
- 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 > 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
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. Review OAuth Grant Events | Retained consent and grant events; not a current permission inventory | 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 Changes | Retained redirect URI change events; not a complete current registration inventory | 90 days |
| 5. Possible Token Relay After Error | OAuth error followed by successful auth from a different IP; does not prove a redirect or relay | 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 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:
- 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.
