Skip to main content

ASP.NET Core JWT Authentication Tutorial (2026 Guide)

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 before UseAuthorization(). 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!

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...