## Why JWT — and Why Most Implementations Are Wrong
When I built the admin module for my portfolio, I needed authentication that was stateless, secure, and simple to maintain. JWT was the obvious choice. But after reading dozens of tutorials, I noticed most of them stop right where the real problems begin.
## The Correct Mental Model for JWT
A JWT is a signed claim. When a user logs in, you give them a signed token that says "this user is who they say they are." Every subsequent request carries this token. You verify the signature — no database lookup needed.
Login → Server creates token → Client stores token → Client sends token with every request → Server verifies signature → Access granted
Login → Server creates token → Client stores token → Client sends token with every request → Server verifies signature → Access granted
## Implementation
### Step 1: Configure JWT Settings
```json
// appsettings.json
"JwtSettings": {
"SecretKey": "minimum-32-character-secret-key-here",
"Issuer": "your-app-name",
"Audience": "your-app-users",
"ExpiryMinutes": 60
}
```
### Step 2: Register JWT in Program.cs
```csharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
ValidAudience = builder.Configuration["JwtSettings:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecretKey"])),
ClockSkew = TimeSpan.Zero // Don't allow 5-minute grace period
};
});
```
### Step 3: Token Generation Service
```csharp
public class TokenService : ITokenService
{
private readonly IConfiguration _config;
public string GenerateAccessToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Role, user.Role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["JwtSettings:SecretKey"]));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["JwtSettings:Issuer"],
audience: _config["JwtSettings:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(60),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
```
### Step 4: Protect Endpoints
```csharp
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Ok(userId);
}
[Authorize(Roles = "Admin")]
[HttpDelete("users/{id}")]
public IActionResult DeleteUser(int id) { }
```
## The Mistakes Most Tutorials Don't Warn You About
**Mistake 1: Storing JWT in localStorage**
LocalStorage is accessible via JavaScript. XSS attack = stolen token. Use HttpOnly cookies instead.
**Mistake 2: No token expiry**
Tokens without expiry are permanent security holes. Always set expiry.
**Mistake 3: Weak secret key**
"mysecret" is not a secret key. Use a cryptographically random 256-bit key.
**Mistake 4: Not validating ClockSkew**
Default ClockSkew in ASP.NET is 5 minutes — meaning expired tokens still work for 5 extra minutes. Set it to zero.
## Conclusion
JWT done correctly is elegant and secure. JWT done carelessly creates vulnerabilities that are hard to detect and easy to exploit. The difference is in the details — expiry, storage, key strength, and proper validation. Get these right and you have solid, production-ready authentication.