The problem hiding in plain sight
If your organization sends transactional or automated email through the Microsoft Graph API, there's a good chance you're using an Entra ID app registration with the Mail.Send Application permission. It's the standard pattern: register an app, grant it Mail.Send, get admin consent, authenticate with client credentials, and let the app send mail without any user needing to be signed in.
The problem is what "Application permission" actually means. Unlike delegated permissions which are bound to whichever user signed in an application permission grants access at the tenant level. Once Mail.Send is consented for an app, that app can send email as any mailbox in the organization, not just the one mailbox it was built for. A notification service meant to send as [email protected] can, technically, send as your CEO, your finance team, or anyone else in the directory.
This becomes a real concern the moment the app's client secret or certificate leaves your direct control for example, when it's shared with a third-party developer building the integration. If those credentials are ever mishandled, copied, or the developer's own environment is compromised, the blast radius isn't "one mailbox." It's every mailbox in the tenant.
Microsoft provides a purpose-built mechanism to close this gap without giving up the Graph-based, app-only sending pattern.
Two mechanisms, one now legacy
There have been two ways to scope app-only Exchange Online access down to specific mailboxes:
Application Access Policies — the older mechanism. It ties an app's AppId to a mail-enabled security group and restricts the Entra-consented permission to only apply within that group.
RBAC for Applications — the current, actively developed mechanism, and the one Microsoft now recommends. It extends Exchange Online's existing role-based access control model so that an application (via its service principal) can be assigned a specific role like Application Mail.Send scoped to a management scope or Entra administrative unit, independently of whatever was consented in Entra ID.
Microsoft has stated that RBAC for Applications replaces Application Access Policies going forward, so unless you have a specific reason to stay on the legacy model, build new restrictions on RBAC for Applications.
Step by step to setup RBAC for Applications
The workflow has five parts: define who the app is allowed to touch, register the app as a known principal inside Exchange, assign it a scoped role, verify it, and critically clean up the broader grant it started with.
1. Define the resource scope
Create a management scope that identifies exactly which mailbox (or mailboxes) the app should be allowed to send from:
Connect-ExchangeOnline
New-ManagementScope -Name "AppSendScope" ` -RecipientRestrictionFilter "EmailAddresses -eq '[email protected]'"
For multiple allowed senders, use a group-based filter instead of individual addresses, or a broader recipient filter matching a naming convention.
2. Register the app as an Exchange service principal
Exchange doesn't automatically know about your Entra app registration — you need to create a pointer object to it.
New-ServicePrincipal -AppId "<Application (client) ID>" ` -ObjectId "<Service Principal Object ID>" ` -DisplayName "AppSendPrincipal"
This is where most people get tripped up (more on that below): the -ObjectId here is not the Object ID shown on the App Registrations page in Entra. It's the Object ID of the app's enterprise application / service principal object (a different object representing the same app). To get that use below command.
Connect-MgGraph -Scopes "Application.Read.All"
Get-MgServicePrincipal -Filter "appId eq '<Application (client) ID>'" | Select-Object Id, DisplayName, AppId
The Id field in that output is what goes into -ObjectId above.
3. Assign the scoped role
New-ManagementRoleAssignment -App "<Application (client) ID>" ` -Role "Application Mail.Send" ` -CustomResourceScope "AppSendScope"
4. Verify
Get-ManagementRoleAssignment -RoleAssignee "AppSendPrincipal"
Test-ServicePrincipalAuthorization -Identity "<Application (client) ID>" ` -Resource "[email protected]"
Test-ServicePrincipalAuthorization should return InScope = True for the allowed mailbox, and InScope = False for any mailbox outside the scope. Unlike normal permission changes, this test command bypasses Exchange's permission cache, so it reflects the current configuration immediately.
5. Additional Step
The scoped role assignment does not, by itself, restrict anything, if the app still holds an unscoped Mail.Send grant in Entra ID.
RBAC for Applications and Entra ID consent are two independent authorities, and their permissions are additive a union, not an override. If your app has org-wide Mail.Send consented in Entra, and you also configure a scoped Mail.Send via RBAC for Applications, the result is still org-wide access. The scope adds nothing, because the broader grant already covers everything the scope would have allowed.
The fix is,
Go to Entra admin center → Identity → App registrations → [your app] → API permissions.
Find Mail.Send under Microsoft Graph → Application permissions.
Remove it.
Only after that unscoped grant is gone does the RBAC scope actually become a restriction rather than a redundant, overridden rule.
Lessons from a real rollout
A few practical issues came up repeatedly when applying this to multiple applications, worth flagging so you don't lose time on them:
"Couldn't find a service principal with the following identity" on New-ManagementRoleAssignment almost always means the New-ServicePrincipal step either wasn't run or failed. Exchange needs its own pointer object before it can assign a role to an app.
"AADServicePrincipalNotFound" on New-ServicePrincipal almost always means the Object ID you supplied doesn't belong to that AppId's service principal in this tenant usually because it was copied from the wrong Entra blade (App Registrations instead of Enterprise Applications) or mistyped/truncated during copy-paste. Pull it programmatically with Get-MgServicePrincipal rather than copying it by hand from the portal.
Get-ManagementRoleAssignment doesn't accept -App or -AppID. Use -RoleAssignee "<DisplayName>" instead, or pipe the full list through Where-Object.
If Get-MgServicePrincipal -Filter "appId eq '...'" returns nothing at all, no enterprise application object exists yet for that app in this tenant. Creating one with New-MgServicePrincipal -AppId "<id>" resolves it, after which the filter query will return a valid Id.
When scaling this across many applications, keep scope and principal names unique and traceable (e.g. AppSendScope-<AppName>, <AppName>Principal) so a later audit can tell at a glance which restriction belongs to which app.
Beyond scoping: rounding out the defense
Restricting which mailbox an app can send from closes the biggest gap, but it's worth pairing with a few other measures:
Grant only what's needed. Mail.Send alone, not Mail.ReadWrite or broader mail scopes, unless the app genuinely needs them.
Use a dedicated sending mailbox, not a real employee's mailbox, so the scoped identity itself carries no independent value if compromised.
Prefer certificate-based authentication over client secrets where the app supports it. Certificates are harder to casually copy or leak, and easier to rotate on a schedule.
Avoid handing the raw credential to a developer at all, where possible. Store it in a secrets manager the app retrieves at runtime, rather than sharing it directly.
Monitor sign-ins and mail-send activity for the app's service principal via Exchange's unified audit log, and alert on any denied attempt a denial against a mailbox outside the intended scope is a useful early signal, not just background noise.
Review and rotate periodically. Treat the app's permissions the same way you'd treat a privileged account: subject to periodic access review, not a "set once and forget" configuration.
Join the conversation! Your thoughts help the community grow.