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:
- API app
- Expose an application ID URI, e.g.
api://elysiate-api
- Expose scopes:
access_as_user
(or granular scopes likeorders.read
)
- 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 validatetid
/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.