## 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
## 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.