A JWT is a compact format for transmitting signed claims between two parties.
In many traditional web applications (MVC, Razor Pages), authentication usually works with cookies. The browser sends the cookie with each request, and the server validates it.
In the world of REST APIs, you can also use cookies in some scenarios, but it’s very common to use Bearer tokens when you have mobile clients, SPAs, external integrations, or multiple services communicating with each other.
- Stateless: the API doesn’t need to store in memory who is connected. If you have 5 load-balanced servers, any of them can validate the token.
- Diverse clients: a mobile app, a SPA, or an IoT device don’t always manage cookies the same way a classic browser does.
An alternative is for the client to send a Bearer credential with each request. A very common format for this credential is JWT (JSON Web Token).
Today we’ll see how to implement a complete Login system that generates these tokens and how to configure ASP.NET Core to validate them.
What is a JWT?
A JWT is a text string like eyJhbGci..., encoded with Base64URL and divided into three parts separated by dots:
- Header: indicates which signing algorithm was used (e.g., HS256).
- Payload: Contains the Claims (data). It states who you are (
sub: 123), your name (name: Luis), and when the token expires (exp: 17000000). - Signature: allows verifying the integrity and authenticity of the token using a symmetric key or an asymmetric key pair.
Caution! The payload is not encrypted, only Base64URL encoded. Anyone who obtains the token can read it. NEVER store passwords or sensitive data inside the token. Only identifiers and roles.
Installation and Configuration
We need the official Microsoft package for working with JWT.
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearerThe Secret Key (appsettings.json)
We need a key to sign the tokens. It must be long, random, and secret.
For learning, you can place it in appsettings.json, but in production it should come from environment variables, user-secrets, Azure Key Vault, or another secure store.
{
"Jwt": {
"Key": "EstaEsUnaClaveSuperSecretaYDebeSerMuyLargaParaSeguridad",
"Issuer": "mi-api.com",
"Audience": "mi-app-frontend"
}
}Configuring the Middleware (Program.cs)
Now we tell .NET: “Hey, when a token arrives, verify the signature matches my secret key”.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// --- JWT CONFIGURATION ---
var jwtSettings = builder.Configuration.GetSection("Jwt");
var jwtKey = jwtSettings["Key"]
?? throw new InvalidOperationException("Missing Jwt:Key");
var key = Encoding.UTF8.GetBytes(jwtKey);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = jwtSettings["Issuer"],
ValidateAudience = true,
ValidAudience = jwtSettings["Audience"],
ValidateLifetime = true, // Verifies if expired
ClockSkew = TimeSpan.Zero // Removes the default 5-minute tolerance
};
});
// -------------------------
var app = builder.Build();
app.UseAuthentication(); // 👈 Important! Before Authorization
app.UseAuthorization();
app.MapControllers();
app.Run();Generating the Token on Login
Let’s create the endpoint where the user sends their username/password and we return the token.
Ideally, this would be in a service, but for the example we’ll do it in the controller.
[HttpPost("login")]
public IActionResult Login([FromBody] LoginDto login)
{
// 1. Validate user (This would go against a DB)
if (login.Username != "admin" || login.Password != "1234")
{
return Unauthorized("Invalid credentials");
}
// 2. Create Claims (User data)
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, "1"), // User ID
new Claim(ClaimTypes.Name, login.Username),
new Claim(ClaimTypes.Email, "[email protected]"),
new Claim(ClaimTypes.Role, "Admin") // Role
};
// 3. Create the signature
var jwtKey = _config["Jwt:Key"]
?? throw new InvalidOperationException("Missing Jwt:Key");
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
// 4. Generate the Token
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(1), // Expires in 1 hour
Issuer = _config["Jwt:Issuer"],
Audience = _config["Jwt:Audience"],
SigningCredentials = creds
};
var tokenHandler = new JwtSecurityTokenHandler();
var tokenConfig = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(tokenConfig);
return Ok(new { token = tokenString });
}The hardcoded password in the example is only for illustrating the flow. In a real application, you must validate a password hash with ASP.NET Core Identity or an equivalent mechanism, and also implement rate limiting and other login protections.
Protecting Routes
Now that the user has the token, they must send it with each request in the Authorization header:
Authorization: Bearer eyJhbGci...
To protect our endpoints, we use the Authorize attribute.
[Authorize] // 🔒 Only users with a valid Token can access this
[HttpGet("profile")]
public IActionResult GetProfile()
{
// We can automatically retrieve user's data from the Token
var usuario = User.Identity?.Name;
var id = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Ok($"Hello {usuario}, your ID is {id}. You are inside.");
}If you try to call /profile without a token (or with a fake one), you will receive a 401 Unauthorized.
If you send the correct token, you will receive a 200 OK.