Friday, 2 January 2026

Securing Enterprise REST APIs in .NET with Azure AD: Principles, Pitfalls, and Production-Ready Patterns

 Enterprise APIs don’t become “secure” because they use HTTPS and JWTs. Real-world security is a set of principles, implemented as repeatable defaults, backed by operational controls (monitoring, rotation, scanning), and hardened against misconfiguration.

This article walks through the security principles you should follow for an enterprise REST API built with ASP.NET Core (.NET 8) and Azure AD (Microsoft Entra ID) — with clean code examples you can adapt directly.

1) Security principles (the enterprise baseline)

These principles shape every design choice:

  • Default deny: Every endpoint requires authentication unless explicitly opened.
  • Least privilege: Users/services only get the permissions they need (scopes/app roles).
  • Defense in depth: Multiple layers (gateway + app + data layer + monitoring).
  • Fail securely: Errors should not leak implementation details; auth failures should be consistent.
  • Reduce attack surface: Disable unnecessary endpoints/features; lock down CORS/metrics.
  • Protect secrets: No secrets in source control; store in Key Vault; rotate.
  • Observe & audit: Structured logs, correlation IDs, audit trails.
  • Validate input, encode output: Prevent injection, deserialization abuse, and data leakage.
  • Secure dependencies: Patch cadence + scanning + SBOM.
  • Operational readiness: Rate limiting, DDoS protection, incident response, and repeatable config.

2) Azure AD authentication done right (JWT bearer)

Use Microsoft.Identity.Web so you inherit best practices and avoid hand-rolling JWT validation.

Install packages

dotnet add package Microsoft.Identity.Web
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

appsettings.json (no secrets here)

{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "<tenant-guid>",
"ClientId": "<api-app-registration-client-id>"
}
}

> For APIs, you typically do not need ClientSecret unless you call downstream APIs on behalf of the app/user.

Program.cs — authentication + strict defaults

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

var builder = WebApplication.CreateBuilder(args);

// Never log identity PII outside dev
IdentityModelEventSource.ShowPII = builder.Environment.IsDevelopment();

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

builder.Services.AddAuthorization();

builder.Services.AddControllers();

var app = builder.Build();

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

3) Authorization: use scopes/app roles + “default deny”

Authentication answers: “Who are you?”

Authorization answers: “What are you allowed to do?”

Enterprise APIs should not rely on route string checks or ad-hoc middleware exceptions. Instead:

  • Require auth for all endpoints by default
  • Use scopes/app roles for fine-grained access
  • Only explicitly allow anonymous for health/public endpoints

Global “require authenticated user” (Fallback policy)

builder.Services.AddAuthorization(options =>
{
// Default deny: anything not explicitly allowed requires authentication
options.FallbackPolicy = options.DefaultPolicy;
});

Now any controller/action without [AllowAnonymous] requires auth.

Scope-based authorization (recommended)

If your API uses scopes like api://<client-id>/read:

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireReadScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scp", "read"); // "scp" claim for delegated permissions
});
});

Use it in controllers:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
[HttpGet]
[Authorize(Policy = "RequireReadScope")]
public IActionResult GetReports() => Ok(new { Status = "ok" });
}

App role–based authorization (service-to-service friendly)

For application permissions you typically use roles claim:

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("roles", "Admin");
});
});

4) CORS: don’t use AllowAnyOrigin() in production

CORS is not “security” by itself (it’s a browser enforcement), but misconfigured CORS is a common enterprise audit finding and can amplify token misuse.

builder.Services.AddCors(options =>
{
options.AddPolicy("FrontendOnly", policy =>
{
policy
.WithOrigins("https://app.company.com")
.AllowAnyHeader()
.AllowAnyMethod()
.WithExposedHeaders("X-Request-ID");
});
});

app.UseCors("FrontendOnly");

Rules of thumb

  • Keep origins explicit
  • Avoid WithExposedHeaders(“*”)
  • Don’t allow credentials unless you truly need them

5) Secure error handling (don’t leak internals)

A production API should return safe messages, while logs retain details.

app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";

var safeMessage = app.Environment.IsDevelopment()
? "Internal error (dev mode). Check logs for details."
: "Internal error occurred. Please contact support.";

await context.Response.WriteAsync($@"{{""error"":""{safeMessage}""}}");
});
});

Avoid

  • Returning exception messages to clients in production
  • Returning stack traces
  • Logging tokens/claims/PII

6) Secrets management: Key Vault + environment variables (never git)

Enterprise rule: If it’s a secret, it must be rotatable and must not be in source control.

Recommended:

  • Azure Key Vault (prod)
  • Environment variables / managed identity
  • Secret scanning in CI
  • Rotation procedures

If you must access Key Vault:

// Requires Azure.Identity + Key Vault configuration (example conceptually)
builder.Configuration.AddEnvironmentVariables();
// In Azure: configure Key Vault references or add AzureKeyVault provider

7) Protect operational endpoints (/metrics, admin routes)

Prometheus metrics and management endpoints are often unintentionally public.

Two good patterns:

  • Put them behind network controls (private endpoint / internal ingress)
  • Or require authentication / a shared secret (as a fallback)

Example: require auth for /metrics

app.MapGet("/metrics", () => Results.Text("..."))
.RequireAuthorization(); // simplest: same auth pipeline as API

If you must use a secret header (less ideal than auth):

app.MapGet("/metrics", (HttpContext ctx, IConfiguration cfg) =>
{
var expected = cfg["Ops:MetricsKey"];
var incoming = ctx.Request.Headers["X-METRICS-KEY"].ToString();
if (incoming != expected) return Results.Unauthorized();
return Results.Text("...");
});

8) Rate limiting (protect availability)

Availability is a security property. Add rate limiting at gateway + app.

using System.Threading.RateLimiting;

builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", limiterOptions =>
{
limiterOptions.Window = TimeSpan.FromSeconds(10);
limiterOptions.PermitLimit = 100;
limiterOptions.QueueLimit = 0;
});
});

app.UseRateLimiter();

app.MapControllers().RequireRateLimiting("api");

9) Input validation and injection prevention

Enterprise APIs routinely fail here — not at auth.

  • Validate request models (length, ranges, allowed values)
  • Avoid dynamic SQL and string concatenation in queries
  • Parameterize everything (Dapper/EF Core)
  • Avoid unsafe deserialization settings

Example: model validation

public sealed class CreateUserRequest
{
[Required, EmailAddress]
public string Email { get; set; } = default!;

[Range(1, 100)]
public int DepartmentId { get; set; }
}

In controllers, rely on [ApiController] to automatically return 400 for invalid models.

10) Secure logging: correlation IDs, structured logs, no tokens

  • Add a request ID (X-Request-ID) and include it in all logs
  • Log who did what, but never log:

=> Authorization headers
=> raw JWTs
=> secrets
=> PII unless necessary and approved

Example: simple correlation header

app.Use(async (ctx, next) =>
{
var requestId = ctx.Request.Headers.TryGetValue("X-Request-ID", out var incoming)
? incoming.ToString()
: ctx.TraceIdentifier;

ctx.Response.Headers["X-Request-ID"] = requestId;
await next();
});

11) Security headers & HTTPS

  • Enforce HTTPS redirection
  • Enable HSTS in production
    HSTS (HTTP Strict Transport Security) is a security policy that forces web browsers to use only secure HTTPS connections for a website, preventing attackers from downgrading connections to unsecure HTTP (SSL stripping) and protecting against man-in-the-middle attacks.
  • Consider adding headers (depending on hosting/gateway)
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();

12) Dependency and supply-chain hygiene

Enterprise minimum:

  • Dependabot/Renovate
  • dotnet list package — vulnerable
  • SBOM generation
  • Locked down build pipeline permissions

Example command:

dotnet list package --vulnerable

13) Production checklist

Use this as a release gate:

  1. Auth
  • Default deny enabled (fallback policy)
  • Tokens validated by Microsoft Identity Web
  • ShowPII disabled outside dev

2. AuthZ

  • Scopes/app roles enforced on sensitive endpoints
  • No route-string-based bypass logic

3. CORS

  • Explicit allowed origins in prod

4. Errors

  • No stack traces or exception messages returned in prod

5. Secrets

  • No secrets in repo; Key Vault + rotation done
  • Secret scanning enabled

6. Ops endpoints

  • /metrics not public
  • Admin endpoints locked down

7. Abuse protection

  • Rate limiting enabled
  • Gateway/WAF in front

8. Data layer

  • Parameterized queries only
  • Input validation on all externally controlled fields

9. Observability

10. Dependencies

  • Vulnerability scanning + patching cadence

Closing thoughts

A secure enterprise API is mostly about secure defaults and removing foot-guns:

  • stop relying on “special-case routes”
  • protect internal endpoints
  • never ship with permissive CORS
  • never commit secrets
  • enforce authorization consistently and centrally

Hope you like the articles. Happy secured REST APIs programming.