OAuth Permissions, Admin Consent, and Security Attacks
This section builds on the service principal identity model from Section 1 and focuses on what those identities are allowed to do once they authenticate. The goal is to give you a working understanding of OAuth permissions, the admin consent model, common attack techniques that exploit the permission system, and the governance tools available to harden your tenant against them.
Section 2 learning progression - permissions → consent → attacks → governance
Every permission grant in Microsoft Entra is either delegated or application.
| Dimension | Delegated Permission | Application Permission |
|---|---|---|
| User context | App acts on behalf of a signed-in user | App acts as itself - no user context |
| Scope boundary | User's own authorization is the ceiling - the app cannot do more than the user can | Covers the entire tenant scope of the data, not just one user's data |
| Graph object | OAuth2PermissionGrant | appRoleAssignment |
| Token claim | scp (scope) | roles |
| Consent model | User can sometimes consent independently, depending on tenant policy | Always requires admin consent - no user can self-consent |
| Blast radius | Bounded by the specific user's access | Tenant-wide - every mailbox, every file, every user |
Think of it as: permission scope × tenant population = blast radius. An app with Mail.ReadWrite.All application permission can silently read and modify every mailbox in the tenant. An app with Mail.Read delegated permission from a specific user can only access that user's mail. Application permissions always multiply across the entire tenant.
Delegated Vs App
Delegated permission flow - app inherits the signed-in user's permission ceiling
Application permission - no user context, no ceiling on blast radius
The authorization code flow for delegated access follows this path:
Admin consent changes the flow so that one privileged action approves the permission for the entire organization. This removes the per-user consent prompt and grants tenant-wide access.
Static consent declares all required permissions in the app registration upfront. Dynamic consent allows delegated permissions to be requested progressively at runtime - the app can ask for Mail.Read only when the user actually navigates to the email feature. Application permissions cannot use dynamic consent; they are always declared statically and require admin consent.
When an app requests https://graph.microsoft.com/.default, it gets all statically-configured permissions - this is the standard pattern in client_credentials (app-only) flows. In delegated flows, .default requests everything the app is registered for. Understanding .default is essential because most app-only token requests use this shortcut.
OAuth consent flow - authorization redirect → consent prompt → auth code → token exchange
Consent is the control surface - permission scope × tenant population = blast radius
Static vs dynamic consent - and the special case for application permissions
Consent is also the primary attack vector for OAuth-based phishing. In an illicit consent grant attack:
Illicit consent grant attacks bypass MFA and persist across password changes. Revoking the access requires explicitly finding and removing the OAuth permission grant, not resetting the password.
Illicit consent grant - the consent prompt is real; the app is malicious; the token survives password changes
Illicit consent grant - five-step inline example with remediation steps
Tenant controls that defend against this:
Tenant controls against illicit consent grant - layered funnel; combine for defense-in-depth
Consent grant visibility and revocation - grant removal does not revoke existing tokens
Application permissions are not limited to Microsoft Graph. Any OAuth2-enabled API registered in Entra can be listed in an app's permission request. The most dangerous example is the Azure Service Management API:
https://management.azure.com/ with user_impersonation delegated scopeAn application with Azure Service Management user_impersonation consent from a Global Administrator can manage the organization's entire Azure environment on behalf of that admin. This permission is rarely audited because most tooling focuses on Graph.
Other non-Graph API scopes frequently seen in tenant permission reviews:
Non-Graph API permissions - Azure Service Management is the most dangerous blind spot
The Application Administrator role is the primary delegated control point for managing applications in Entra. It is frequently over-assigned and poorly understood.
What Application Administrators can do:
What they cannot do without a more privileged role:
The privilege escalation path:
An Application Administrator who adds a new credential to a service principal that already holds high-privilege Graph permissions (granted by a Global Administrator) can then authenticate as that service principal and exercise those permissions.
Application Administrator can impersonate any existing service principal in the tenant by adding credentials to it. If that service principal holds RoleManagement.ReadWrite.Directory, Application Administrator becomes an indirect path to Global Administrator.
Application Administrator should be treated as a highly privileged role, assigned only through PIM with just-in-time activation, and its credential modification and consent actions should be monitored via audit logs.
What Application Administrator can and cannot consent to - and the privilege escalation gap
Application Administrator privilege escalation path - add credentials to SP, then authenticate as that SP
The device code flow is designed for devices that cannot open a browser. An attacker exploits it as follows:
microsoft.com/devicelogin.Microsoft Threat Intelligence has tracked active campaigns using this technique (Storm-2372, active since 2024).
Device code flow phishing is currently one of the most active cloud attack vectors. If Conditional Access policies do not explicitly block device code flow, every user is a potential target with no password or MFA required.
Defense: block device code flow via Conditional Access authentication flow restrictions for all users except approved narrow scenarios such as conference room devices on a specific named network.
Storm-2372 device code phishing - victim satisfies MFA, attacker holds the tokens
Device code flow phishing - five-step sequence diagram, attacker and victim swim lanes
Conditional Access policy blocking device code flow - assignments, conditions, grant, and report-only start
Access tokens stored in browser local storage, environment variables, pipeline logs, or memory can be extracted and replayed from any machine. MFA is not re-prompted because the token is already authenticated.
Defense:
Token theft - tokens leak from browser storage, CI/CD logs, env vars; defense is Token Protection + CAE
In delegated scenarios, refresh tokens can have sliding window lifetimes of 24 hours to 90 days. A stolen refresh token can generate new access tokens for persistent access long after the original session ended.
Defense:
Refresh token lifecycle - a stolen refresh token can maintain persistent access for up to 90 days
A compromised service principal with Mail.ReadWrite.All can silently read and modify every mailbox in the tenant. A service principal with Files.ReadWrite.All can access every SharePoint file. These are common in enterprise tenants because proof-of-concept apps accumulated high-privilege grants that were never cleaned up.
Discovering stale over-permissioned service principals:
appRoleAssignments and cross-reference with service principal sign-in logsOver-permissioned service principal lateral movement - one stale SP gives tenant-wide blast radius
Attack Chain
The following scenario connects every risk covered in this section into a single coherent attack.
The setup: An app registration called "HR Sync Tool" was created 18 months ago for a proof of concept. A Global Administrator granted it Mail.ReadWrite.All application permission. The developer who created it has left. The app has an active client secret with six months remaining. No owner is assigned.
Step 1 - Credential discovery: The attacker searches GitHub and finds a config file committed by the former developer containing the client secret.
Step 2 - Silent authentication: The attacker authenticates as the service principal using the stolen secret. No MFA fires. No Conditional Access policy applies. The sign-in looks normal.
Step 3 - Silent exfiltration: With Mail.ReadWrite.All, the attacker silently reads email from the CEO, CFO, and Legal team for three weeks. No user is prompted. The access token is continuously re-acquired via client credentials.
Step 4 - Privilege escalation: A second attacker compromises an Application Administrator account via device code phishing. The Application Administrator adds a new credential to an existing service principal that holds RoleManagement.ReadWrite.Directory. The attacker now has effective Global Administrator-equivalent access.
What detection would have looked like:
Each detection fires after the damage starts. The earlier controls - secret scanning, workload identity CA, least-privilege consent, credential governance policies - are preventive. Detection buys you response time, not immunity. Build both layers.
Attack chain detection - each step maps to a specific detection tool, all available as templates
Attack chain debrief - where do your controls apply, and where is the earliest detection point?
Use the Entra admin center (Enterprise Applications > Permissions) to review delegated and application permissions granted to each enterprise app.
Use Microsoft Defender for Cloud Apps App Governance for a unified dashboard showing:
Use PowerShell to enumerate permissions at scale:
# Install once on machines that do not already have the required Graph SDK submodules.
# If an older Microsoft.Graph.Authentication version is already loaded in this session,
# start a new pwsh session after installation before you run Connect-MgGraph.
Install-Module Microsoft.Graph.Identity.SignIns,Microsoft.Graph.Applications `
-Scope CurrentUser `
-Force `
-AllowClobber
Import-Module Microsoft.Graph.Identity.SignIns
Import-Module Microsoft.Graph.Applications
Connect-MgGraph -Scopes "Application.Read.All","DelegatedPermissionGrant.ReadWrite.All"
# Enumerate application permission grants for the disposable demo app created in Step 0.
Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $demoSpObjectId
# Enumerate delegated permission grants for the same demo app.
Get-MgOauth2PermissionGrant -Filter "clientId eq '$demoSpObjectId'"
Four tools for permission discovery - App Governance is the best for cross-app tenant-wide visibility
Permissions granted via admin consent can be removed without reinstalling the app.
# Remove a specific delegated permission grant.
# Use the disposable demo grant created in Step 0 or another grant ID you already enumerated.
Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $demoDelegatedGrant.id
Removing a permission does not revoke existing valid tokens. Token revocation is a separate action.
Revoking permissions after consent - two-step revocation: remove grant + revoke sessions
Microsoft Entra Entitlement Management can manage access packages that include app role assignments. Instead of permanent admin consent grants, you create an access package that:
This is useful when developers or operators need temporary elevated API access without leaving permanent high-privilege grants in the tenant.
Both Entitlement Management and Lifecycle Workflows require Entra ID Governance or Entra Suite licensing. Without this license, the only path is manual permission grant management and manual offboarding cleanup.
Lifecycle Workflows automate identity tasks triggered by employee lifecycle events:
Custom task extensions let you call out to Logic Apps or Function Apps when built-in tasks are not sufficient. For example, a Leaver workflow could trigger a Logic App that disables a service principal, removes its credentials, or queues credential rotation work for apps owned by the departing employee. There is no service-principal sign-in-session revoke endpoint in Graph.
This connects the identity governance story back to automation: the managed identities and Graph API calls covered in earlier sections are what power custom Lifecycle Workflow extensions.
Entitlement Management and Lifecycle Workflows - JML triggers + approval-gated time-limited access packages
Have Graph Explorer access if permitted, Microsoft Graph PowerShell modules, and the disposable demo object IDs ready. If tenant policy blocks Graph Explorer consent, continue with the provided demo grants. Clean up delegated grants, app role assignments, and disposable apps after the lab.
Use Graph Explorer to experience delegated versus application-only access, review App Governance for overprivileged applications, revoke a delegated permission grant, and verify a Conditional Access policy blocking device code flow.
Use one setup path and keep the variables it creates in the same terminal session. Each example creates a disposable app, a delegated grant, and an application grant so the later steps have predictable objects to inspect and revoke.
Native PowerShell:
$tenantId = "<tenant-id>" az login --tenant $tenantId $accessToken = az account get-access-token ` --resource-type ms-graph ` --tenant $tenantId ` --query accessToken ` -o tsv $demoName = "Session2-PermissionsDemo-$(Get-Date -Format 'yyyyMMddHHmmss')" $demoApp = az ad app create --display-name $demoName --sign-in-audience AzureADMyOrg | ConvertFrom-Json $demoSp = az ad sp create --id $demoApp.appId | ConvertFrom-Json $demoAppObjectId = $demoApp.id $demoSpObjectId = $demoSp.id $signedInUserId = az ad signed-in-user show --query id -o tsv $graphSp = Invoke-RestMethod ` -Method Get ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '00000003-0000-0000-c000-000000000000'" ` -Headers @{ Authorization = "Bearer $accessToken" } $graphSpId = $graphSp.value[0].id $demoDelegatedGrant = Invoke-RestMethod ` -Method Post ` -Uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" ` -Headers @{ Authorization = "Bearer $accessToken"; "Content-Type" = "application/json" } ` -Body (@{ clientId = $demoSpObjectId consentType = "Principal" principalId = $signedInUserId resourceId = $graphSpId scope = "openid profile offline_access User.Read" } | ConvertTo-Json) $demoAppRoleAssignment = Invoke-RestMethod ` -Method Post ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$demoSpObjectId/appRoleAssignments" ` -Headers @{ Authorization = "Bearer $accessToken"; "Content-Type" = "application/json" } ` -Body (@{ principalId = $demoSpObjectId resourceId = $graphSpId appRoleId = "498476ce-e0fe-48b0-b801-37ba7e2685c6" } | ConvertTo-Json)
Graph PowerShell:
# Run once on machines that do not already have the required Graph PowerShell submodules. Install-Module Microsoft.Graph.Identity.SignIns,Microsoft.Graph.Applications ` -Scope CurrentUser ` -Force ` -AllowClobber Import-Module Microsoft.Graph.Identity.SignIns Import-Module Microsoft.Graph.Applications Connect-MgGraph -Scopes "Application.ReadWrite.All","Application.Read.All","DelegatedPermissionGrant.ReadWrite.All","AppRoleAssignment.ReadWrite.All","User.Read" $demoName = "Session2-PermissionsDemo-$(Get-Date -Format 'yyyyMMddHHmmss')" $demoApp = New-MgApplication -DisplayName $demoName -SignInAudience "AzureADMyOrg" $demoSp = New-MgServicePrincipal -AppId $demoApp.AppId $demoAppObjectId = $demoApp.Id $demoSpObjectId = $demoSp.Id $signedInUser = Get-MgUser -UserId (Get-MgContext).Account $graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'" | Select-Object -First 1 $demoDelegatedGrant = New-MgOauth2PermissionGrant -BodyParameter @{ clientId = $demoSpObjectId consentType = "Principal" principalId = $signedInUser.Id resourceId = $graphSp.Id scope = "openid profile offline_access User.Read" } $demoAppRoleAssignment = New-MgServicePrincipalAppRoleAssignment ` -ServicePrincipalId $demoSpObjectId ` -BodyParameter @{ principalId = $demoSpObjectId resourceId = $graphSp.Id appRoleId = "498476ce-e0fe-48b0-b801-37ba7e2685c6" }
Az CLI:
$tenantId = "<tenant-id>" az login --tenant $tenantId $demoName = "Session2-PermissionsDemo-$(Get-Date -Format 'yyyyMMddHHmmss')" $demoApp = az ad app create --display-name $demoName --sign-in-audience AzureADMyOrg | ConvertFrom-Json $demoSp = az ad sp create --id $demoApp.appId | ConvertFrom-Json $demoAppObjectId = $demoApp.id $demoSpObjectId = $demoSp.id $signedInUserId = az ad signed-in-user show --query id -o tsv $graphSpId = az rest ` --method get ` --uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=appId eq '00000003-0000-0000-c000-000000000000'" ` --query "value[0].id" ` --output tsv $delegatedGrantBodyPath = Join-Path $env:TEMP "session2-delegated-grant.json" $appRoleAssignmentBodyPath = Join-Path $env:TEMP "session2-app-role-assignment.json" (@{ clientId = $demoSpObjectId consentType = "Principal" principalId = $signedInUserId resourceId = $graphSpId scope = "openid profile offline_access User.Read" } | ConvertTo-Json -Compress) | Set-Content -Path $delegatedGrantBodyPath -Encoding ascii $demoDelegatedGrant = az rest ` --method post ` --uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" ` --headers "Content-Type=application/json" ` --body "@$delegatedGrantBodyPath" | ConvertFrom-Json (@{ principalId = $demoSpObjectId resourceId = $graphSpId appRoleId = "498476ce-e0fe-48b0-b801-37ba7e2685c6" } | ConvertTo-Json -Compress) | Set-Content -Path $appRoleAssignmentBodyPath -Encoding ascii $demoAppRoleAssignment = az rest ` --method post ` --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$demoSpObjectId/appRoleAssignments" ` --headers "Content-Type=application/json" ` --body "@$appRoleAssignmentBodyPath" | ConvertFrom-Json Remove-Item $delegatedGrantBodyPath,$appRoleAssignmentBodyPath
https://developer.microsoft.com/en-us/graph/graph-explorer in a browser.Sign in to Graph Explorer.If Graph Explorer is blocked by user-consent policy in your tenant, continue with the disposable demo app and delegated grant from Step 0. The grant object behaves the same for enumeration and revocation.
/meGET https://graph.microsoft.com/v1.0/me.Expected: returns your own user object. This is delegated access.
/users and expand scopeGET https://graph.microsoft.com/v1.0/users.User.Read.All scope and re-run the call.Expected: the first call returns a 403 Forbidden until the broader delegated scope is consented. After consent, the users call succeeds.
Native PowerShell:
# Reuse the signed-in user token from Step 0. $headers = @{ Authorization = "Bearer $accessToken" } Invoke-RestMethod ` -Method Get ` -Uri "https://graph.microsoft.com/v1.0/me" ` -Headers $headers # This returns 403 unless the current client has User.Read.All consented. Invoke-RestMethod ` -Method Get ` -Uri "https://graph.microsoft.com/v1.0/users" ` -Headers $headers
Graph PowerShell:
Connect-MgGraph -Scopes "User.Read" Get-MgUser -UserId "me" # Reconnect with the broader delegated scope if you want to test directory reads. Connect-MgGraph -Scopes "User.Read","User.Read.All" Get-MgUser -Top 5
Az CLI:
az login --tenant $tenantId az rest ` --method get ` --url "https://graph.microsoft.com/v1.0/me" # This returns 403 unless the current Azure CLI client has User.Read.All consented. az rest ` --method get ` --url "https://graph.microsoft.com/v1.0/users"
These alternate clients use their own app registrations instead of Graph Explorer, but they demonstrate the same delegated model: the token carries scp scopes and stays bounded by user context.
Enterprise Applications.Graph Explorer to review the delegated grant created in Step 1.Session2-PermissionsDemo-* enterprise app from Step 0 and open Permissions.Record the permission grant IDs you can see.
The code paths below intentionally query the disposable demo app from Step 0 because it gives you a predictable delegated grant ID for enumeration and revocation.
Native PowerShell:
Invoke-RestMethod ` -Method Get ` -Uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=clientId eq '$demoSpObjectId'" ` -Headers @{ Authorization = "Bearer $accessToken" }
Graph PowerShell:
Get-MgOauth2PermissionGrant -Filter "clientId eq '$demoSpObjectId'" | Select-Object Id, Scope, ConsentType, PrincipalId
Az CLI:
$grants = az rest ` --method get ` --uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=clientId eq '$demoSpObjectId'" | ConvertFrom-Json $grants.value | Select-Object id, scope, consentType, principalId
Permissions blade open from Step 3.Native PowerShell:
$grants = Invoke-RestMethod ` -Method Get ` -Uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=clientId eq '$demoSpObjectId'" ` -Headers @{ Authorization = "Bearer $accessToken" } $grants.value | Select-Object id, scope, consentType # Revoke a specific delegated grant. # $grantId = $demoDelegatedGrant.id # Invoke-RestMethod ` # -Method Delete ` # -Uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$grantId" ` # -Headers @{ Authorization = "Bearer $accessToken" }
Graph PowerShell:
Import-Module Microsoft.Graph.Identity.SignIns Connect-MgGraph -Scopes "DelegatedPermissionGrant.ReadWrite.All" $grants = Get-MgOauth2PermissionGrant -Filter "clientId eq '$demoSpObjectId'" $grants | Select-Object Id, Scope, ConsentType # Remove the disposable demo grant from Step 0, or another non-essential grant ID you found above. # Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $demoDelegatedGrant.Id
Az CLI:
$grants = az rest ` --method get ` --uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=clientId eq '$demoSpObjectId'" | ConvertFrom-Json $grants.value | Select-Object id, scope, consentType # Revoke a specific delegated grant. # $grantId = $demoDelegatedGrant.id # az rest ` # --method delete ` # --uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$grantId"
Step 1: Remove the consent grant. Step 2: If a compromised user session is involved, revoke that user's refresh tokens separately. Step 3: For service principals, disable the app or rotate credentials and remember that existing access tokens remain valid until expiry.
Expected observation: the portal shows the permission gone after the revocation call runs. Existing Graph Explorer or delegated client sessions still work until their access tokens expire, demonstrating that permission revocation is not immediate token revocation.
Cloud Apps > App Governance.Mail.ReadWrite.All or Files.ReadWrite.All application permissions.Expected: identify apps that are inactive, unowned, or holding high-privilege permissions that exceed their apparent purpose. This step is intentionally portal-first because App Governance is the purpose-built investigation surface.
Protection > Conditional Access > Policies.Report-only mode initially.Expected: the policy appears in the list. In Report-only mode, sign-in logs show what would have been blocked without affecting current sessions.
Native PowerShell:
# Create a CA policy blocking device code flow (Report-only) # Requires Security Administrator or Conditional Access Administrator. # Replace the excluded user with a real break-glass account before using this outside a lab. $policyBody = @{ displayName = "Block Device Code Flow" state = "enabledForReportingButNotEnforced" conditions = @{ clientAppTypes = @("all") applications = @{ includeApplications = @("All") } users = @{ includeUsers = @("All") excludeUsers = @("<break-glass-user-id>") } authenticationFlows = @{ transferMethods = "deviceCodeFlow" } } grantControls = @{ operator = "OR" builtInControls = @("block") } } | ConvertTo-Json -Depth 6 Invoke-RestMethod ` -Method Post ` -Uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" ` -Headers @{ Authorization = "Bearer $accessToken"; "Content-Type" = "application/json" } ` -Body $policyBody
Graph PowerShell:
Import-Module Microsoft.Graph.Identity.SignIns Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess" $params = @{ displayName = "Block Device Code Flow" state = "enabledForReportingButNotEnforced" conditions = @{ clientAppTypes = @("all") applications = @{ includeApplications = @("All") } users = @{ includeUsers = @("All") excludeUsers = @("<break-glass-user-id>") } authenticationFlows = @{ transferMethods = "deviceCodeFlow" } } grantControls = @{ operator = "OR" builtInControls = @("block") } } New-MgIdentityConditionalAccessPolicy -BodyParameter $params
Az CLI:
$policyBodyPath = Join-Path $env:TEMP "session2-device-code-policy.json" (@{ displayName = "Block Device Code Flow" state = "enabledForReportingButNotEnforced" conditions = @{ clientAppTypes = @("all") applications = @{ includeApplications = @("All") } users = @{ includeUsers = @("All") excludeUsers = @("<break-glass-user-id>") } authenticationFlows = @{ transferMethods = "deviceCodeFlow" } } grantControls = @{ operator = "OR" builtInControls = @("block") } } | ConvertTo-Json -Depth 6) | Set-Content -Path $policyBodyPath -Encoding ascii az rest ` --method post ` --uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" ` --headers "Content-Type=application/json" ` --body "@$policyBodyPath" Remove-Item $policyBodyPath
Native PowerShell:
Invoke-RestMethod ` -Method Delete ` -Uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$($demoDelegatedGrant.id)" ` -Headers @{ Authorization = "Bearer $accessToken" } Invoke-RestMethod ` -Method Delete ` -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$demoSpObjectId/appRoleAssignments/$($demoAppRoleAssignment.id)" ` -Headers @{ Authorization = "Bearer $accessToken" } # Remove this too if you created the report-only device-code policy in Step 6. # Invoke-RestMethod ` # -Method Delete ` # -Uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies/<policy-id>" ` # -Headers @{ Authorization = "Bearer $accessToken" } az ad sp delete --id $demoSpObjectId az ad app delete --id $demoAppObjectId
Graph PowerShell:
Connect-MgGraph -Scopes "Application.ReadWrite.All","DelegatedPermissionGrant.ReadWrite.All","AppRoleAssignment.ReadWrite.All" Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId $demoDelegatedGrant.Id Remove-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $demoSpObjectId -AppRoleAssignmentId $demoAppRoleAssignment.Id # Remove this too if you created the report-only device-code policy in Step 6. # Remove-MgIdentityConditionalAccessPolicy -ConditionalAccessPolicyId "<policy-id>" Remove-MgServicePrincipal -ServicePrincipalId $demoSpObjectId Remove-MgApplication -ApplicationId $demoAppObjectId
Az CLI:
az rest ` --method delete ` --uri "https://graph.microsoft.com/v1.0/oauth2PermissionGrants/$($demoDelegatedGrant.id)" az rest ` --method delete ` --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$demoSpObjectId/appRoleAssignments/$($demoAppRoleAssignment.id)" # Remove this too if you created the report-only device-code policy in Step 6. # az rest ` # --method delete ` # --uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies/<policy-id>" az ad sp delete --id $demoSpObjectId az ad app delete --id $demoAppObjectId
scp claim and a roles claim in an access token?Admin takeaways checklist - the five non-negotiables from this section
Next-section bridge - this session's permissions become next session's tokens in action
Volatile platform behavior and dated claims in this section were checked against these current sources on April 26, 2026:
All links below were reviewed on 2026-03-10.
Section 2 - Permissions and Scopes - Reference Guide