JWT authentication in ASP.NET Core is a stateless, token-based approach where clients send a signed JWT (usually as a bearer token) and ASP.NET Core validates it on every request to build HttpContext.User. It’s for developers building APIs (REST, SPAs, mobile back ends, microservices) who want scalable authentication without server sessions. The key is understanding how tokens are structured, how they’re validated in the middleware pipeline, and how to handle lifetimes and refresh securely. (IETF Datatracker)
Why does JWT authentication matter for modern APIs?
Modern APIs are consumed by SPAs, mobile apps, partner services, and internal microservices, often across domains and deployments where cookie-based sessions are awkward or undesirable. JWTs are popular because the server can validate a request using only the token and its signing key (or issuer metadata) without loading per-user session state from a shared store. That “statelessness” improves horizontal scalability, but it also shifts responsibility to: strong validation rules, short token lifetimes, safe client storage, and robust monitoring. (IETF Datatracker)
What is a JWT and what problem does it solve?
A JSON Web Token (JWT) is a compact, URL-safe token format for transferring claims between parties. JWTs are typically signed (JWS) so the receiver can verify integrity and authenticity; they can also be encrypted (JWE), though most “JWT auth” in APIs refers to signed tokens.
The problem it solves
JWTs solve the problem of proving identity (or client identity) to an API repeatedly without the API maintaining server-side login session state for every caller.
What JWT does not automatically solve
JWTs do not automatically provide:
- A login UI.
- User credential verification.
- Token refresh and revocation.
- Safe client-side storage.
JWT is a token format; standards like OAuth 2.0 and OpenID Connect are about flows and identity delegation. (ASP.NET Core supports JWT bearer validation either way—you just need correct validation settings.) (Microsoft Learn)
What’s inside a JWT?
A JWT has three Base64URL-encoded parts separated by dots: header.payload.signature.
What is in the header?
Typically, the header includes:
- alg (signing algorithm, e.g., HS256 / RS256)
- kid (key ID, common when keys rotate)
Example (decoded JSON):
JSON
{
"alg": "HS256",
"typ": "JWT"
}
What is in the payload?
The payload contains claims (registered + custom). Common registered claims include:
- iss (issuer)
- aud (audience)
- exp (expiration)
- nbf (not before)
- iat (issued at)
- sub (subject)
Example:
JSON
{
"iss": "https://issuer.example",
"aud": "api://orders",
"sub": "user-123",
"exp": 1736352000,
"role": "Admin"
}
What is the signature?
The signature provides integrity/authenticity. It proves the token was issued by someone holding the signing key (HMAC) or private key (RSA/ECDSA). (IETF Datatracker)
Critical misconception: JWTs are encrypted
JWTs are encoded, not encrypted, unless you’re explicitly using JWE. Anyone who gets the token can Base64URL-decode the header and payload and read the claims. Do not put secrets or sensitive personal data in a JWT.
How does JWT authentication flow work end-to-end?
Here’s the typical flow for APIs:
- Client authenticates (login/client credentials/federated sign-in).
- Issuer mints a JWT (adds claims, sets expiration, signs).
- Client calls API with Authorization: Bearer
- API validates token (signature, issuer, audience, lifetime).
- ASP.NET Core builds HttpContext.User from claims.
- Authorization checks run (policies/roles/claims) before controller/minimal endpoint executes (Microsoft Learn).
Text diagram (request pipeline perspective):
How does ASP.NET Core authenticate requests with JWT bearer internally?
ASP.NET Core authentication is built around:
- IAuthenticationService.
- Registered authentication handlers (e.g., JWT bearer handler).
- Authentication middleware that invokes those handlers.
Authentication determines who the caller is; authorization determines what they can access. When the JWT bearer handler succeeds, ASP.NET Core populates HttpContext.User with a ClaimsPrincipal. (Microsoft Learn)
How do you configure JWT bearer authentication in ASP.NET Core?
The canonical starting point is Microsoft’s JWT bearer configuration guidance. (Microsoft Learn)
The outcome is a minimal Program.cs configuration that validates the issuer, audience, signature, or lifetime.
The example uses a symmetric key for simplicity. In production, you may validate tokens from an external authority (OIDC) or use asymmetric keys for your own issuer.
C#
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// If validating tokens from an OIDC authority, you’d often set:
// options.Authority = "https://issuer.example";
// options.Audience = "api://orders";
var signingKey = builder.Configuration["Jwt:SigningKey"];
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(signingKey!)),
RequireExpirationTime = true,
ValidateLifetime = true,
// Keep this small; clock skew is a real-world reality, not a license for long tokens.
ClockSkew = TimeSpan.FromMinutes(1),
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseHttpsRedirection();
// Order matters:
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
These switches matter because disabling core validation checks is a known insecure pattern (and even flagged by .NET analyzers). (Microsoft Learn)
What should appsettings.json look like?
JSON
{
"Jwt": {
"Issuer": "https://issuer.example",
"Audience": "api://orders",
"SigningKey": "REPLACE_WITH_A_LONG_RANDOM_SECRET"
}
}
Production note: Storing signing keys in appsettings is usually not appropriate—use a secrets manager or managed key store and rotate keys (details later). (If your org’s standard is different, this needs clarification from the security or infra team.)
Which configuration mistakes cause most 401 Unauthorized issues?
- UseAuthentication() missing or placed after UseAuthorization().
- Issuer or audience mismatch between token issuer and API validation.
- Wrong signing key or wrong algorithm.
- Token expired (clock skew misconfigured) (Microsoft Learn).
How do you generate and sign JWT tokens securely?
Token generation should happen only after you authenticate the user or client (password, SSO, client credentials, etc.). Your API should not mint tokens because someone asked.
Task: Generate a short-lived access token with minimal claims
C#
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
public sealed class JwtTokenService
{
private readonly string _issuer;
private readonly string _audience;
private readonly SymmetricSecurityKey _key;
public JwtTokenService(IConfiguration config)
{
_issuer = config["Jwt:Issuer"]!;
_audience = config["Jwt:Audience"]!;
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Jwt:SigningKey"]!));
}
public string CreateAccessToken(string userId, string role)
{
var claims = new List
{
new(JwtRegisteredClaimNames.Sub, userId),
new(ClaimTypes.Role, role),
};
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _issuer,
audience: _audience,
claims: claims,
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(10),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
The JwtSecurityTokenHandler API is widely used and documented, but note that IdentityModel guidance on NuGet indicates newer APIs exist (for some scenarios) and that some packages are considered legacy in newer IdentityModel versions. So keep an eye on library direction when upgrading. (Microsoft Learn)
When should you prefer HMAC or RSA/ECDSA?
- HMAC (HS256): Simplest; same secret signs and validates. Good when the same service issues and validates tokens (or you tightly control key distribution).
- RSA/ECDSA (RS256/ES256): Private key signs; public key validates. Better when multiple services validate tokens and you want safer distribution and key rotation via public keys.
If you’re integrating with an external identity provider, you’ll typically validate using issuer metadata and rotating signing keys (JWKS) rather than hard-coding keys. ASP.NET Core’s JWT bearer handler supports this style of configuration. (Microsoft Learn)
How does ASP.NET Core validate JWTs at runtime?
At runtime, validation is driven mainly by TokenValidationParameters. These checks are the difference between signed token parsing and secure authentication. (Microsoft Learn)
What checks run during validation?
- Signature validation (ValidateIssuerSigningKey)
- Issuer check (ValidateIssuer)
- Audience check (ValidateAudience)
- Lifetime validation (ValidateLifetime, RequireExpirationTime)
- Clock skew tolerance (ClockSkew) (Microsoft Learn)
What happens when validation fails?
The handler fails authentication, and the request will typically end up as:
- 401 Unauthorized (not authenticated).
- Or 403 Forbidden (authenticated but not authorized), depending on your endpoint and policies (Microsoft Learn).
How do you hook into validation events for diagnostics and hardening?
You can attach handlers like OnAuthenticationFailed or OnTokenValidated via JwtBearerOptions.Events. (Microsoft Learn)
C#
.AddJwtBearer(options =>
{
// ...TokenValidationParameters...
options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
// Log context.Exception (avoid leaking details to clients)
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
// Optional: additional checks (e.g., user is disabled, token jti revoked)
return Task.CompletedTask;
}
};
});
How do claims-based authorization and policies work with JWTs?
JWT authentication gives you a ClaimsPrincipal. Authorization uses that principal to enforce access rules.
Outcome: Protect endpoints using policies (recommended over ad-hoc checks)
C#
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("CanReadOrders", policy =>
policy.RequireClaim("scope", "orders.read"));
});
Controller example:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[HttpGet]
[Authorize(Policy = "CanReadOrders")]
public IActionResult GetOrders() => Ok(new { message = "orders" });
[HttpPost]
[Authorize(Policy = "AdminOnly")]
public IActionResult CreateOrder() => Ok();
}
Minimal API example:
C#
var orders = app.MapGroup("/api/orders").RequireAuthorization("CanReadOrders");
orders.MapGet("/", () => Results.Ok(new { message = "orders" }));
Key design tip: Keep tokens small and policies expressive. If you keep adding claims to avoid a database call, you may end up with bloated tokens and brittle authorization logic.
How should you handle token expiration, refresh, and logout?
JWTs are stateless, so logout and revocation are not free.
What is the practical strategy most teams use?
- Short-lived access tokens (minutes).
- Refresh tokens (longer-lived, stored server-side and revocable).
- Rotation (issue a new refresh token when used; revoke old one).
If you need refresh tokens, that’s typically an application-level feature (persistence, hashing, replay detection, revocation lists). ASP.NET Core can validate bearer tokens, but refresh-token design is your responsibility unless you adopt a full identity solution. Microsoft’s guidance also emphasizes selecting the right approach if you’re acting on behalf of a user and using OIDC. (Microsoft Learn)
What’s hard about revocation in stateless systems?
Once an access token is issued, it remains valid until it expires unless you:
- Check a server-side deny list (jti) during OnTokenValidated.
- Use very short expirations and rely on refresh controls.
That’s why short-lived access tokens are the default recommendation for high-security systems.
How do you secure JWT authentication in production?
What should you do to protect signing keys?
- Keep signing keys out of source control.
- Rotate keys deliberately (plan for kid and key rollover).
- Restrict who and what can mint tokens.
Why is HTTPS non-negotiable?
Bearer tokens are credentials. If intercepted, an attacker can replay them until expiration.
What is RequireHttpsMetadata and why does it matter?
If you validate tokens using an authority or metadata endpoint, HTTPS is required by default; disabling it should be limited to development. (Microsoft Learn)
Where should clients store tokens safely?
- Browser apps: Avoid local storage for long-lived tokens if you can. Focus on preventing token leakage via XSS and adopting safer patterns.
- Native/mobile: Use secure OS keystores.
OWASP’s JWT guidance emphasizes preventing common token-handling mistakes (leakage, weak validation, etc.). (OWASP Cheat Sheet Series)
Which JWT attack vectors should you explicitly design against?
- Token theft and replay (mitigate with short expiration, HTTPS, secure storage).
- Weak or disabled validation (issuer/audience/lifetime/signature must be on) (Microsoft Learn).
- Over-trusting claims (treat claims as inputs, not truth, unless you trust the issuer).
- Putting secrets in the payload (payload is readable) (IETF Datatracker).
What are the performance trade-offs of JWT authentication?
JWT validation happens on every request to protected endpoints, so the main performance levers are:
How do you keep per-request validation cheap?
- Keep tokens small with fewer and lighter claims.
- Avoid custom validation that forces database calls on every request. Use it sparingly, e.g., only for high-risk endpoints.
- Prefer platform caching for issuer metadata and keys when using an authority.
IdentityModel’s configuration system is designed to retrieve and refresh metadata, which helps reduce repeated network calls for keys and configuration. (Microsoft Learn)
When can JWT validation become a bottleneck?
- Very high RPS APIs with large tokens.
- Heavy custom claim transformations.
- Per-request revocation checks that hit a central database.
If you must do server-side checks, consider targeted enforcement (admin actions, money movement, data export) rather than all endpoints.
How do you monitor and troubleshoot JWT authentication failures?
Task: Log failures without leaking security details
- Log authentication failures from OnAuthenticationFailed.
- Include correlation IDs and trace IDs.
- Avoid returning exception details to clients.
What are the most common runtime causes of invalid tokens?
- Issuer or audience mismatch.
- Signing key mismatch (wrong secret or rotated key without kid support).
- Token expired or server clock drift.
- Algorithm mismatch.
Using handler events is the fastest way to see what’s going wrong during validation. (Microsoft Learn)
Which common JWT mistakes cause security bugs?
- Disabling issuer, audience, or lifetime validation just to make it work. It often survives into production. (Microsoft Learn)
- Long-lived access tokens, which turn token theft into prolonged access.
- Storing sensitive data in the token payload. It’s readable. (IETF Datatracker)
- Confusing authentication with authorization (authN ≠ access control).
- Rolling your own crypto. Use proven libraries and middleware.
What does a production-ready JWT checklist look like?
- HTTPS enforced everywhere, including metadata endpoints when using an authority. (Microsoft Learn)
- Strong signing keys and a rotation plan.
- ValidateIssuer, ValidateAudience, ValidateLifetime, and RequireExpirationTime enabled. (Microsoft Learn)
- Short-lived access tokens with the refresh strategy documented.
- Claims minimized, so no secrets and no unnecessary data. (IETF Datatracker)
- Authorization policies defined (roles/claims/policies).
- Monitoring enabled, with auth failures logged and anomaly detection considered.
- Integration tests cover: expired token, wrong audience, wrong issuer, wrong signature.
Summary: what should you do next?
- Configure JWT bearer auth with strict validation (issuer/audience/signature/lifetime). (Microsoft Learn)
- Keep access tokens short-lived and design refresh and revocation intentionally if you need them. (Microsoft Learn)
- Use policy-based authorization and keep JWT claims minimal and insensitive. (IETF Datatracker)
- Add diagnostics via JWT bearer events so you can debug 401 and 403 issues quickly. (Microsoft Learn)

Top comments (0)