APP-03: Internal App Registration Permissions

Overview

Internal app registrations are applications you created and registered in your own Entra ID tenant. You own the code and control the configuration. Even so, misconfigured Graph API permissions can expose excessive access to mailboxes, directory data, or role management, often more than the application actually needs.

This control requires quarterly review of all internal app registrations that hold high-privilege Graph permissions, with documented business justification for each elevated permission retained. Internal apps are distinct from third-party enterprise apps consented from external vendors; those are covered by APP-06: Third-Party Enterprise App Permissions.

"Internal app registrations are applications you created and control. While you own the code, misconfigured permissions can expose excessive access. Regular review ensures your own apps only have necessary permissions."

Prerequisites

Required Roles

  • Global Administrator - Full access to review any app registration
  • Application Administrator - Can review and modify most app registrations
  • Cloud Application Administrator - Can review app registrations (excluding app proxy apps)
  • Privileged Role Administrator - Required for apps holding role management permissions

Required Licenses

  • Microsoft Entra ID (any tier)

Time Estimate

  • Initial Audit: 30-45 minutes
  • Per-App Review: 10-15 minutes
  • Documentation: 15-30 minutes

Step-by-Step Instructions

Step 1: Identify Internal App Registrations

Internal app registrations are found under App registrations, not Enterprise applications. Enterprise applications include both your own apps and third-party apps you have consented to.

  1. Navigate to Microsoft Entra admin center (https://entra.microsoft.com)
  2. Go to IdentityApplicationsApp registrations
  3. Click All applications to see everything registered in your tenant
  4. Filter by Owned applications to narrow to apps your organization owns

Note: Enterprise applications that correspond to your own app registrations appear in both blades. Third-party vendor apps appear only under Enterprise applications. If an app has no corresponding App registrations entry owned by your organization, it is a third-party app. Review it under APP-06: Third-Party Enterprise App Permissions instead.

Step 2: Flag High-Privilege Graph Permissions

Review the API permissions configured on each internal app registration. The following permissions require elevated scrutiny and documented business justification:

Permissions to Flag for Review

PermissionRiskDescription
Mail.ReadWrite.AllHighRead and write all mailboxes in the tenant
Directory.ReadWrite.AllHighFull read/write access to Entra ID directory data
RoleManagement.ReadWrite.DirectoryHighAssign or remove any admin role in the tenant
AppRoleAssignment.ReadWrite.AllHighGrant the app additional permissions at runtime
Mail.ReadWrite (Application)HighRead/write all mailboxes without user context
Files.ReadWrite.All (Application)HighAccess all OneDrive and SharePoint files
User.ReadWrite.All (Application)HighModify all user accounts

Audit via PowerShell

# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Application.Read.All", "Directory.Read.All"

# Permissions to flag
$flaggedPermissions = @(
    "Mail.ReadWrite.All",
    "Directory.ReadWrite.All",
    "RoleManagement.ReadWrite.Directory",
    "AppRoleAssignment.ReadWrite.All",
    "Mail.ReadWrite",
    "Files.ReadWrite.All",
    "User.ReadWrite.All",
    "Group.ReadWrite.All"
)

# Get Microsoft Graph service principal for permission resolution
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"

# Get all app registrations owned by your organization
$appRegistrations = Get-MgApplication -All

$results = @()

foreach ($app in $appRegistrations) {
    # Get the corresponding service principal
    $sp = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'"
    if (-not $sp) { continue }

    # Get granted app role assignments (application permissions)
    $appRoles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id

    foreach ($role in $appRoles) {
        $permission = $graphSp.AppRoles | Where-Object { $_.Id -eq $role.AppRoleId }

        if ($permission.Value -in $flaggedPermissions) {
            $results += [PSCustomObject]@{
                AppName    = $app.DisplayName
                AppId      = $app.AppId
                Permission = $permission.Value
                GrantedOn  = $role.CreatedDateTime
            }
        }
    }
}

$results | Export-Csv -Path "InternalApps-HighPrivPermissions.csv" -NoTypeInformation
$results | Format-Table -AutoSize

Step 3: Document Business Justification

For each internal app with a flagged permission, record the following in your app inventory or ITSM system:

  1. Application name and App ID
  2. Permission name and type (delegated vs. application)
  3. Business purpose: What function requires this permission?
  4. Data access scope: Which mailboxes, users, or directory objects does the app actually touch?
  5. Owner: Which team or individual owns and maintains this app?
  6. Last reviewed date
  7. Justification decision: Retain, reduce, or revoke

If a less-privileged alternative is available, prefer it:

Current PermissionLeast-Privilege Alternative
Mail.ReadWrite.AllMail.ReadWrite.Shared + Application Access Policy
Files.ReadWrite.AllSites.Selected + specific site permissions
User.ReadWrite.AllDelegated flow or scoped admin role
Directory.ReadWrite.AllSpecific read/write permissions scoped to needed object types

Step 4: Reduce Permissions Where Possible

Remove a Permission via Entra Admin Center

  1. Navigate to App registrations → Select the application
  2. Go to API permissions
  3. Select the permission to remove
  4. Click Remove permission
  5. Re-grant admin consent for the updated, reduced permission set

Restrict Mail Access to Specific Mailboxes

If Mail.ReadWrite.All is necessary but can be scoped:

Connect-ExchangeOnline

# Create a mail-enabled security group for allowed mailboxes
New-DistributionGroup -Name "AppName-MailboxAccess" -Type Security

# Add only the mailboxes the app legitimately needs
Add-DistributionGroupMember -Identity "AppName-MailboxAccess" -Member "service@contoso.com"

# Create an Application Access Policy to restrict the app
New-ApplicationAccessPolicy `
    -AppId "your-internal-app-id" `
    -PolicyScopeGroupId "AppName-MailboxAccess" `
    -AccessRight RestrictAccess `
    -Description "Restrict internal app to specific mailboxes only"

# Verify the policy works
Test-ApplicationAccessPolicy -Identity "service@contoso.com" -AppId "your-internal-app-id"

Use Sites.Selected Instead of Sites.ReadWrite.All

$siteId = "contoso.sharepoint.com,<site-guid>,<web-guid>"
$appId  = "your-internal-app-id"

$params = @{
    roles = @("write")
    grantedToIdentities = @(
        @{
            application = @{
                id          = $appId
                displayName = "Your Internal App"
            }
        }
    )
}

New-MgSitePermission -SiteId $siteId -BodyParameter $params

Step 5: Establish Quarterly Review Cadence

APP-03 requires a formal quarterly review cycle. At each review:

  1. Re-run the PowerShell audit to detect new or changed permissions
  2. Verify that justifications remain current and accurate
  3. Remove permissions for apps that are decommissioned or no longer active
  4. Record the review date and reviewer in your app inventory

Suggested schedule:

Review ItemFrequency
Full permission audit of internal appsQuarterly
Newly registered internal appsAt registration
Decommissioned app cleanupAt decommission
Business justification refreshAnnually (minimum)

Verification Checklist

  • All internal app registrations are inventoried and have an assigned owner
  • Apps holding Mail.ReadWrite.All, Directory.ReadWrite.All, or RoleManagement.ReadWrite.Directory are identified and flagged
  • Business justification is documented for every flagged permission
  • Least-privilege alternatives have been evaluated for each flagged permission
  • Application Access Policies are configured where mailbox-scoped access is sufficient
  • Decommissioned internal apps have been removed or disabled
  • Quarterly review schedule is established and the first review is complete
  • Review findings are recorded in your ITSM or app inventory system

Troubleshooting

Issue: App stops working after permission reduction

Cause: The application actively uses the permission that was removed.

Solution:

  1. Check application error logs and Entra sign-in logs for failed API calls
  2. Identify the specific Graph calls failing and the minimum permission required
  3. If a broad permission is genuinely necessary, document the business justification and add compensating controls (Application Access Policy, enhanced audit logging)

Issue: Cannot determine the purpose of an internal app

Cause: The app may have been created by a former employee or undocumented project.

Solution:

  1. Check audit logs for the original registration event:
    Search-UnifiedAuditLog -StartDate (Get-Date).AddYears(-1) -EndDate (Get-Date) -Operations "Add application" -RecordType AzureActiveDirectory
    
  2. Review sign-in logs for recent activity
  3. Check the Owners list on the app registration
  4. If no owner or justification can be found after 30 days, disable (do not delete) the app and monitor for any reported breakage before removal

Issue: Permission reappears after removal

Cause: The app or a developer is re-requesting the permission through a consent flow.

Solution:

  1. Restrict user consent to prevent re-consent (see APP-08)
  2. Configure the admin consent workflow so that future re-grants require explicit approval (see APP-04)
  3. Document the permission as "denied" in the app inventory

Issue: App has delegated permissions granted by individual users

Cause: Users consented to delegated permissions outside of the admin consent process.

Solution:

  1. Query OAuth2 permission grants:
    Get-MgOauth2PermissionGrant -Filter "clientId eq 'service-principal-id'"
    
  2. Revoke individual user consents:
    Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId "<grant-id>"
    

Related Resources


Last updated: January 2025