Protecting APIs with Microsoft Entra ID

Published by Mika Berglund on

Protecting APIs with Microsoft Entra ID

In this article I describe a practical approach to protecting APIs with Entra ID. The goal is to configure your app registrations so that your API app gets the information it needs to make solid authorization decisions for each request. Entra ID provides that information through app roles and scopes, and your API app can rely on these when the setup is clean.

I use two simple applications named Web app and API app to keep the examples concrete. This pattern is common and shows how an application can act on behalf of a user. In that scenario the API app must understand what the user can do and what the application can do for the user. Entra ID supports this model out of the box when the app registrations match the requirements.

This article walks through the structure behind protecting APIs with Entra ID and shows how the OAuth authorization flows enable it. The result works the same way whether a Web app, a background service, or an AI agent calls your API.

Remember also to have a look at my other Microsoft Entra ID articles.

Overview

When you build an API that serves data or functionality to different applications, you want the API app to get the information it needs to make solid authorization decisions for every request. In Microsoft Entra ID, this information comes from app roles and scopes. When structured well, they give the API app a predictable model for deciding whether a request should be allowed or denied.

Here is how the flow works:

  1. The Web app asks Entra ID for an access token with one or more scopes.
  2. Entra ID returns a token that contains both the scopes the user granted and the user’s assigned app roles.
  3. The Web app calls the API app and includes the access token.
  4. The API app verifies that Entra ID issued the token.
  5. The API app reads the app roles and scopes from the token and derives what is allowed for this request.
  6. The API app performs the action or denies it.

This structure gives the API app a clear and consistent way to enforce authorization using only information from the token. It is also a practical foundation for protecting APIs with Entra ID in real systems.

What Is the Difference Between App Roles and Scopes

App roles and scopes both describe permissions in Entra ID, but they serve different purposes. App roles define what a user or an application can do in the API app. Each role represents one action or a set of actions. When you assign a role to a user, a group, or an application, you set the limits for what that identity can do.

Scopes describe what the Web app may do on the user’s behalf. The Web app requests scopes during the OAuth flow, and the user can grant or deny them. Inside the API app, scopes tell you what permissions the user has delegated to the Web app for this specific call.

The two signals work together. App roles set the hard limits for what the user or application is allowed to do. Scopes narrow down which of those allowed actions the Web app can take for the user. Scopes never expand permissions.

App roles answer: What is this user or application allowed to do at all?
Scopes answer: What has the user allowed this Web app to do for them in this call?

When the request reaches the API app, the user must hold the right role and the caller must present the right scope. This pattern forms the core of protecting APIs with Entra ID.

Creating the App Registrations

Everything starts with the app registrations. The API app defines the permission model with its app roles and scopes. The Web app asks for those permissions when it calls the API. Both registrations must line up so that Entra ID can issue tokens that the API app can trust.

In the next sections we create the two registrations and look at the settings that matter for authorization.

API App Application Registration

We start by creating the app registration for the API app. Then we add app roles to it. These roles define what the user or the calling application can do. Each role represents an action or a set of actions. You can choose whether a role can be assigned to users and groups, to applications, or to both. Make this choice early, since it shapes how the API app is meant to be used.

Next, expose the API. Set the Application ID URI to identify the API app as a resource. Then define at least one scope. In this example we add the user_impersonation scope. Calling applications request this scope when they want to act on behalf of a user with the permissions that the user already has, i.e. the app roles the user has been assigned to through direct assignment or group membership.

Add any additional scopes the API app needs. Scopes describe what the calling application wants to do for the user. The API app uses them to understand the caller’s intent.

The API app holds the full definition of its permission model. This keeps the structure predictable and sets the baseline for the Web app registration.

Web App Application Registration

Create a second app registration for the Web app. This registration represents the Web app that calls the API app on behalf of the user. Configure the platform settings so the Web app can sign users in and request tokens from Entra ID.

Next, give the Web app access to the API app. Add the delegated permission that corresponds to the user_impersonation scope you created earlier. The Web app requests this scope during the OAuth flow when it needs to act for the user. The user can grant or deny this permission, depending on how the application is used. You can also add the Web app as a trusted application to the API app. Users are not asked for consent to trusted applications.

The Web app does not define app roles. All roles belong to the API app, because all actions that need protection happen in the API app. The Web app is just the UI to the functionality exposed by the API app. The Web app only asks for the scopes it needs, and it relies on Entra ID to include the user’s app roles in the access token that it receives.

With both app registrations in place, we can move on to assigning app roles to users, groups, and applications.

Assigning App Roles to Users, Groups, and Applications

Once the API app defines its app roles, you can start assigning them to the identities that need access. You can assign a role directly to a user, to a security group, or to an application. The choice depends on how you want to manage access.

Assigning a role directly to a user gives that user the permissions described by the role. This works for small setups, but it does not scale well. Assigning roles to security groups works better. You place users into groups that represent your access model, and the API app receives the user’s effective roles in the access token.

Some roles are intended only for applications. These roles describe what an application can do when it calls the API as itself. Assign the role directly to the calling application.

When the Web app calls the API on behalf of a user, the access token includes the roles the user holds, either through direct assignments or group memberships. When a background service calls the API as itself, the token includes the roles assigned to that application. In both cases, the API app receives the information it needs to understand what the caller is allowed to do.

Authorization Inside the API App

When a request reaches the API app, it carries an access token from Entra ID. That token contains app roles, scopes, and other claims. The API app uses these to decide what the caller is allowed to do and should rely on the token as much as possible. This model is a core part of protecting APIs with Entra ID.

How to Use App Roles for Authorization

App roles describe what a user or an application can do in the API app. These roles map directly to the functionality the API exposes. When the Web app calls on behalf of a user, the token contains the user’s roles. When a background service calls as itself, the token contains the roles assigned to that application. The API app checks these roles to see if the caller may perform the requested action.

You should design your app roles to match how you talk about access in the system. If the API exposes operations for reading and writing data, then roles should reflect those capabilities. The API app then checks for the required role before it performs an operation.

How to Use Scopes for Delegated Permissions

Scopes describe what the Web app may do on the user’s behalf. During the OAuth flow, the Web app asks for one or more scopes, and the user can grant or deny them. When the user grants a scope, the user delegates a subset of their permissions to the Web app.

Inside the API app, scopes tell you what the user has allowed the Web app to do in this specific call. Scopes do not change what the user is allowed to do in general. They only limit what the Web app can do for the user. The API app uses the scopes in the token to understand how far the Web app may act on the user’s behalf.

How to Derive Allowed Actions from App Roles and Scopes

The API app derives the allowed actions by applying the scopes to the user’s roles. The user must hold the role for the action. The Web app must hold a scope that lets it act for the user for that same action.

App roles and scopes must have unique names inside the API app’s registration, so you cannot name a scope the same as a role. Because of this, the API app needs a simple way to map scopes to the roles that apply for a request. Entra ID does not provide a built-in mapping mechanism. A static mapping in the API app usually works well. App roles and scopes change rarely, so a static structure does not add much maintenance.

For example, a scope named Profile.Manage.My could map to several app roles, such as Account.Read.My, Account.Write.My, ContactInfo.Read.My, ContactInfo.Write.My, and ContactInfo.Delete.My. When a request arrives, the API app looks at the scopes included in the access token and uses its mapping to determine which app roles apply for this call. It then compares that set with the roles the user actually holds. Any role in the mapping that the user does not have is not included in the effective permissions for this request.

The API app checks both signals for each request and then performs or denies the action.

A Practical Example: A CRM Application Protecting Its APIs with Entra ID

To illustrate how app roles and scopes work together, let’s look at how this could be implemented in a simple CRM application. The CRM system exposes its functionality through an API app. A Web app provides the user interface and calls the API on behalf of the user.

The API app includes operations for managing accounts, contacts, and proposals. These operations must be protected so that users and applications can access only what they are allowed to.

Example App Roles

The API app defines these app roles:

  • Accounts: Accounts.Read.Team, Accounts.Read.All, Accounts.Write.Team, Accounts.Write.All
  • Contacts: Contacts.Read.My, Contacts.Read.Team, Contacts.Read.All, Contacts.Write.My, Contacts.Write.Team, Contacts.Write.All
  • Proposals: Proposals.ReadWrite.Team, Proposals.ReadWrite.All

These roles reflect the functionality the CRM API exposes and follow a simple constraint model:

  • My: actions on objects owned by the user
  • Team: actions within the user’s team or department
  • All: actions across the entire tenant

This keeps the permission model predictable while still giving the CRM system enough flexibility for everyday scenarios. Users gain access through direct role assignments or group membership. Background services can also be assigned roles when calling the API as themselves.

Example Scopes

The API app exposes the following scopes:

  • CRM: Crm.Read.My, Crm.Read.Team, Crm.Read.All, Crm.ReadWrite.My, Crm.ReadWrite.Team, Crm.ReadWrite.All
  • Sales: Sales.ReadWrite.Team, Sales.ReadWrite.All

The CRM scopes map to the Accounts and Contacts roles:

  • Crm.Read.* maps to the read roles for Accounts and Contacts with the same suffix.
  • Crm.ReadWrite.* maps to both read and write roles for Accounts and Contacts with the same suffix.

The Sales scopes map to the Proposals roles:

  • Sales.ReadWrite.Team maps to Proposals.ReadWrite.Team.
  • Sales.ReadWrite.All maps to Proposals.ReadWrite.All.

At runtime, the API app uses this mapping together with the user’s app roles in the access token. The scopes tell the API app what the user has allowed the Web app to do. The app roles tell the API app what the user is actually allowed to do. The intersection of the two becomes the effective permissions for the request.

End-to-End Example

  1. The user signs in to the Web app.
  2. The Web app requests an access token for the API app and includes Crm.ReadWrite.Team.
  3. Entra ID issues a token containing the scopes the user granted and the app roles assigned to the user.
  4. The Web app calls the CRM API and includes the access token.
  5. The API app verifies that Entra ID issued the token.
  6. The API app looks at the user’s app roles to understand what the user is allowed to do.
  7. The API app looks at the scopes in the token to understand what the user has allowed the Web app to do for them.
  8. The API app uses its internal mapping to translate Crm.ReadWrite.Team into the corresponding Accounts and Contacts roles:
    Accounts.Read.Team, Accounts.Write.Team, Contacts.Read.Team, Contacts.Write.Team.
    It then intersects this set with the roles the user actually holds.
  9. The API app derives the effective permissions for the request.
  10. The API app performs the action or denies it.

This example shows how roles and scopes work together in a real CRM system. The app roles define what the user or an application may do. The scopes define what the user allows the Web app to do on their behalf. The API app derives the effective permissions by applying one to the other and then makes a clear authorization decision.

Summary

Protecting APIs with Entra ID works best when the Web app and the API app follow a clear structure. The API app should rely on the access token as much as possible. The token carries what the API needs for authorization, including the user’s app roles and the scopes the user has granted to the Web app.

App roles define what the user or an application is allowed to do in the API app. Scopes define what the user allows the Web app to do on their behalf in a specific call. Roles and scopes must have unique names, and the API app must implement the mapping between them. The API app then derives the effective permissions by intersecting the user’s app roles with the roles implied by the granted scopes. This gives the API app predictable and secure authorization decisions without extra custom layers.

A clean set of app registrations, a simple mapping model, and a consistent authorization approach inside the API provide a solution that is easy to understand, easy to maintain, and flexible enough for most real systems. This pattern gives you a solid and repeatable way of protecting APIs with Entra ID in real applications.

References


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *