Microsoft Entra ID security

How to review app permissions in Microsoft Entra ID

List the permissions apps hold with Microsoft Graph PowerShell, check 1 app's consent in the Entra admin centre, or use Altivane's Applications report for a rated list of high-privilege apps.

By the Published Updated

To review app permissions in Microsoft Entra ID, list the delegated permission grants and the application permissions granted to each app, then look for broad permissions an app does not need. Graph returns what has been consented to, not what an app asks for, and the admin centre shows 1 application at a time.

Choose the method that fits the job
MethodBest fit
PowerShellA tenant-wide CSV of delegated grants and application permissions on Microsoft Graph, broad ones first.
Admin centreChecking what 1 application has been granted, by administrators and by users.
AltivaneRecommended for recurring reviews, with applications holding high-risk permissions rated against a threshold.

Before you start

Use an account in a role that can read grants, such as Global Reader, Directory Readers or Application Administrator. The PowerShell example requests Directory.Read.All, the least privileged permission for listing delegated grants and enough for its other calls; your organisation may require administrator consent. The admin centre steps need Cloud Application Administrator or Application Administrator.

The examples show permissions that have been granted, so one an app requests but nobody has approved is left out, and a recent grant can be slow to appear. The script reads application permissions on Microsoft Graph only, so those on other APIs, such as Exchange Online or SharePoint, are not in the CSV; delegated grants cover every API. Access can also come from Entra roles, Exchange RBAC for applications or Teams resource-specific consent, which are not read here. The examples read and export; they do not grant or revoke anything.

Option 1

Review app permissions with PowerShell

Install the modules once, then connect and export every delegated grant and every application permission on Microsoft Graph.

PowerShell
# Install once, if the modules are not already installed.
$modules = 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Applications',
    'Microsoft.Graph.Identity.SignIns'
Install-Module -Name $modules -Scope CurrentUser
PowerShell
$ErrorActionPreference = 'Stop'
$graphAppId = '00000003-0000-0000-c000-000000000000'
$wide = '\.(ReadWrite|FullControl)\.All$'
Connect-MgGraph -Scopes 'Directory.Read.All' -NoWelcome
try {
    # Names of every service principal, looked up by object ID.
    $names = @{}
    Get-MgServicePrincipal -All -Property 'id', 'displayName' |
        ForEach-Object { $names[$_.Id] = $_.DisplayName }

    # Microsoft Graph's own service principal, for the names of its application permissions.
    $graph = Get-MgServicePrincipal -Filter "appId eq '$graphAppId'" -Property 'id', 'appRoles'
    $roles = @{}
    foreach ($role in $graph.AppRoles) { $roles[[string]$role.Id] = $role.Value }

    $rows = [System.Collections.Generic.List[object]]::new()

    # Every app granted an application permission on Microsoft Graph.
    $assigned = Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $graph.Id -All
    $apps = @($assigned | Where-Object { $_.PrincipalType -eq 'ServicePrincipal' })
    foreach ($item in $apps) {
        $permission = $roles[[string]$item.AppRoleId]
        if (-not $permission) { $permission = [string]$item.AppRoleId }
        $rows.Add([pscustomobject]@{
            Application = $item.PrincipalDisplayName
            Kind        = 'Application permission'
            Resource    = $item.ResourceDisplayName
            Permission  = $permission
            UserId      = ''
            Wide        = $permission -match $wide
        })
    }

    # Every delegated grant, one row per permission.
    foreach ($grant in Get-MgOauth2PermissionGrant -All) {
        $admin = $grant.ConsentType -eq 'AllPrincipals'
        $kind = if ($admin) { 'Delegated, admin consent' } else { 'Delegated, user consent' }
        $client = $names[$grant.ClientId]
        if (-not $client) { $client = $grant.ClientId }
        $resource = $names[$grant.ResourceId]
        if (-not $resource) { $resource = $grant.ResourceId }
        foreach ($scope in @($grant.Scope -split ' ' | Where-Object { $_ })) {
            $rows.Add([pscustomobject]@{
                Application = $client
                Kind        = $kind
                Resource    = $resource
                Permission  = $scope
                UserId      = $grant.PrincipalId
                Wide        = $scope -match $wide
            })
        }
    }

    $report = @($rows | Sort-Object @{ Expression = 'Wide'; Descending = $true },
        Application, Kind, Permission)
    if ($report.Count -eq 0) {
        Write-Output 'No application permissions or delegated grants were found.'
    } else {
        $report | Export-Csv -Path './app-permissions.csv' -NoTypeInformation -Encoding utf8
        $flagged = @($report | Where-Object { $_.Wide }).Count
        Write-Output "$($report.Count) permissions exported; $flagged are marked Wide."
    }
} finally {
    Disconnect-MgGraph | Out-Null
}

The CSV has 1 row per permission per app, broad ones first. Kind says how it was granted: an application permission, a delegated one an administrator consented to for everyone, or one a single user agreed to, whose object ID is in UserId. Wide is true for names ending in ReadWrite.All or FullControl.All, which allow changes across the tenant. It is a rough first filter, not Microsoft's rating, and passes over broad read permissions such as Mail.Read.

Check a single app

Reconnect with Connect-MgGraph -Scopes 'Directory.Read.All', then read 1 app's grants. The output shows each API and permission as an ID. This takes 3 calls per app, so the export above works from the Microsoft Graph side instead.

PowerShell
# Run while connected to Microsoft Graph. Replace the example app name.
$filter = "displayName eq 'Contoso Payroll'"
$sp = Get-MgServicePrincipal -Filter $filter -Property 'id', 'displayName'
Get-MgOauth2PermissionGrant -Filter "clientId eq '$($sp.Id)'" |
    Select-Object ConsentType, PrincipalId, ResourceId, Scope
Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id |
    Select-Object ResourceDisplayName, AppRoleId

Option 2

Review app permissions in the admin centre

  1. Sign in to the Microsoft Entra admin centre for the correct tenant.
  2. Go to Entra ID > Enterprise apps > All applications and select the application.
  3. Select Permissions.
  4. Open the Admin consent tab for the permissions granted to the whole organisation, and the User consent tab for those granted to a specific user or group.
  5. Select a permission to open the Permission Details pane. Leave Revoke permission alone: it removes the grant in the tenant.

This suits 1 application you already have in mind. These steps show 1 application at a time, so for every app together, export with PowerShell or a report.

Common questions

Why is a permission my app requests not in the results?

Only consent counts here. A registration lists what it asks for, and a request nobody has approved does not appear in the grants.

Why does the list include apps nobody here installed?

The delegated grants cover every app in the tenant, Microsoft's own services included, and users can consent for themselves in some cases. Filter Kind to the user consent rows and read UserId to see who agreed.

Who decides whether an app keeps a permission?

The app's owners. Altivane lists registrations with no owner in App Registrations Without Owners and shows each high-privilege application's last sign-in when Microsoft reports one. Credentials are the other half of an app review: see how to find expiring app secrets.

Microsoft references

Technical references checked on 5 October 2026. The example scripts have been reviewed and syntax checked; they have not been run against a live tenant.

Talk to us about Altivane

Questions, or pricing? Get in touch.

Contact us