Azure AD (Entra ID) + .NET: secure auth the right way

Sep 12, 2025
azureentra-idazure-adoauth2
0

Microsoft Entra ID (formerly Azure AD) is the identity backbone for Microsoft 365 and Azure. This post shows practical patterns to protect .NET APIs and web apps with OpenID Connect and OAuth 2.0 using MSAL.

What you’ll build

  • A protected ASP.NET Core Web API that validates JWTs from Entra ID
  • A web client that signs users in and calls the API with an access token
  • Role/scope-based authorization and production hardening tips

App registrations (portal)

Create two app registrations in Entra ID:

  1. API app
  • Expose an application ID URI, e.g. api://elysiate-api
  • Expose scopes: access_as_user (or granular scopes like orders.read)
  1. Client app
  • Grant API permissions to the scopes you exposed
  • Allow accounts in your tenant (or multitenant if needed)
  • Configure redirect URI (e.g. https://localhost:5173/signin-oidc for web, or SPA reply URLs for SPA)

Protecting a .NET API

Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication().AddJwtBearer("AzureAd", options =>
{
    options.Authority = builder.Configuration["AzureAd:Authority"]; // https://login.microsoftonline.com/{tenantId}/v2.0
    options.Audience  = builder.Configuration["AzureAd:Audience"];  // api://elysiate-api
    options.TokenValidationParameters.ValidateIssuer = true;
});

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("OrdersRead", policy => policy.RequireClaim("scp", "orders.read"));
});

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

app.MapGet("/api/orders", () => Results.Ok(new[] { "A123", "B456" }))
   .RequireAuthorization("OrdersRead");

app.Run();

appsettings.json:

{
  "AzureAd": {
    "Authority": "https://login.microsoftonline.com/<tenant-id>/v2.0",
    "Audience": "api://elysiate-api"
  }
}

Signing in and calling the API from a web app

Use MSAL.NET (server-rendered) or MSAL.js (SPA). Example with server-rendered MVC:

builder.Services
  .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
  .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAdWeb"))
  .EnableTokenAcquisitionToCallDownstreamApi()
  .AddDownstreamWebApi("ElysiateApi", builder.Configuration.GetSection("ElysiateApi"))
  .AddInMemoryTokenCaches();

app.MapGet("/call-api", async (IDownstreamWebApi api) =>
{
    var response = await api.GetForUserAsync<IEnumerable<string>>("ElysiateApi", options =>
        options.RelativePath = "/api/orders");
    return Results.Ok(response);
}).RequireAuthorization();

Configuration:

{
  "AzureAdWeb": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<tenant-name>.onmicrosoft.com",
    "TenantId": "<tenant-id>",
    "ClientId": "<client-app-id>",
    "ClientSecret": "<secret>",
    "CallbackPath": "/signin-oidc"
  },
  "ElysiateApi": {
    "BaseUrl": "https://localhost:5001",
    "Scopes": "api://elysiate-api/orders.read"
  }
}

SPA scenario (React/MSAL.js) outline:

const msal = new PublicClientApplication({
  auth: { clientId: "<client-id>", authority: "https://login.microsoftonline.com/<tenant-id>" }
});
await msal.loginPopup({ scopes: ["api://elysiate-api/orders.read"] });
const token = await msal.acquireTokenSilent({ scopes: ["api://elysiate-api/orders.read"] });
await fetch("/api/orders", { headers: { Authorization: `Bearer ${token.accessToken}` } });

Roles vs scopes

  • Scopes (scp claim): delegated permissions for user-to-API access.
  • App roles (roles claim): used for role-based access (app or user assignments). Define in app manifest and check via RequireRole.

Multitenancy

  • Set client app to Accounts in any organizational directory and validate tid/issuer in API if you need to restrict tenants.
  • Use per-tenant config for allowed domains and conditional policies.

B2C option

For consumer apps, use Microsoft Entra ID B2C. It issues tokens the same way but uses user flows/custom policies.

Production hardening checklist

  • Use v2.0 endpoints and validate issuer/tenant
  • Enforce HTTPS and SameSite/Secure cookies
  • Rotate client secrets or use certificates / Managed Identity
  • Cache metadata (OpenID configuration) and JWKS
  • Narrow scopes; least-privileged access
  • Add conditional access/MFA where appropriate

With solid app registrations and MSAL, Entra ID gives you enterprise-grade auth that plugs cleanly into .NET with minimal code.