ASP.NET Core JWT Authentication: Complete Implementation Guide
Building a secure API with ASP.NET Core requires proper authentication. In this guide, we'll implement JWT (JSON Web Token) authentication from scratch, step by step.

Prerequisites
Before we start, make sure you have:
- .NET 8 SDK or later installed
- A code editor (Visual Studio, VS Code, or Rider)
- Basic understanding of C# and REST APIs
Step 1: Create a New ASP.NET Core Web API Project
# Create a new Web API project dotnet new webapi -n JwtAuthDemo cd JwtAuthDemo # Install required NuGet packages dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer dotnet add package System.IdentityModel.Tokens.Jwt
💡 What is a NuGet Package? NuGet is the package manager for .NET, similar to npm for Node.js. Packages are pre-built libraries that add functionality to your project.
Step 2: Configure JWT Settings in appsettings.json
Add JWT configuration to appsettings.json:
{ "Jwt": { "Key": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!", "Issuer": "https://yourdomain.com", "Audience": "https://yourdomain.com", "ExpiryMinutes": 60 }, "Logging": { "LogLevel": { "Default": "Information" } } }
Glossary
- Key: The secret key used to sign the JWT. Must be kept secure and never exposed.
- Issuer: The entity that issued the token (usually your API server).
- Audience: The intended recipient of the token (usually your API or client app).
- ExpiryMinutes: How long the token remains valid.
Step 3: Create Models
Create a Models folder and add the following files:
LoginRequest.cs
namespace JwtAuthDemo.Models; public class LoginRequest { public string Username { get; set; } = string.Empty; public string Password { get; set; } = string.Empty; }
LoginResponse.cs
namespace JwtAuthDemo.Models; public class LoginResponse { public string Token { get; set; } = string.Empty; public DateTime Expiration { get; set; } }
Step 4: Create JWT Service
Create a Services folder and add JwtService.cs:
using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; namespace JwtAuthDemo.Services; public class JwtService { private readonly IConfiguration _configuration; public JwtService(IConfiguration configuration) { _configuration = configuration; } public string GenerateToken(string username, string role) { // 1. Create Claims (user information) var claims = new[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.Role, role), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; // 2. Get the secret key from configuration var key = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]!) ); // 3. Create signing credentials var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); // 4. Set expiration time var expiry = DateTime.UtcNow.AddMinutes( Convert.ToDouble(_configuration["Jwt:ExpiryMinutes"]) ); // 5. Create the token var token = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], claims: claims, expires: expiry, signingCredentials: credentials ); // 6. Return the serialized token string return new JwtSecurityTokenHandler().WriteToken(token); } }
Code Explanation
- Claims: Key-value pairs containing user information embedded in the token.
- SymmetricSecurityKey: A cryptographic key used for both signing and verifying the token.
- SigningCredentials: Specifies the algorithm (HMAC-SHA256) used to sign the token.
- JwtSecurityToken: The actual token object containing all the information.
Step 5: Configure Authentication in Program.cs
Replace the contents of Program.cs:
using System.Text; using JwtAuthDemo.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); // Add services to the container builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Register JwtService builder.Services.AddSingleton<JwtService>(); // Configure JWT Authentication builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!) ) }; }); builder.Services.AddAuthorization(); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // IMPORTANT: Order matters! app.UseAuthentication(); // Must come before UseAuthorization app.UseAuthorization(); app.MapControllers(); app.Run();
⚠️ Critical: Middleware Order
UseAuthentication()must be called beforeUseAuthorization(). Authentication identifies who you are, and authorization determines what you can do.
Step 6: Create the Auth Controller
Create Controllers/AuthController.cs:
using JwtAuthDemo.Models; using JwtAuthDemo.Services; using Microsoft.AspNetCore.Mvc; namespace JwtAuthDemo.Controllers; [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly JwtService _jwtService; public AuthController(JwtService jwtService) { _jwtService = jwtService; } [HttpPost("login")] public IActionResult Login([FromBody] LoginRequest request) { // In production, validate against a database if (request.Username == "admin" && request.Password == "password123") { var token = _jwtService.GenerateToken(request.Username, "Admin"); var expiration = DateTime.UtcNow.AddMinutes(60); return Ok(new LoginResponse { Token = token, Expiration = expiration }); } return Unauthorized(new { message = "Invalid credentials" }); } }
Step 7: Create a Protected Endpoint
Create Controllers/WeatherController.cs:
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace JwtAuthDemo.Controllers; [ApiController] [Route("api/[controller]")] public class WeatherController : ControllerBase { [HttpGet("public")] public IActionResult GetPublicData() { return Ok(new { message = "This is public data" }); } [Authorize] // This endpoint requires authentication [HttpGet("protected")] public IActionResult GetProtectedData() { var username = User.Identity?.Name; return Ok(new { message = "This is protected data", user = username }); } [Authorize(Roles = "Admin")] // Requires Admin role [HttpGet("admin")] public IActionResult GetAdminData() { return Ok(new { message = "Admin-only data" }); } }
Step 8: Testing the API
Run the application:
dotnet run
Test 1: Login and Get Token
curl -X POST https://localhost:7000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"password123"}'
Response:
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expiration": "2026-01-23T11:00:00Z" }
Test 2: Access Protected Endpoint
curl -X GET https://localhost:7000/api/weather/protected \ -H "Authorization: Bearer YOUR_TOKEN_HERE"
Success response:
{ "message": "This is protected data", "user": "admin" }
Common Issues and Solutions
Issue 1: 401 Unauthorized
Cause: Token is missing or invalid.
Solution: Ensure you're sending the token in the Authorization header with the Bearer prefix.
Issue 2: Token Validation Failed
Cause: The secret key in appsettings.json doesn't match the one used to sign the token.
Solution: Restart the application after changing the key.
Security Best Practices
- Never hardcode secrets: Use environment variables or Azure Key Vault in production.
- Use HTTPS: Always transmit tokens over encrypted connections.
- Short expiration times: Set tokens to expire in 15-60 minutes.
- Implement refresh tokens: Allow users to get new tokens without re-entering credentials.
- Validate on every request: The
[Authorize]attribute ensures the token is checked.
Conclusion
You now have a fully functional JWT authentication system in ASP.NET Core. This setup provides a solid foundation for building secure APIs.