An API is the front door to your data, and attackers know it. A single unguarded endpoint or a token that never expires can expose an entire customer database. The good news is that ASP.NET Core gives you strong, well-tested security primitives - the job is to use them deliberately and consistently. This guide walks through the practical layers of securing a REST API, from identity to transport to input.
Authentication vs authorization
These two words are often used interchangeably, but they solve different problems. Authentication answers “who are you?” - verifying the caller’s identity, typically by validating a token or signing in a user. Authorization answers “what are you allowed to do?” - deciding whether that identity may perform a given action. You always authenticate first, then authorize.
In ASP.NET Core, authentication is configured in the pipeline and authorization is expressed with roles, claims and policies. Applying [Authorize] to a controller or endpoint is the simplest gate:
[ApiController]
[Route("api/orders")]
[Authorize] // every action requires an authenticated caller
public class OrdersController : ControllerBase
{
private readonly IOrderService _orders;
public OrdersController(IOrderService orders) => _orders = orders;
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
var order = await _orders.GetForUserAsync(id, User);
return order is null ? NotFound() : Ok(order);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "CanManageOrders")] // finer-grained authorization
public async Task<IActionResult> Delete(int id)
{
await _orders.DeleteAsync(id, User);
return NoContent();
}
}
JWT bearer tokens and ASP.NET Core Identity
For stateless APIs, JWT bearer tokens are the standard. The client authenticates once, receives a signed token, and sends it in the Authorization: Bearer header on every request; the API validates the signature and claims without a database round-trip. Register the scheme in Program.cs and always validate issuer, audience, lifetime and signing key:
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true, // reject expired tokens
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
Use ASP.NET Core Identity to manage user accounts, password hashing and roles behind that token issuance. Keep access tokens short-lived and pair them with refresh tokens so a stolen token has a small window of use. For third-party sign-in and single sign-on, prefer OAuth2 / OpenID Connect with a trusted identity provider rather than handling raw credentials yourself.
Enforce HTTPS and HSTS
None of the above matters if tokens travel in plaintext. Redirect all traffic to HTTPS and enable HSTS so browsers refuse to downgrade to HTTP. Both are one line each: app.UseHttpsRedirection() and app.UseHsts() (in production). Terminate TLS at your gateway and never expose an unencrypted endpoint, even “just for testing.”
Validate input and guard against over-posting
Treat every incoming payload as hostile until proven otherwise. With the [ApiController] attribute, ASP.NET Core automatically returns a 400 when model validation fails, so annotate your models with data annotations or use FluentValidation for richer, testable rules.
Equally important is preventing over-posting (mass assignment). Never bind requests straight to your EF Core entities - a caller could set fields like IsAdmin or Balance that were never meant to be editable. Accept a dedicated DTO / input model that exposes only the safe fields, validate it, then map to your entity:
public record CreateOrderRequest(
[Required] string Product,
[Range(1, 100)] int Quantity);
[HttpPost]
public async Task<IActionResult> Create(CreateOrderRequest request)
{
// ModelState is validated automatically by [ApiController]
var order = new Order
{
Product = request.Product,
Quantity = request.Quantity,
UserId = User.GetUserId() // set server-side, never from the client
};
await _orders.AddAsync(order);
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
Prevent SQL injection with parameterised access
SQL injection remains one of the most damaging flaws, and it’s entirely preventable. Entity Framework Core parameterises queries for you, so LINQ-based data access is safe by default. If you must run raw SQL, always use parameters - FromSqlInterpolated or explicit SqlParameter objects - and never concatenate user input into a query string. The same discipline applies to any dynamic query building.
Configure CORS and add rate limiting
A browser-facing API needs a deliberate CORS policy. Whitelist the specific origins that may call you; never combine AllowAnyOrigin with credentials, which effectively opens your API to every site. Define a named policy and apply it explicitly.
To blunt brute-force and abuse, use the built-in rate limiting middleware introduced in modern .NET. A fixed-window or sliding-window limiter caps how many requests a client can make in a period, protecting login endpoints and expensive operations:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueLimit = 0;
});
});
// ...
app.UseRateLimiter();
Manage secrets properly
Connection strings, signing keys and API keys must never live in source control. In development, use the .NET user-secrets tool; in production, use environment variables or a managed vault such as Azure Key Vault. ASP.NET Core’s configuration system layers these providers transparently, so Configuration["Jwt:Key"] works identically whether the value comes from a vault or a local secret store - only the storage location changes.
Follow the OWASP API Security Top 10
The OWASP API Security Top 10 is the industry checklist for API risks, and its top entries are about authorization, not exotic exploits. Broken Object Level Authorization - returning a record without checking the caller actually owns it - is the number-one issue; always scope queries to the authenticated user. Combine that with structured logging via ILogger for auditability, but scrub sensitive data (tokens, passwords, card numbers) out of your logs, and return correct HTTP status codes: 401 for unauthenticated, 403 for forbidden, 400 for bad input, 404 for missing or unauthorised resources.
Common mistakes to avoid
- Trusting the client for identity. Deriving the user ID from the request body instead of the validated token invites impersonation. Read it from
User. - Skipping object-level checks. Loading order 42 for any authenticated user, without verifying ownership, is the classic broken authorization bug.
- Leaking details in errors. Returning stack traces or SQL messages hands attackers a map. Return generic messages and log the detail server-side.
- Long-lived or unvalidated tokens. Disabling lifetime validation or issuing year-long tokens turns one leak into a permanent breach.
Best practices checklist
- Authenticate then authorize every endpoint; default to
[Authorize]and open up explicitly. - Validate every input and accept DTOs, never raw entities.
- Force HTTPS and HSTS, and keep access tokens short-lived with refresh tokens.
- Whitelist CORS origins and apply rate limiting to sensitive routes.
- Keep secrets in a vault and sensitive data out of logs.
Securing an API well takes discipline across many layers, and it’s easy to miss one. If you’d like an experienced team to build or review it, our .NET development service covers secure API design end to end. Once the security foundation is in place, tune the data layer too - see our guide to Entity Framework Core best practices.



