Securing ASP.NET Core APIs: Best Practices for 2026

ASP.NET Core APIs power most of what we host — partner integrations, mobile backends, SPA backends, microservice meshes, B2B data exchange. The defensive baseline has shifted in 2026: post-quantum TLS is shipping in .NET 10 LTS, passkey-first auth is mainstream, and API supply-chain attacks now outnumber injection bugs as the top OWASP vector. This is the production playbook we apply to every API on Adaptive's ASP.NET Core hosting, with 10 strategies, code, and the failure mode each one prevents.

.NET 10LTS — updated for

OWASPAPI Top 10 2023+

FREESSL + WAF on every plan

The 2026 API threat landscape, at a glance

Broken Object Level Authorization

#1 on OWASP API Top 10. /orders/123 → /orders/124, see someone else's data.

🔴 Critical

Broken Authentication

Long-lived tokens, missing PKCE, hand-rolled JWT validation.

🟠 High

Excessive Data Exposure

Returning the full entity instead of a DTO. Hidden fields leak through.

🟠 High

Resource Exhaustion

No rate limit + expensive endpoints = trivial DoS surface.

🟠 High

Supply-Chain Attack

Compromised NuGet package, malicious GitHub Action, typo-squat dep.

🟡 Medium

Misconfigured CORS

AllowAnyOrigin() in production. Common in projects bootstrapped from dev templates.

Six of the top eight 2026 API breaches we've reviewed traced back to one of these patterns. The strategies below address each.

Quick reference: the 10 strategies

  • JWT Validation Done Right (or OIDC)

Hand-rolled JWT validation that skips audience checks, accepts alg: none, or trusts the kid header to load arbitrary keys. Every one of these is in production today.

Use the built-in Microsoft.AspNetCore.Authentication.JwtBearer handler. Configure every parameter explicitly — defaults are not enough:

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)

.AddJwtBearer(options =>

{

options.Authority = "https://login.example.com";

options.Audience = "api.example.com";

options.RequireHttpsMetadata = true;

options.TokenValidationParameters = new TokenValidationParameters

{

ValidateIssuer = true,

ValidateAudience = true,

ValidateLifetime = true,

ValidateIssuerSigningKey = true,

ClockSkew = TimeSpan.FromSeconds(30), // not 5 minutes

RequireSignedTokens = true,

RequireExpirationTime = true,

// Pin the algorithm; alg=none and HS256-key-confusion attacks are real.

ValidAlgorithms = new[] { "RS256", "PS256", "ES256" }

};

// Strip the access token from logs and metrics.

options.Events = new JwtBearerEvents

{

OnMessageReceived = ctx => { ctx.Request.Headers.Remove("Authorization"); return Task.CompletedTask; }

};

});

For greenfield projects, prefer OpenID Connect via OAuth2 with PKCE over hand-managed JWTs. Microsoft Entra ID, Auth0, Okta, and Keycloak all handle key rotation, breach detection, and MFA for you. Roll your own only when data residency or licensing forces it.

  • Resource-Based Authorization on Every Endpoint

Broken Object Level Authorization (BOLA) is OWASP API Top 10 #1 for a reason. [Authorize] alone proves the caller is signed in — it says nothing about whether they own the resource they're requesting.

Use ASP.NET Core's policy + resource-based authorization for every endpoint that touches a per-user resource:

builder.Services.AddAuthorization(o =>

{

o.AddPolicy("CanReadOrder", p => p.Requirements.Add(new OrderAccessRequirement()));

});

builder.Services.AddSingleton<IAuthorizationHandler, OrderAccessHandler>();

// In the endpoint:

app.MapGet("/api/orders/{id:int}", async (int id, IAuthorizationService auth,

ClaimsPrincipal user, OrderService svc) =>

{

var order = await svc.GetAsync(id);

if (order is null) return Results.NotFound();

var result = await auth.AuthorizeAsync(user, order, "CanReadOrder");

if (!result.Succeeded) return Results.Forbid(); // 403, not 200

return Results.Ok(order.ToDto());

}).RequireAuthorization();

Make IDOR impossible by construction: scope every query by the authenticated user. db.Orders.Where(o => o.Id == id && o.CustomerId == userId) means even if the policy check is bypassed, the database returns nothing. For Blazor-backed APIs serving the same data, the equivalent pattern lives in our Blazor security strategies article (strategy #2).

  • Rate Limiting + Concurrency Caps

A single expensive endpoint (full-text search, report generation, image processing) can be DoS'd by one attacker with a for loop. No rate limit = no defense.

The .NET 7+ built-in rate limiter is production-grade. Use a fixed-window for simplicity, token-bucket for bursty traffic:

builder.Services.AddRateLimiter(options =>

{

// Default: 60 req/min per IP

options.AddFixedWindowLimiter("default", o =>

{

o.PermitLimit = 60;

o.Window = TimeSpan.FromMinutes(1);

o.QueueLimit = 0;

});

// Expensive endpoint: 10 req/min per authenticated user

options.AddPolicy("expensive", ctx =>

RateLimitPartition.GetFixedWindowLimiter(

partitionKey: ctx.User.FindFirst("sub")?.Value ?? "anon",

factory: _ => new FixedWindowRateLimiterOptions

{

PermitLimit = 10, Window = TimeSpan.FromMinutes(1), QueueLimit = 0

}));

options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

});

app.UseRateLimiter();

app.MapPost("/api/reports/generate", GenerateReport)

.RequireRateLimiting("expensive");

Adaptive's edge WAF rate-limits before traffic reaches your application — included on every Adaptive plan. Combine with the in-app limiter above for defense in depth: edge stops the cheap noise, the app limiter stops the targeted abuse.

  • FluentValidation + DTOs Everywhere

Returning your EF entity directly leaks any field on it — even soft-deleted, audit, or internal-only properties. Accepting an entity as input opens mass-assignment: an attacker sends { "id": 1, "isAdmin": true } and bypasses your domain rules.

Use input DTOs validated with FluentValidation, output DTOs projected with a mapper. Never bind directly to your EF model:

public record CreateOrderRequest(string Sku, int Quantity, string ShippingAddress);

public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>

{

public CreateOrderValidator()

{

RuleFor(x => x.Sku).NotEmpty().Matches(@"^[A-Z0-9-]{3,32}$");

RuleFor(x => x.Quantity).InclusiveBetween(1, 100);

RuleFor(x => x.ShippingAddress).NotEmpty().MaximumLength(500);

}

}

app.MapPost("/api/orders", async (CreateOrderRequest req,

IValidator<CreateOrderRequest> validator, OrderService svc, ClaimsPrincipal user) =>

{

var validation = await validator.ValidateAsync(req);

if (!validation.IsValid) return Results.ValidationProblem(validation.ToDictionary());

var order = await svc.CreateAsync(user.GetUserId(), req);

return Results.Created($"/api/orders/{order.Id}", order.ToDto());

});

  • ProblemDetails + No Exception Leak

Default unhandled-exception output in dev mode leaks file paths, connection strings, and internal class names. Even with UseExceptionHandler, the stack trace can leak via WAF logs, browser dev tools, or proxy logs upstream.

// Map exceptions to RFC 7807 ProblemDetails — never raw stack traces in production

builder.Services.AddProblemDetails(o =>

{

o.CustomizeProblemDetails = ctx =>

{

// Strip any sensitive details from the payload

ctx.ProblemDetails.Extensions.Remove("exception");

ctx.ProblemDetails.Extensions["requestId"] = ctx.HttpContext.TraceIdentifier;

};

});

app.UseExceptionHandler();

app.UseStatusCodePages();

// And in your endpoints, throw typed exceptions and let middleware map them:

app.MapGet("/api/users/{id:int}", async (int id, UserService svc) =>

{

var user = await svc.GetAsync(id);

return user is null

? Results.Problem(statusCode: 404, title: "Not Found")

: Results.Ok(user.ToDto());

});

  • CORS — Explicit Allowlist Only

builder.WithOrigins("*") or AllowAnyOrigin() in production. We see this on roughly 30% of APIs we audit. Combined with cookie-based auth, it's a CSRF disaster.

The correct pattern: explicit origins, narrowest methods, no wildcards. Read origins from configuration so prod, staging, and dev can differ:

var allowedOrigins = builder.Configuration

.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();

builder.Services.AddCors(o =>

{

o.AddPolicy("ProdCors", p => p

.WithOrigins(allowedOrigins)

.WithMethods("GET", "POST", "PUT", "DELETE", "PATCH")

.WithHeaders("Authorization", "Content-Type")

.AllowCredentials()

.SetPreflightMaxAge(TimeSpan.FromMinutes(10)));

});

app.UseCors("ProdCors");

  • TLS 1.3 + HSTS + Post-Quantum on .NET 10

.NET 10 LTS enables TLS 1.3 by default and introduces post-quantum cryptography support — ML-KEM (Key Encapsulation Mechanism) for key exchange, ML-DSA (Digital Signature Algorithm) for signatures. These are NIST-standardized algorithms designed to resist attacks from cryptographically relevant quantum computers. For the broader .NET 10 LTS upgrade picture, see our .NET 10 vs .NET 8 LTS comparison.

if (!app.Environment.IsDevelopment())

{

app.UseHsts(); // Strict-Transport-Security header

app.UseHttpsRedirection(); // 308 redirect HTTP → HTTPS

}

// In appsettings.json:

// "HSTS": { "MaxAgeDays": 365, "IncludeSubDomains": true, "Preload": true }

Every .NET 10 LTS hosting plan includes FREE SSL on every site, TLS 1.3 termination at the edge, and post-quantum-ready certificate support. No "we'll add HTTPS for $19.95/year" tax.

  • API Versioning + Sunset Discipline

Old API versions that never get patched. v1 of your API still accepts the long-deprecated auth flow because someone might still be using it. That "someone" is a fingerprinting attacker probing for the path of least resistance.

Use the official versioning package and commit to a sunset schedule:

builder.Services.AddApiVersioning(o =>

{

o.DefaultApiVersion = new ApiVersion(2, 0);

o.AssumeDefaultVersionWhenUnspecified = false; // force callers to opt in

o.ReportApiVersions = true; // emit api-supported-versions header

o.ApiVersionReader = ApiVersionReader.Combine(

new HeaderApiVersionReader("X-Api-Version"),

new UrlSegmentApiVersionReader());

});

// Endpoint declares the version it supports

app.MapGet("/api/v{version:apiVersion}/orders", GetOrders)

.HasApiVersion(2.0)

.HasDeprecatedApiVersion(1.0); // emits Sunset / Deprecation headers

Commit publicly to a deprecation schedule. We use 12 months from deprecation to sunset, with Deprecation and Sunset response headers from day one. Predictable schedule = customers actually migrate.

  • Structured Audit Logs + SIEM Forwarding

When (not if) you have an incident, the question is "who accessed what, when?". If your only answer is the IIS access log, you have nothing. SIEM-grade structured logs are the difference between a 1-hour incident review and a 3-week post-mortem.

// Serilog with the Compact JSON formatter feeds straight into Splunk / Datadog / Elastic

builder.Host.UseSerilog((ctx, lc) => lc

.ReadFrom.Configuration(ctx.Configuration)

.Enrich.FromLogContext()

.Enrich.WithProperty("Application", "OrdersApi")

.Enrich.WithProperty("Environment", ctx.HostingEnvironment.EnvironmentName)

.WriteTo.File(new CompactJsonFormatter(), "/var/log/orders-api/.json",

rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30));

// Auth/access events as structured properties

app.Use(async (ctx, next) =>

{

using (LogContext.PushProperty("UserId", ctx.User.FindFirst("sub")?.Value))

using (LogContext.PushProperty("RequestId", ctx.TraceIdentifier))

using (LogContext.PushProperty("ClientIp", ctx.Connection.RemoteIpAddress?.ToString()))

{

await next();

}

});

  • Supply-Chain Audit + SBOM

Compromised NuGet packages, typo-squatted dependencies, malicious GitHub Actions in your CI pipeline. Supply-chain attacks rose 740% from 2020 to 2025 and are the fastest-growing API breach vector.

Weekly: vulnerability scan including transitive deps

dotnet list package --vulnerable --include-transitive

.NET 10 LTS ships dotnet audit baked in:

dotnet audit

Generate a CycloneDX SBOM as a CI artifact

dotnet sbom --output sbom.json

Three CI gates worth adding today: (1) Dependabot or Renovate auto-PR for vulnerable deps, (2) CycloneDX SBOM as build artifact, (3) workflow pinning — pin every uses: to a SHA, never a tag. The tag attack vector is widely automated.

Authentication options compared

Production readiness checklist

✅ Transport & Network

HSTS with includeSubDomains; preload

TLS 1.3 default; PQ-ready on .NET 10 LTS

CORS allowlist from config, never wildcard

Rate limiter wired + edge WAF active

✅ Data Hygiene

DTOs in, DTOs out — never raw entities

FluentValidation rules + ModelState rejection

ProblemDetails for errors, no stack-trace leakage

Database queries scoped by current user

✅ Operations

Structured JSON logs forwarded to SIEM

API version + sunset headers on every response

Dependabot / Renovate auto-PRs for CVEs

Quarterly external pentest

Hosting-Layer Security Adaptive Provides

Even with perfect application code, your hosting layer has to cooperate. Every Adaptive Web Hosting plan includes:

LayerWhat's Included

EdgeDDoS protection, managed Web Application Firewall — rate-limits malicious requests before they reach your API

NetworkTLS 1.3 termination, FREE SSL on every site, post-quantum-ready on .NET 10 LTS hosting

HostHardened Windows Server 2022 baseline, regular patching, Server Core editions

IISDedicated Application Pools per site (1–4 GB RAM by plan tier) — one compromised API can't pivot to others

StorageHigh-performance SSD, automated backups, encryption at rest

DatabaseReal SQL Server 2022 with Always Encrypted, ledger tables, Query Store

InfrastructureAWS US-East data center, 99.99% uptime SLA, 30-day money-back guarantee

Choose a plan

$9.49/mo

Dev environments, staging, smaller production APIs. 1 GB dedicated app pool, real SQL Server.

View Developer plan →

Popular

ASP.NET Business

$17.49/mo

Production APIs that need to stay fast under load. 2 GB dedicated app pool, real SQL Server, WAF.

View Business plan →

ASP.NET Professional

$27.49/mo

Agencies + SaaS builders. 10 sites, 200 GB storage, 4 GB dedicated app pool, top-priority scheduling.

View Professional plan →

Frequently Asked Questions

Should we use OIDC or self-issued JWTs for a new API in 2026?

For greenfield projects, default to OIDC via an external provider (Microsoft Entra ID, Auth0, Okta, Keycloak). The "buy the rotation, MFA, breach detection, passkey support" math is overwhelmingly favorable — these providers handle CVEs in the auth layer in hours, not months. Self-issued JWTs are reasonable for internal-only service-to-service APIs where you control both ends.

What's the right access token lifetime?

5–15 minutes for most APIs. Refresh tokens last 7–30 days with rotation on every use. The math: short access tokens limit stolen-token blast radius; rotating refresh tokens surface theft (the legitimate client's refresh fails the moment a stolen one is used).

Do we need rate limiting if our edge WAF already does it?

Yes — defense in depth. The edge WAF stops cheap noise (script-kiddie floods, known scanner IPs); the in-app limiter stops targeted abuse from authenticated users hitting your most-expensive endpoints. These solve different problems. Use both.

How do I prevent BOLA when I have many endpoints?

The cheapest, most reliable defense: scope every database query by the authenticated user. If db.Orders.Where(o => o.Id == id && o.CustomerId == userId) returns nothing, the policy check is a defense-in-depth backup, not the only line of defense. Combine with resource-based authorization for the policy-check layer.

What does "post-quantum-ready" actually mean for my API today?

It means your TLS termination can negotiate ML-KEM (key exchange) and ML-DSA (signatures) when the client supports them. Real-world impact in 2026: low for most apps — but for any data with a 10+ year secrecy requirement (financial, government, healthcare), "harvest now, decrypt later" attacks make PQ matter today. .NET 10 LTS supports it natively; Adaptive's TLS layer is PQ-ready on every plan.

How often should we run external pentests?

Quarterly for production APIs handling payment, health, or identity data. Annually for lower-risk APIs. Continuous SAST in CI (CodeQL, Snyk Code), dependency scanning on every PR, OWASP ZAP DAST in staging on every deploy. Pentests are point-in-time; SAST + dependency scans are continuous — both matter.

What's the difference between [Authorize] and resource-based authorization?

[Authorize] proves the caller is signed in (authentication). Resource-based authorization proves the caller owns or has rights to the specific resource being requested (authorization). Most BOLA bugs come from teams stopping at [Authorize] and assuming "logged-in user = can see this data" — which is false the moment you have more than one customer.

How does this article relate to the Blazor / .NET Core security best practices?

Complementary, not duplicate. The ASP.NET Core security best practices article covers the broader full-stack picture (Identity, anti-forgery, secrets management). The Blazor security strategies article focuses on the Blazor-specific surfaces (circuits, SignalR, WASM assemblies). This article is API-specific — authentication, rate limiting, validation, supply chain. Read all three for the full picture on the Adaptive stack.

Bottom line

API security in 2026 is dominated by three threats: BOLA (broken object-level authorization), broken authentication, and supply-chain compromise. The ten strategies above address each, and they're the baseline we apply to every production API hosted on Adaptive. The good news: ASP.NET Core's defaults are genuinely strong if you configure them — there's no "secure-mode" plugin to install.

If you're scoping APIs that back enterprise Blazor apps, see our eight real-world enterprise Blazor patterns for the API-shape per use case. If you're still on .NET Framework, the ASP.NET 4.8 → .NET 10 LTS migration guide walks through getting onto a modern security baseline. Operationally, if your API is recycling app pools more than expected, the app pool recycling diagnostic is the right next read.

On Adaptive Web Hosting, the security primitives — WAF, DDoS protection, dedicated IIS Application Pools, hardened Windows Server 2022, FREE SSL, and post-quantum-ready TLS 1.3 on .NET 10 LTS — are included on every tier. ASP.NET Developer ($9.49/mo) for development and smaller production APIs, ASP.NET Business ($17.49/mo) for the typical line-of-business API, ASP.NET Professional ($27.49/mo) for agencies and SaaS builders managing multiple APIs. Every plan ships with a 30-day money-back guarantee. View all plans, read the broader ASP.NET Core security checklist, or talk to a security engineer about a specific scenario.

Back to Blog