The scary part of a multi-tenant app isn’t the app. It’s the service principal it drops into your tenant the moment someone consents, a real security object, with real permissions, owned by a company whose change log you don’t read. Three things usually go wrong, and they go wrong quietly: someone grants application permissions where delegated would do, someone accepts Directory.ReadWrite.All because the consent dialog made it sound routine, and nobody can later name which external apps are already sitting in the tenant with a grant. Work the list in this order.
1. Know what “multi-tenant” actually means on the registration
In an app registration, the switch is signInAudience. AzureADMyOrg is single-tenant, only your directory. AzureADMultipleOrgs means any Entra tenant can provision a service principal for it. AzureADandPersonalMicrosoftAccount adds consumer accounts on top. If you build SaaS, multi-tenant is correct. The mistake is reaching for it when you have three known customer tenants you could just add as guests. Broaden the audience on purpose, not because the wizard defaulted there.
2. Separate delegated from application permissions, they fail differently
Delegated permissions act as the signed-in user and are capped by that user’s rights. Application permissions act as the app, with no user in the loop and no ceiling but the scope itself. A vendor that asks for Mail.Read delegated reads what the caller could already read. The same vendor asking for Mail.Read application reads every mailbox in the tenant, forever, including after the employee who clicked “Accept” has left. If an app wants application-level Directory.ReadWrite.All to “sync users,” that is not a scope, that’s a standing key to your directory. Make them justify it.
3. Audit what’s already consented before you touch anything
You can’t govern what you can’t list. In the admin center: Identity → Applications → Enterprise applications, then filter Application type = All and add the Homepage URL/owner columns. The tell for an external multi-tenant app is appOwnerOrganizationId pointing at a tenant that isn’t yours. The Graph endpoints that matter: /servicePrincipals for the objects, /oauth2PermissionGrants for delegated consent, and /servicePrincipals/{id}/appRoleAssignments for application permissions.
List the externally-owned ones first, those are the apps where another organisation controls the code:
4. Check publisher verification and consent necessity before you approve
A verified publisher is a blue badge backed by a Microsoft Partner Network account Microsoft has actually checked, it won’t tell you the app is safe, but an unverified publisher asking for tenant-wide application permissions is a hard stop until you know who they are. Three questions per app: who owns it (publisher + appOwnerOrganizationId), what does it truly need (delegated beats application; read beats write; scoped beats .All), and does anyone still use it. An unused app with a live grant is pure liability.
5. Revoke safely, dry-run first, and know which consent you’re killing
Admin consent covers the whole tenant; user consent covers one user. Pulling an admin grant can break every user at once, so model the blast radius before you commit. Start from your reviewed CSV, not a live pipe, and keep the state-changing call behind -WhatIf until you’ve read the output:
Revoking the grant leaves the service principal in place, disable or delete it separately if the app is genuinely gone, and tell the owning team first, because a broken integration at 9am reads exactly like a breach.
If you do one thing this week: run step 3, sort the CSV by owner, and find the externally-owned apps holding application permissions ending in .All. That short list is where your next incident is hiding.