You have built an SPFx web part that shows a user their upcoming calendar events. It works perfectly in your development tenant. You package it, deploy it to the client’s app catalogue, and a week later the security officer sends an email asking why your web part requires tenant-wide access to every user’s calendar.
They are not being obstructive. They have read the permission request correctly, and the answer is genuinely uncomfortable.
How SPFx gets its permissions
When an SPFx solution calls Microsoft Graph, it does not authenticate as itself. It borrows the identity of a single Entra ID application called the SharePoint Online Client Extensibility Web Application Principal. You declare what you need in the solution package:
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "Calendars.Read"
}
]
On deployment, an administrator approves that request in the SharePoint admin centre. The scope is then granted — and this is the part that matters — to the shared principal, not to your web part.
The consequence
Once Calendars.Read is granted, every SPFx component in that tenant can use it. Not just yours. Any web part, any extension, anything deployed to the app catalogue now or in future, by you or by anyone else with deployment rights.
The permission boundary is the tenant, not the solution. There is no per-web-part isolation, and there is no way to configure one.
So when a security officer asks whether approving your calendar web part means a future web part could also read calendars, the honest answer is yes. If they ask whether you can scope the grant to only your component, the honest answer is no.
Why this is often acceptable anyway
Before treating this as disqualifying, it is worth being clear about what it does and does not mean.
Graph calls from SPFx run in the delegated context by default. The signed-in user’s own permissions still apply. A web part granted Calendars.Read lets a user read their own calendar — it does not let them read the chief executive’s. The grant expands what SPFx code can attempt; it does not expand what any individual user is entitled to see.
It also matters who can deploy to the app catalogue. If that is restricted to two administrators under change control, the practical risk of a rogue component appearing is low. The shared principal is a real architectural weakness, but it is one mitigated by deployment governance rather than by code.
When you need real isolation
Some environments cannot accept it — typically where a compliance framework requires that each application’s access be independently auditable and revocable. In those cases the pattern is to stop calling Graph from SPFx at all:
- Create a dedicated app registration for your solution, with only the permissions it needs
- Put an API in front of it — Azure Functions or API Management — that holds those permissions and exposes only the specific operations required
- Have the web part call that API using
AadHttpClient, authenticating against your own app registration rather than the shared principal
The permission grant is now scoped to an application the client controls independently. It can be audited, restricted by IP, rate limited, and revoked without affecting anything else in the tenant.
The cost is real: another Azure resource to run and pay for, another deployment pipeline, and CORS configuration that will take an afternoon longer than you expect. Audience mismatches between the token your web part requests and the audience your API validates are the single most common failure, and the error messages are not helpful.
The practical advice
Settle this in architecture, not in development. The sequence that costs projects weeks looks like this: build the solution, complete UAT, submit for security review, discover the shared principal is unacceptable, redesign the data access layer, retest.
The sequence that does not: ask during discovery whether tenant-wide Graph grants are acceptable, get an answer in writing, and design accordingly. It is a ten-minute conversation at the right moment and a three-week rebuild at the wrong one.