DEV Community

Anton Staykov
Anton Staykov

Posted on

Microsoft Graph least privilege has two control planes

The Microsoft Graph least-privilege story has improved a lot. That is not the problem.

The problem is that many teams are still looking at only half of it.

Granular permissions such as User.Create, User.EnableDisableAccount.All, User.RevokeSessions.All, and Application.ReadWrite.OwnedBy are a better design surface than the old habit of reaching for Directory.ReadWrite.All whenever an app needed to do anything useful in the directory. Microsoft Graph permissions intentionally follow a {resource}.{operation}.{constraint} naming pattern, which lets engineers express the operation an app is allowed to perform instead of throwing the app into a tenant-wide bucket (Microsoft Graph permissions overview).

That shift matters. But it does not finish the authorization model.

The less understood truth is this: Microsoft Graph least privilege has two control planes. The first is the Graph permission plane: what the client application is allowed to request from Microsoft Graph. The second is the Microsoft Entra control plane: who or what is allowed to control the application object, the service principal, and the directory objects the app will act on.

If you design only the first plane, your architecture can still be wildly overprivileged.

The first plane: Graph permissions authorize the client

Start with the piece engineers usually know.

Microsoft Graph supports delegated access and app-only access. In delegated access, the app calls Microsoft Graph on behalf of a signed-in user. In app-only access, the app calls Microsoft Graph as itself, without a signed-in user (Microsoft Graph permissions overview).

Delegated permissions are the easiest place to see the intersection model. Microsoft states it plainly: in a delegated scenario, the privileges an app has are determined by the Microsoft Graph permissions granted to the app and the user's own permissions (Microsoft Graph delegated permissions).

That sentence should be printed on every onboarding app design review.

If your custom HR onboarding app has been granted the delegated permission User.Create, the app has permission to call the create-user operation. But the signed-in user still needs the Entra authority to create users. The Graph permission authorizes the client. The Entra role authorizes the human. Delegated Microsoft Graph access needs both.

Flip it around and the same lesson holds. If the signed-in user is a User Administrator, but the app has not been granted the right delegated Microsoft Graph permission, the user's admin role does not magically donate authority to the client. A user can perform an operation in the Entra admin center and still fail through a custom app because the app itself is not authorized for the Graph call.

This is the first correction:

In a delegated scenario, admin role assignment is not a substitute for Graph consent, and Graph consent is not a substitute for admin role assignment.

The second plane: Entra controls who can control the thing doing the calling

Now comes the part that gets missed.

Microsoft Entra applications are represented by two related objects: the application object and the service principal object. The application object is the global template for the app. The service principal is the local representation of that app in a specific tenant, and it defines what the app can actually do in that tenant, who can access it, and what resources it can access (Application and service principal objects in Microsoft Entra ID).

That distinction is not directory trivia. It is a control-plane boundary.

The application object controls things like redirect URIs, credentials, requested permissions, exposed APIs, and ownership. The service principal controls the enterprise application instance in the tenant: assignments, local policies, consented permissions, provisioning configuration, sign-in behavior, and other tenant-specific settings (Application and service principal objects).

So when you ask, "is this Graph app least privileged?" the permission list is not enough. You also need to ask who can mutate the app registration, who can mutate the enterprise application, who can add credentials, who can add owners, who can change user assignment, and who can change the permissions the app requests.

That is the second plane.

Owner is not a label, owner is control

Application and enterprise application (service principal) ownership is one of the most underestimated privilege paths in Microsoft Entra.

Microsoft documents that an owner of an enterprise application can manage the organization-specific configuration of the application, including single sign-on, provisioning, and user assignment. The owner can also add or remove other owners. More importantly, Microsoft states that owners have the same permissions as application administrators, scoped to an individual application (Enterprise application ownership).

Read that again slowly: application administrator, scoped to one application.

That is not harmless metadata. That is delegated control over a highly sensitive object.

The default user permissions documentation is even more concrete:

Owners of application registrations can update application authentication (reply urls), credentials, owners, permissions, policies, and basic properties. Owners of enterprise applications can update service principal credentials, owners, permissions, app role assignments, provisioning settings, and related policies (Default user permissions and ownership permissions).

Microsoft also calls out the dangerous consequence directly:

... the application might have more permissions than the owner, which can become elevation of privilege because an owner can create or update objects while impersonating the application (Enterprise application ownership note).

That is the part many app teams miss. The owner does not need to have the same directory role as the app. If the app has powerful application permissions, the person who can add a credential or change ownership can potentially create a path to act as the app.

Static secrets get most of the attention here, but ownership is the quieter root. If I can become owner, add myself as another owner, add or rotate credentials, adjust the app configuration, or influence the service principal's local settings, I am no longer merely adjacent to the app. I am in the control plane of the app.

The bad pattern: broad Graph permission plus casual ownership

Here is the pattern I still see too often.

An engineering team builds a Graph automation app. They correctly avoid Directory.ReadWrite.All and use a narrower Graph permission. Good. Then they leave three developers as owners of the app registration, two operations engineers as owners of the enterprise application, and a shared service account with broad rights because "someone needs to maintain it."

On paper, the app permission set looks defensible. In reality, the control plane around the app is loose.

That is not least privilege. That is narrow API permission sitting inside broad object control.

The better question is not only "what Graph permissions does the app have?" The better question is: "who can change the identity that has those permissions?"

If the answer is a long list of permanent owners, the app is overprivileged even if the Graph permission list looks elegant.

Scoped Entra roles are the missing design tool

This is where Microsoft Entra role scoping becomes much more interesting than most app teams realize.

Microsoft Entra roles are usually assigned at tenant scope, but Microsoft also supports assigning roles at narrower scopes, including application registrations and administrative units. Microsoft describes the resource a role assignment applies to as the scope, and supports restricted scope for built-in and custom roles (Assign Microsoft Entra roles).

For app registrations, the model is direct. Instead of granting someone tenant-wide Application Administrator, you can assign a relevant built-in or even custom role scoped to a single app registration. Microsoft describes this as a way to let a principal update credentials and basic properties of a single app without granting tenant-wide application administration (Assign roles with app registration scope).

The Graph shape is simple:

POST https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments
Content-Type: application/json

{
  "@odata.type": "#microsoft.graph.unifiedRoleAssignment",
  "principalId": "<principal-object-id>",
  "roleDefinitionId": "<role-definition-id>",
  "directoryScopeId": "/<application-object-id>"
}
Enter fullscreen mode Exit fullscreen mode

That directoryScopeId value matters. For app registration scope, /<id> means the principal can manage that Microsoft Entra object. Microsoft explicitly contrasts that with /administrativeUnits/<id>, which means the principal can manage the members of the administrative unit based on the assigned role (Assign roles with app registration scope).

That distinction is the whole architecture in miniature. Same role-assignment resource. Different scope semantics. Different blast radius.

Administrative units are not just for humans

Administrative units are usually explained as a delegation feature for helpdesk and regional administration. That is true, but incomplete.

Microsoft documents that Entra roles can be assigned with administrative-unit scope so the role permissions apply only when managing members of that administrative unit, not tenant-wide settings or unrelated directory objects (Assign roles with administrative unit scope).

For example, a User Administrator scoped to the "Germany Employees" administrative unit can manage users in that administrative unit without becoming User Administrator for the whole tenant, subject to the role's supported administrative-unit behavior and restrictions (Administrative-unit role scope).

That already matters for humans. It matters even more for automation.

If you are building an onboarding app for five subsidiaries, why should the app's runtime identity be able to manage every user in the tenant? If the work is limited to five administrative units, the role assignment should be limited to those administrative units.

This is the balance shift: Graph permissions are always tenant-wide. Entra role assignments can be scoped.

The widely unknown pattern: service principal plus administrative-unit scope

Now for the part that is still too obscure.

You can assign a Microsoft Entra role to a service principal at administrative-unit scope.

This is not how most portal walkthroughs teach the feature. Most examples show a user as the principal. Some examples mention groups. The architecture pattern for app identities is still widely unknown. But the Graph contract supports the pieces.

The unifiedRoleAssignment resource represents a role definition assigned to a principal at a scope, and Microsoft lists supported principals as users, role-assignable groups, and service principals (unifiedRoleAssignment resource type). The create-role-assignment API documents directoryScopeId formats for the directory provider, including / for tenant-wide scope, /administrativeUnits/{administrativeunit-id} for administrative-unit scope, and /{application-objectId} for application scope (Create unifiedRoleAssignment).

Put those two facts together and the payload becomes obvious:

POST https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments
Content-Type: application/json

{
  "@odata.type": "#microsoft.graph.unifiedRoleAssignment",
  "principalId": "<service-principal-object-id>",
  "roleDefinitionId": "<role-definition-id>",
  "directoryScopeId": "/administrativeUnits/<administrative-unit-id>"
}
Enter fullscreen mode Exit fullscreen mode

That assignment says: this service principal has this Entra role, but only for the members of this administrative unit, subject to the role's administrative-unit support.

In practice, there is one more wrinkle engineers need to know. Microsoft documents that service principals and guest users do not receive directory read permissions by default, and service principals using an administrative-unit-scoped role assignment need corresponding read permissions, commonly Directory Readers at tenant scope or another role that includes the necessary read permissions (Service principals and administrative-unit scoped role assignments).

That tenant-scope read dependency annoys people at first. It should not. Read capability and write authority are different risks. A service principal might need enough tenant-level read to resolve directory objects while its actual administrative authority remains scoped to one administrative unit. That is still a materially better design than granting a tenant-wide write role because the app needs to manage one slice of the directory.

The pattern is not theoretical. It is validated, battle-tested, and still underused.

Why this changes Graph automation design

Once you see the two planes, the design review changes.

For a delegated app, you ask:

  • What Microsoft Graph delegated permission does the client need?
  • Which Entra role must the signed-in user hold?
  • Can that role be scoped to an administrative unit or application registration instead of the tenant?
  • Who owns the app registration and enterprise application?

For an app-only automation identity, you ask:

  • What Microsoft Graph application permission does the service principal need?
  • Can the operation be represented by an Entra role assignment instead of a broad Graph application permission?
  • Can that Entra role be scoped to an administrative unit or specific (application) object?
  • Does the service principal have only the directory read permissions needed to make that scoped role usable?
  • Who can add credentials or owners to the app and service principal?

That last question is not an afterthought. It is often the difference between least privilege and theater.

A practical model for app teams

Here is the model I would use with app and platform engineers.

First, choose the narrowest Microsoft Graph permission that matches the API operation. Use User.Create if the app creates users. Use Application.ReadWrite.OwnedBy if the app only needs to manage applications it owns. Avoid directory-wide permissions unless the operation truly has no narrower resource-specific permission (Microsoft Graph permissions reference).

Second, decide whether the app needs delegated or app-only access. In delegated mode, remember that Graph evaluates the client permission and the user's own authority. In app-only mode, remember that the app is the actor, so application permissions or role assignments must be treated as production privilege (Microsoft Graph permission types).

Third, scope Entra roles as aggressively as the business process allows. If the operator manages one app, use app-registration scope. If the automation manages one population, use administrative-unit scope. If the app really needs tenant-wide scope, make that an explicit exception, not the default (Assign Microsoft Entra roles at different scopes).

Fourth, treat owners as privileged. Application owners and enterprise application owners should be reviewed like role assignments, because they can control the object that holds the app's permissions and credentials (Default user permissions and ownership permissions).

Finally, keep the object model visible. The app registration and the service principal are not interchangeable. The app registration is the template. The service principal is the tenant-local instance that controls what the app can actually do in that tenant (Application and service principal relationship).

The sentence to bring to your next design review

Least privilege for Microsoft Graph is not "we picked a smaller Graph permission."

That is a good start. It is not the finish line.

Least privilege for Microsoft Graph means the client has the narrowest Graph permission, the actor has the narrowest Entra authority, the role assignment has the narrowest scope, and the app object has the narrowest ownership model.

Miss any one of those, and your beautiful permission list can still hide a control-plane problem.

The industry is finally getting better Microsoft Graph permission granularity. Now app teams need to get equally serious about the Entra control plane around those permissions.

Because the app that can touch your directory is only as least-privileged as the people and identities that can control the app.

References

Top comments (0)