Programming / .NET
Tutorial

Secure .NET API Authentication with JWT and Claims-Based Authorization

Learn how to secure a .NET API with JWT authentication and claims-based authorization. This tutorial covers prerequisites, setup, validation, and production checks so you can implement a practical, verifiable authorization flow.

Secure .NET API Authentication with JWT and Claims-Based Authorization

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:

  • sub or another stable user identifier
  • iss for issuer validation
  • aud for audience validation
  • exp for 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:

  1. no Authorization header
  2. expired token
  3. wrong issuer
  4. wrong audience
  5. valid token without the required claim
  6. 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.

Continue learning

Related content