DrikWeb Technical Guide #9

ASP.NET Core API security

Securing an ASP.NET Core API: authentication, authorization, rate limiting and audit

A valid Bearer token is not enough to make an API secure. Identifying the caller is only the first layer: you still need to decide what it can do, which resources it may access and how much traffic it may generate.

In ASP.NET Core, authentication, authorization, rate limiting and audit are easier to reason about when they remain separate, testable responsibilities instead of security checks scattered through controllers.

Securing an ASP.NET Core API: authentication, authorization, rate limiting and audit

Securing an ASP.NET Core API is not just about checking that a request contains a valid token. Real systems need separate controls to identify the caller, decide what it may do, protect service capacity and reconstruct relevant operations.

In short: authentication, authorization, rate limiting and audit solve different problems. A robust API treats them as complementary layers instead of concentrating security in a single middleware or scattering checks across controllers.

1. Authentication: who is calling?

Authentication establishes the identity of a client or user. In ASP.NET Core it can use JWT Bearer, cookies, API keys or an external identity provider.

The key distinction is that identity is not permission: a valid token proves who the caller is, not that it may perform every operation exposed by the API.

2. Authorization: what is the caller allowed to do?

For APIs with granular permissions, ASP.NET Core policies provide explicit, reusable rules. Claims can represent application capabilities such as access to a specific area, document downloads, inspection data or write operations.

This avoids scattering if statements throughout controllers. A policy can be applied to multiple endpoints, tested independently and mapped to a meaningful domain requirement.

3. Authorize the resource, not only the endpoint

In many systems it is not enough to know that a client may call GET /documents/{id}. You must also verify that it may access that specific document. This is where capability-based rules meet resource-based authorization.

The rule should be evaluated where both identity and resource context are available rather than relying on UI filtering or client-provided parameters.

4. API keys: never store the secret in clear text

When an integration uses API keys, store a non-reversible hash in the database, optionally combined with a pepper kept outside the database. The complete key should be shown to the client only when it is created.

This means unauthorized database access does not directly expose reusable credentials. Key identification, revocation, rotation and last-used tracking are also useful operational controls.

5. Rate limiting: protect capacity and stability

An authenticated client can still generate too much traffic, intentionally or by mistake. Rate limiting caps requests over time and can be configured by endpoint, API key, user, IP address or operation category.

ASP.NET Core's rate limiting middleware makes it possible to define different policies. Expensive or sensitive endpoints can have stricter limits than lightweight read operations.

Behind a reverse proxy or CDN, the limiter must also use the correct client identity. When traffic passes through a provider such as Cloudflare, forwarded headers should be handled carefully and trusted only from configured proxies.

6. Audit: reconstruct relevant operations

Application logs mainly help diagnose software; audit records help reconstruct operationally or security-relevant actions. For an important change, useful fields can include who performed the action, which operation, which resource, when it happened and the outcome.

Audit should not become an indiscriminate copy of request payloads. Data minimization still applies: record what is required for traceability without duplicating personal data, credentials or sensitive content.

7. Errors: do not expose internal details

A secure API returns consistent errors without exposing stack traces, SQL queries, table names or credential details. ProblemDetails can provide a uniform response shape while technical information stays in internal logs correlated through a request identifier.

Status codes should also remain meaningful: 401 when identity is missing or invalid, 403 when the caller is authenticated but not authorized, and 429 when a traffic limit has been exceeded.

8. Consent and domain constraints remain separate controls

In some APIs a caller may be authenticated and have the technical permission, but the operation must still be blocked because consent, a contractual condition or another domain requirement is missing.

These checks should not be hidden inside authentication. Keeping them separate makes the reason for denial clearer and allows domain rules to evolve without mixing identity, authorization and process state.

A practical layered model

A typical flow can be:

  1. identify the client or user;
  2. resolve claims and permissions;
  3. apply the endpoint policy;
  4. authorize the specific resource;
  5. evaluate domain or consent constraints;
  6. apply the appropriate rate limit;
  7. execute the operation;
  8. write an audit event when relevant.

Each layer has a clear responsibility and can be tested separately. This reduces the risk of turning security into a chain of hard-to-follow conditions spread between controllers and repositories.

Centralize what must be consistent, without creating one giant security component

Centralization does not mean putting every check into a single service. It is useful to centralize what should remain consistent — credential validation, policies, rate limiting and audit-event structure — while domain-specific decisions stay close to the resource or process they protect.

The same principle applies when designing enterprise APIs with ASP.NET Core: responsibilities and boundaries should be explicit rather than simply moved from one controller to another.

FAQ

Is a Bearer token enough to secure an API?

No. The token mainly solves identity. You still need authorization, resource checks, abuse protection and audit for relevant operations.

Should I use roles or policies in ASP.NET Core?

Roles can be sufficient for simple cases. When permissions become granular or depend on claims and application requirements, policies make the rules more explicit, reusable and testable.

Should rate limiting be used only on public endpoints?

No. Authenticated clients can also generate excessive traffic because of bugs, aggressive retries or misuse. Limits can therefore differ by endpoint, client or operation category.

What should an audit record contain?

Caller identity, event type, affected resource, timestamp and outcome are often a good starting point. Full payloads should generally be avoided unless there is a specific, justified requirement.

Conclusion

API security does not end with a Bearer token. A robust design separates identity, permissions, resource access, capacity protection, domain constraints and traceability.

ASP.NET Core provides the building blocks to organize these concerns as separate, testable components. The result is not only stronger security but also an architecture where each denial can be explained and each rule can evolve without multiplying ad-hoc checks.

In summary

A valid token identifies the caller. It does not decide everything the caller may do.

Authentication, authorization, resource access, rate limiting and audit should remain separate responsibilities. This makes API security easier to test, reason about and maintain.

Related guides

Do you need to design or secure an existing .NET API?

I can support software houses and development teams with ASP.NET Core API design, granular authorization, legacy-system integration, rate limiting, logging and application audit.

Let's discuss the project