Why this pattern matters
A .NET API that accepts requests without a clear authentication and authorization boundary is hard to operate safely: every endpoint becomes a potential exposure point, and every integration has to be trusted more than it should be. JWT authentication solves the identity verification side by letting the API validate a signed token locally, while claims-based authorization lets you make access decisions based on explicit token data instead of ad hoc logic scattered across controllers.
In this tutorial, you will build a practical API security baseline: the API will validate bearer tokens, extract claims, enforce authorization policies, and reject requests that do not meet the expected identity or role requirements. By the end, you should be able to decide whether this approach fits your deployment, implement the flow, validate it with reproducible checks, and confirm what to verify before production use.
What you will build
You will configure an ASP.NET Core API so that:
- requests without a valid JWT are rejected with
401 Unauthorized - requests with a valid JWT but missing required claims are rejected with
403 Forbidden - protected endpoints use policies or role-based rules instead of custom authorization code
- the API validates token signature, issuer, audience, and expiration before trusting claims
If you need a broader implementation walkthrough for the same pattern, Implement Secure JWT Authentication in ASP.NET Core APIs covers the token issuance and validation flow in more depth.
Prerequisites and stop-here-if warnings
Before you start, confirm the following:
- you have a .NET SDK and an ASP.NET Core API project ready to modify
- you know which identity provider issues the JWTs
- you can identify the issuer, audience, signing algorithm, and token lifetime expected by the API
- you have a non-production environment for validation
Stop here if any of these are unknown
Do not wire authorization policies into production until you can answer these questions:
- Who issues the tokens?
- Which signing key or trust chain will the API use?
- What exact issuer and audience values should be accepted?
- Which claims represent user identity, tenant, role, or privilege?
- How will key rotation be handled?
If any of those are unclear, build the token contract first. Otherwise you risk accepting tokens that look valid but are not intended for this API.
Preparation: define the token contract
Goal
Make the token structure explicit before you write code. Authorization becomes much easier to reason about when claim names, required claims, and policy boundaries are fixed up front.
Action
Decide which claims your API will rely on. A practical minimum is:
subor another stable user identifierissfor issuer validationaudfor audience validationexpfor expiration- one or more authorization claims such as
role,scope, or a custom permission claim
For role-based access, keep the role list small and meaningful. For more granular control, use claims like permission or scope so you can authorize by capability instead of broad role membership.
If your organization uses role claims as the primary authorization model, Secure .NET APIs with JWT Authentication and Role Claims is useful background for designing those boundaries carefully.
Expected output
You should have a simple written contract that states:
- who issues the token
- what the API accepts as trusted identity data
- which claims gate which endpoints
- what should happen when a claim is missing or incorrect
Validation
Review one example token from your issuer and confirm that every claim your API depends on is present and stable across refreshes or reauthentication.
Common failure
A frequent mistake is using a display name, email address, or mutable profile field as the identity key. Those values can change and do not make good authorization anchors.
Implementation: configure JWT authentication
Goal
Teach the API how to validate a bearer token before any controller or endpoint code runs.
Action
Add authentication and token validation configuration in Program.cs. The exact validation parameters depend on your issuer, but the API should always validate signature, issuer, audience, and lifetime.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SigningKey"]!)),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1)
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CanReadOrders", policy =>
policy.RequireClaim("permission", "orders.read"));
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("admin"));
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
If your issuer uses asymmetric signing, replace the symmetric key example with the issuer’s public key or metadata-driven validation. Do not hardcode secrets in source code.
Expected output
The application should now:
- read JWT settings from configuration
- reject invalid signatures
- reject tokens from the wrong issuer or audience
- reject expired tokens
- establish a claims principal for authorized requests
Validation
Make one unauthenticated request to a protected endpoint. The API should return 401 Unauthorized. Then send a token with the wrong issuer or an expired exp claim and confirm that the API rejects it.
Common failure
A common misconfiguration is enabling authentication but forgetting app.UseAuthentication() before app.UseAuthorization(). In that case, policies may run without a populated identity and produce confusing results.
Implementation: protect endpoints with claims-based authorization
Goal
Restrict access based on claims rather than by writing custom permission checks in every endpoint.
Action
Apply authorization attributes or endpoint requirements to specific actions.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet]
[Authorize(Policy = "CanReadOrders")]
public IActionResult GetOrders()
{
return Ok(new[] { "order-1", "order-2" });
}
[HttpPost]
[Authorize(Policy = "AdminOnly")]
public IActionResult CreateOrder()
{
return Ok("created");
}
}
The first endpoint requires a permission=orders.read claim. The second requires the caller to be in the admin role. This keeps authorization rules readable and centralized.
Expected output
Protected endpoints should now be separated by policy, and the controller code should remain free of token parsing or custom identity checks.
Validation
Test the following cases:
- no token: expect
401 - valid token without required claim: expect
403 - valid token with required claim: expect
200
That three-state result is the operational signal that your authentication and authorization layers are separated correctly.
Common failure
A frequent error is placing too much business logic inside the controller, such as checking claims manually with User.Claims.First(...). That works at first but becomes difficult to audit and easy to break when claim names change. Prefer policies where possible.
Implementation: inspect claims safely inside the API
Goal
Use token claims for contextual decisions without bypassing the policy layer.
Action
When you need identity data for logging or correlation, read claims defensively.
var userId = User.FindFirst("sub")?.Value;
var tenantId = User.FindFirst("tenant_id")?.Value;
var roles = User.FindAll("role").Select(c => c.Value).ToArray();
Use this data for context, not as a substitute for authorization. If the action requires a claim, enforce that requirement in a policy.
Expected output
Your API can correlate requests to identities or tenants while still depending on the authorization middleware to make access decisions.
Validation
Confirm that endpoints still behave correctly when claims are absent. Logging or telemetry code should not throw exceptions if a claim is missing; authorization should fail earlier when the claim is required.
Common failure
Assuming every token will contain the same claim set leads to runtime null reference errors or hidden authorization gaps. Treat missing claims as normal and explicitly handled.
Validation workflow you can run before production use
Goal
Prove the API rejects malformed or unauthorized requests before traffic reaches production.
Action
Use a short validation matrix:
- no
Authorizationheader - expired token
- wrong issuer
- wrong audience
- valid token without the required claim
- valid token with the required claim
You can exercise these cases with curl or any HTTP client.
curl -i https://localhost:5001/api/orders
For bearer requests, include the token in the header:
curl -i https://localhost:5001/api/orders \
-H "Authorization: Bearer <token>"
Expected output
Your results should be predictable:
- invalid or missing identity data returns
401 - valid identity without permission returns
403 - valid identity with permission returns success
Validation
Record the result of each case and keep it with your release checklist. That evidence is often more valuable than a code review comment because it proves the runtime behavior.
Common failure
If every request returns 401, your signing key, issuer, or audience is probably wrong. If invalid tokens are accepted, recheck the validation parameters immediately and treat it as a release blocker.
Operational follow-up: production checks and maintenance
Goal
Keep the authentication layer trustworthy after deployment.
Action
Before production, verify the following:
- the signing key is stored securely and rotated according to your policy
- issuer and audience values match the runtime environment
- token lifetime is short enough to limit exposure but long enough for clients to operate
- role and permission claims are documented and stable
- logs capture authentication failures without storing tokens or secrets
For logging discipline around security events, Secure .NET Logging with Serilog and Structured Events is helpful when you need clearer request tracing without leaking sensitive data.
Expected output
The API should be able to authenticate and authorize requests consistently across deployments, with a clear operational path for key rotation and policy updates.
Validation
After deployment, confirm that:
- valid production tokens are accepted only by intended environments
- expired tokens fail as expected
- rotated keys still validate through the intended trust mechanism
- changes to claims or roles are reflected in policy behavior
Common failure
The most common production issue is environment drift: a token audience, issuer, or signing key works in test but not in production. Treat token settings as environment configuration, not application assumptions.
Practical decision rules
Use JWT authentication and claims-based authorization when you need stateless request validation, clear access boundaries, and policy-driven authorization at the API layer.
Avoid relying on this pattern alone if:
- you cannot control token issuance
- you do not have a stable issuer or signing trust model
- your authorization needs depend heavily on revocation or centralized session state
- your claim model is still changing rapidly
In those cases, resolve the identity contract first or choose an architecture that better matches your control over the token lifecycle.
Final takeaway
A secure .NET API does not start with controller code; it starts with a verifiable token contract, strict JWT validation, and authorization policies that turn claims into explicit access rules. If you define the claims you trust, validate the token properties you require, and test the unauthorized cases as carefully as the happy path, you get a security model that is easier to operate and far easier to audit.
Use this guidance together with Git rebase vs merge and Dijkstra's algorithm to connect the workflow with related operational context already available on the site.
Part of the Programming: .NET Insights content cluster.