
Learn ASP.NET Core JWT authentication step by step with runnable C# code, best practices, and common pitfalls. Start securing your API today!
If you are building a modern web API, ASP.NET Core JWT authentication is one of the most important skills you can learn. JSON Web Tokens (JWT) let you secure your endpoints in a stateless, scalable way that works perfectly with single-page apps, mobile clients, and microservices. In this step-by-step tutorial, you will learn exactly how to implement JWT token authentication in C# with runnable code, understand why each piece matters, and avoid the security pitfalls that trip up most developers.
By the end, you will have a working ASP.NET Core Web API that issues bearer tokens on login, validates them on protected routes, and follows production-grade best practices.
What Is JWT and Why Use It in ASP.NET Core?
A JSON Web Token is a compact, URL-safe string made of three Base64Url-encoded parts separated by dots: header.payload.signature. The header describes the signing algorithm, the payload carries claims (such as the user ID and roles), and the signature proves the token has not been tampered with.
The reason JWT token authentication in C# is so popular comes down to one word: stateless. The server does not store sessions in memory or a database. Instead, every request carries a signed token that the API can validate independently. This makes horizontal scaling trivial — any server instance can verify any token using the shared signing key. Key benefits include:
- Scalability — no server-side session store required.
- Cross-platform — works with web, mobile, and third-party clients.
- Self-contained — claims travel with the token, reducing database lookups.
- Standards-based — JWT is defined by RFC 7519 and supported everywhere.
Setting Up Your ASP.NET Core Web API Project
Start by creating a new Web API and adding the JWT bearer authentication package. Open a terminal and run:
dotnet new webapi -n JwtAuthDemo
cd JwtAuthDemo
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Next, store your JWT settings in appsettings.json. Never hard-code secrets in source files — use this only for local development, and move the key to environment variables or a secret manager in production.
{
"Jwt": {
"Key": "ThisIsAVerySecretKeyForJwtSigning_ChangeMe_32Chars!",
"Issuer": "https://csharp-coder.com",
"Audience": "https://csharp-coder.com",
"ExpiryMinutes": 60
}
}
Why a 32+ character key? The HMAC-SHA256 algorithm requires a key of at least 256 bits (32 bytes). A short key will throw an exception at runtime and, worse, weaken your signature security.
How to Implement JWT Authentication in ASP.NET Core Web API
Now register the authentication services in Program.cs. This is the core of ASP.NET Core JWT authentication — it tells the framework how to validate every incoming bearer token.
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
var jwtSection = builder.Configuration.GetSection("Jwt");
var key = Encoding.UTF8.GetBytes(jwtSection["Key"]!);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSection["Issuer"],
ValidAudience = jwtSection["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(key),
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Two details here separate beginners from professionals:
ClockSkew = TimeSpan.Zero— by default .NET allows a 5-minute grace period on token expiry. Setting it to zero means tokens expire exactly when you say they do.- Order matters —
UseAuthentication()must come beforeUseAuthorization(). The framework needs to know who the user is before it can decide what they are allowed to do.
Generating a JWT Bearer Token on Login
Now let's create the service that issues tokens. When a user logs in successfully, you generate a signed JWT containing their claims. This is the heart of bearer token authentication.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
public class TokenService
{
private readonly IConfiguration _config;
public TokenService(IConfiguration config) => _config = config;
public string GenerateToken(string userId, string email, string role)
{
var jwt = _config.GetSection("Jwt");
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt["Key"]!));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userId),
new Claim(JwtRegisteredClaimNames.Email, email),
new Claim(ClaimTypes.Role, role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(
issuer: jwt["Issuer"],
audience: jwt["Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(int.Parse(jwt["ExpiryMinutes"]!)),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
The Jti (JWT ID) claim gives each token a unique identifier — essential if you later want to build a token revocation/blacklist feature. Register the service in Program.cs with builder.Services.AddScoped<TokenService>();.
Creating the Login Endpoint
Here is a minimal authentication controller. In a real app you would verify the password against a hashed value stored in your database (for example using ASP.NET Core Identity), never compare plain text.
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly TokenService _tokenService;
public AuthController(TokenService tokenService) => _tokenService = tokenService;
[HttpPost("login")]
public IActionResult Login([FromBody] LoginRequest request)
{
// Replace this with real, hashed-password verification.
if (request.Email == "demo@csharp-coder.com" && request.Password == "Password123!")
{
var token = _tokenService.GenerateToken(
userId: "1",
email: request.Email,
role: "Admin");
return Ok(new { token });
}
return Unauthorized(new { message = "Invalid credentials" });
}
}
public record LoginRequest(string Email, string Password);
Protecting Endpoints with the [Authorize] Attribute
Securing a route is now as simple as adding the [Authorize] attribute. Any request without a valid bearer token receives a 401 Unauthorized response automatically.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
[ApiController]
[Route("api/[controller]")]
public class ProfileController : ControllerBase
{
[HttpGet]
[Authorize]
public IActionResult GetProfile()
{
var email = User.FindFirstValue(ClaimTypes.Email);
var role = User.FindFirstValue(ClaimTypes.Role);
return Ok(new { email, role, message = "You accessed a protected route!" });
}
[HttpGet("admin")]
[Authorize(Roles = "Admin")]
public IActionResult AdminOnly() => Ok("Welcome, Admin!");
}
To call the protected endpoint, the client sends the token in the Authorization header:
curl https://localhost:5001/api/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Notice [Authorize(Roles = "Admin")] — this is role-based authorization driven directly by the role claim you embedded in the token. No extra database call needed.
JWT Authentication Best Practices and Common Pitfalls
Implementing the happy path is easy; doing it securely is where senior engineers earn their keep. Follow these JWT authentication best practices to keep your API safe in production.
- Always use HTTPS. A bearer token is like a password — over plain HTTP, anyone on the network can steal and replay it.
- Keep access tokens short-lived. 15–60 minutes is typical. Pair them with long-lived refresh tokens so users stay logged in without exposing a long-lived access token.
- Never put sensitive data in the payload. JWT claims are only Base64-encoded, not encrypted — anyone can decode them. Never store passwords or secrets there.
- Store the signing key securely. Use environment variables, Azure Key Vault, or AWS Secrets Manager — never commit it to source control.
- Validate everything. Issuer, audience, lifetime, and signing key should all be validated, as shown above. Disabling any check opens an attack vector.
- Avoid storing tokens in localStorage on the browser when possible — it is vulnerable to XSS. Prefer secure, HttpOnly cookies for web apps.
Adding Refresh Tokens (Advanced)
For a complete, production-ready flow, issue a short-lived access token plus a refresh token stored in your database. When the access token expires, the client exchanges the refresh token for a new access token. This limits the damage if an access token leaks while keeping users signed in. The pattern looks like this conceptually:
// On login: return both tokens
return Ok(new {
accessToken = _tokenService.GenerateToken(userId, email, role),
refreshToken = GenerateAndStoreRefreshToken(userId)
});
// On /refresh: validate the stored refresh token, then issue a new access token
// and rotate the refresh token (one-time use) to prevent replay attacks.
Rotating refresh tokens — issuing a new one each time and invalidating the old — is the gold standard for advanced JWT security in C#.
Conclusion: Key Takeaways
You now have a complete, working implementation of ASP.NET Core JWT authentication — from configuring the bearer middleware, to generating signed tokens on login, to protecting endpoints with role-based authorization. More importantly, you understand why each step matters.
Remember these essentials:
- JWT enables stateless, scalable bearer token authentication that's perfect for APIs and microservices.
- Always validate the issuer, audience, lifetime, and signing key.
- Use HTTPS, short-lived access tokens, and refresh tokens for production security.
- Never store secrets in the JWT payload — claims are encoded, not encrypted.
With these fundamentals and best practices, you can confidently secure any ASP.NET Core Web API. Try extending this demo with refresh token rotation and ASP.NET Core Identity to build a fully production-grade authentication system. Happy coding!
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment