Skip to content

Securing C# APIs with Microsoft Entra ID app roles

Practical tutorial: ASP.NET Core, Microsoft Entra ID, JWT, app roles and authorization policies. Signing in successfully does not grant permission to call every API endpoint. Authentication identifies the caller; authorization determines what that caller is allowed to do. This walkthrough builds a small .NET API with distinct read and write permissions.

1. The architecture and the trust boundary

We will define Api.Reader for reading orders and Api.Editor for writing them. An app registration represents the protected API and declares its roles. The corresponding enterprise application is used to assign roles to users, groups or calling applications. The client obtains an access token for this API. ASP.NET Core validates that token before checking the caller’s roles on protected routes.

User or workload → Microsoft Entra ID → Access token
                                      ↓
                                ASP.NET Core API
                                1. Validate token
                                2. Enforce app roles
                                3. Allow operation

Do not accept an access token issued for Microsoft Graph or another resource as authorization to access your API. An ID token used to sign in a user is not an API access token.

2. Prerequisites

  • A Microsoft Entra tenant and sufficient permissions to create an app registration and app roles.
  • A supported version of the .NET SDK capable of running ASP.NET Core Web APIs.
  • A test user and optionally a separate client application for service-to-service tests.
  • The tenant ID and the API application’s client ID.

3. Register the API and declare roles

  1. In the Microsoft Entra admin center, open Entra ID → App registrations → New registration.
  2. Name the application Demo Orders API and deliberately choose its supported account types. Single-tenant is usually easier to reason about for an internal API.
  3. Copy the Directory (tenant) ID and Application (client) ID.
  4. Under Expose an API, configure an application ID URI when required by your client integration.

Under App roles, create role values Api.Reader and Api.Editor. The role Value is what you expect in the roles token claim, not merely a friendly display label. Choose the appropriate allowed member types: Users/Groups, Applications or both. These member types correspond to different authorization flows.

4. Assign roles to users and applications

Open Entra ID → Enterprise applications → Demo Orders API → Users and groups. Assign Api.Reader to your read-only test user and Api.Editor to a user allowed to write. Group assignments can depend on tenant licensing, and nested group membership is not automatically honored. After changing a role assignment, acquire a newly issued token to test the result: an already-issued access token does not update in place.

For a client-credentials workload, the calling application’s registration instead requests your API’s application permission under API permissions. After the required admin consent, the workload can obtain a token containing its assigned application role. This differs from delegated scopes used when an application acts on behalf of a signed-in user.

5. Create the .NET Web API

dotnet new webapi -n EntraRolesApi
cd EntraRolesApi
dotnet add package Microsoft.Identity.Web

Configure the API registration values in appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "YOUR-TENANT-ID",
    "ClientId": "YOUR-API-CLIENT-ID"
  }
}

These identifiers are not secrets. The Microsoft Identity Web package configures JWT bearer validation using these settings. For multi-tenant APIs or nonstandard audiences, review and tailor issuer and audience validation rather than weakening token security.

6. Enforce authorization in ASP.NET Core

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("OrdersRead",
        policy => policy.RequireAuthenticatedUser()
                        .RequireRole("Api.Reader", "Api.Editor"));
    options.AddPolicy("OrdersWrite",
        policy => policy.RequireAuthenticatedUser()
                        .RequireRole("Api.Editor"));
});

var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/api/orders", () =>
    Results.Ok(new[] { new { Id = "10042", Status = "Open" } }))
   .RequireAuthorization("OrdersRead");

app.MapPost("/api/orders", () =>
    Results.Accepted("/api/orders/10043"))
   .RequireAuthorization("OrdersWrite");

app.Run();

This POST handler is deliberately an authorization demonstration: it does not save an order or implement business processing. The read policy accepts either role; the write policy requires Api.Editor. Ensure the role values match the claims emitted by your tenant. If your application uses custom claim mappings, verify the role-claim type used by the authorization middleware.

7. Why 401 is different from 403

401 Unauthorized usually means the request could not be authenticated: a token is missing, expired, has the wrong issuer or does not target the API. 403 Forbidden means the caller was authenticated but lacks the required permission. Hiding an action in the UI is never a substitute for server-side authorization: HTTP endpoints can be called directly.

8. Test matrix: confirm the policy actually works

  1. Call a protected endpoint without a bearer token; access must fail.
  2. Submit a token with the wrong audience; it must be rejected.
  3. Call the write endpoint with a token lacking the required role; access must fail.
  4. Test a reader token against both read and write endpoints.
  5. Test an editor token and verify it is allowed by the intended policies.
  6. Remove or change a role assignment, acquire a fresh token, and verify the new authorization result.

Inspect claims only in an appropriately secured development setting. Do not paste raw production tokens into support tickets or application logs.

9. Common problems and remedies

  • Valid token but 403: The role was not assigned, the token predates the assignment, or the role value is different from the policy string.
  • Unexpected 401: Investigate the audience, tenant, token type and authorization header.
  • Missing roles in a workload token: Check the app role’s allowed member types, the client’s API permissions and admin consent.
  • Too much access: Split read, write and administrative operations into distinct policies.
  • Authorization only in frontend code: Apply policies directly to protected API routes.

10. Production-readiness checklist

Require HTTPS, validate signatures, issuer and audience, enforce roles or scopes on every protected operation, and keep role assignments auditable. Prefer managed identities or certificate-based workload authentication when appropriate rather than long-lived client secrets. Apply least privilege and document revocation. Global application roles may not be enough for row-level or tenant-level business permissions; also validate access to the specific resource being requested.

Official Microsoft documentation

Discussion (0)

No comments yet. Be the first to contribute.

Leave a comment