Graph permission grants belong to the tenant, not to your solution
webApiPermissionRequests in package-solution.json is a request, not a grant. What an administrator approves lands on one tenant-wide service principal — SharePoint Online Client Extensibility Web Application Principal — so every SPFx component in that tenant inherits it. A scope another solution had approved works in yours before you deploy anything; a scope only your dev tenant has will be missing at the customer.
webApiPermissionRequests je jen žádost. Schválené oprávnění dostane jeden tenant-wide servisní principál, takže ho zdědí všechny SPFx komponenty v tenantu – i ta, která o něj nikdy nepožádala. U zákazníka naopak může chybět.
Symptom
The surprising direction: something works that should not yet.
You add a Graph call needing a scope your solution does not declare, bump the package, and prepare to tell everyone the feature stays dormant until an admin approves it in SharePoint admin center → Advanced → API access. Then you test — and the call returns 200 on the first try, with the old package still deployed.
The mirror image is the one that hurts: the same code ships to a customer whose tenant never approved that scope, and the feature fails there with 403 while "working fine in our tenant" for months.
Cause
Delegated permissions for SPFx are not granted per solution. The approval is recorded against the tenant's SharePoint Online Client Extensibility Web Application Principal, and every SPFx web part, extension or library in the tenant requests its Graph token through that same principal. Consequences:
- A scope approved for solution A is silently available to solutions B and C.
- Your
webApiPermissionRequestsentry only makes the request appear in API access for approval. Removing it does not revoke anything. AadHttpClient/MSGraphClienttherefore succeed or fail based on the tenant's aggregate grants, never on your manifest.
What to do
- Declare the scope anyway. It documents the dependency and it is what makes the request visible on a fresh tenant. Just do not treat the declaration as the thing that switches the feature on.
- Decide rollout by a live test on the target tenant, not by reading your own manifest. One call from the app is worth more than any amount of manifest review.
- Keep a fail-safe branch for the missing grant, and make it visible. On a clean tenant this is the only thing that separates "no permission" from "no data" — silently returning an empty result turns a consent problem into a phantom bug. Surface the real error text; a
403may equally mean a licensing gate, not a missing scope. - Audit the other direction too. Because a foreign grant can carry your code, an app may be running on a scope it never declared — and break at the first customer who has a tidier tenant. When a feature depends on Graph, verify the scope is actually in your manifest even though everything works.
- Practical upside in a versioned-runtime setup: adding a scope does not block a release. The package carrying the new request can be uploaded later; until then the feature runs wherever the grant already exists.
The request only appears if the package version changed
Measured on a live tenant, 2026-09-04. Adding a scope to a solution that is already deployed and re-uploading the package without bumping solution.version produces no request at all in API access. The App Catalog treats a same-version upload as a replacement, not an upgrade, so nothing new is ever surfaced for approval.
That combines badly with the tenant-wide rule above, because the two failure modes look identical from your desk:
- The scope was already granted for another solution, so no request appears — and everything works.
- The version did not change, so no request appears — and nothing works on a tenant that lacks the grant.
Check the grant before concluding anything from a missing request. If Microsoft Graph / <scope> is already listed as approved in API access (or by Get-SPOTenantServicePrincipalPermissionGrants), you are in the first case and there is nothing to do. If it is not, bump solution.version and upload again, or grant the scope directly. The official SPO Management Shell adds a grant without any pending request, and it signs in without an app registration of your own:
Connect-SPOService -Url https://contoso-admin.sharepoint.com
Approve-SPOTenantServicePrincipalPermissionGrant -Resource "Microsoft Graph" -Scope "GroupMember.Read.All"
Grant-PnPTenantServicePrincipalPermission does the same from PnP.PowerShell, but only with your own Entra ID app registration (passed as -ClientId or through the ENTRAID_APP_ID environment variable), and PnP lists Directory.ReadWrite.All on Microsoft Graph among its requirements. Don't grant the scope in Entra on the principal itself: SharePoint controls that principal through the API access page, and Microsoft documents direct changes to it in the Entra admin center as unsupported.
Notes
- Same mechanics apply to extensions and library components — anything shipped as SPFx.
- Approval is per tenant, so a dev tenant that accumulated scopes over years is a poor proxy for a customer's. Keep a note of which scopes each solution genuinely needs.
- Nothing here applies to app-only permissions, which live on their own app registration.
- The gap cannot be closed from inside the solution, not even with a Global Administrator at the keyboard — see No "grant it for me" button in a web part.