JWT Tokens and How They Work Internally

JWT (JSON Web Token) is an open standard (RFC 7519) used for securely transmitting information between two parties as a JSON object. JWT is widely used for authentication and authorization purposes because it is stateless, compact, and easy to verify.

1. Structure of JWT

A JWT token consists of three parts separated by periods:

  • Header: Contains metadata about the token, including the type of token (JWT) and the hashing algorithm (e.g., HMAC, SHA256).
  • Payload: Holds the claims, or information, such as user ID and roles.
    • Registered Claims: Predefined claims, e.g., iss (issuer), sub (subject), and exp (expiration).
    • Public Claims: Custom claims that are not reserved.
    • Private Claims: Custom claims agreed upon between parties.
  • Signature: Ensures that the token was not tampered with and verifies the authenticity.

Example JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0Iiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

2. Steps for JWT Token Generation and Validation

  1. Token Creation: The server creates a token when a user logs in, encoding the header and payload, and hashing them using a secret key.
  2. Token Transmission: The server sends the JWT token to the client (usually in the response).
  3. Token Validation: The client sends the JWT in the Authorization header as a Bearer token. The server decodes, checks the signature, and validates claims like expiration.

JWT in the Authorization Header

Authorization: Bearer <JWT_TOKEN>

Pros of JWTs

  • Stateless: No need to store sessions on the server.
  • Compact: Lightweight and fast to transmit.
  • Self-contained: Contains all necessary information about the user.
  • Cross-platform: Compatible with many languages and frameworks.

JWT Algorithm & How Tokens Are Generated

1. JWT Algorithms

  • HS256 (HMAC with SHA-256): Uses a secret key. Symmetric (same key for signing and verifying).
  • RS256 (RSA Signature with SHA-256): Uses a public-private key pair. Asymmetric (more secure for distributed applications).

2. Generating JWT Tokens

Example: Generating a JWT Using HS256

{
    "user_id": "12345",
    "role": "admin",
    "exp": 1704067199
}
  1. Encoding: The payload and header are Base64Url-encoded.
  2. Signature Generation:
    HMACSHA256(
        base64UrlEncode(header) + "." +
        base64UrlEncode(payload),
        my_secret_key
    )
    

Example Code: Generating and Verifying JWT in .NET

Generating a JWT:

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
using System.Text;
 
public string GenerateJwtToken(string userId, string role, string secretKey)
{
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, userId),
        new Claim("role", role),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
    };
 
    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
 
    var token = new JwtSecurityToken(
        issuer: "my_app",
        audience: "my_app",
        claims: claims,
        expires: DateTime.Now.AddMinutes(30),
        signingCredentials: creds);
 
    return new JwtSecurityTokenHandler().WriteToken(token);
}

Verifying a JWT:

public ClaimsPrincipal ValidateJwtToken(string token, string secretKey)
{
    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.UTF8.GetBytes(secretKey);
    var validationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = false,
        ValidateAudience = false
    };
 
    try
    {
        var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
        return principal;
    }
    catch (Exception)
    {
        return null; // Token is invalid or expired
    }
}

Best Practices and Common Pitfalls of JWT

1. Best Practices

  • Use Strong Secret Keys: Ensure the secret key is long, complex, and stored securely.
  • Use Asymmetric Algorithms (e.g., RS256): For distributed applications.
  • Limit the Claims in the Payload: Only include essential information.
  • Set Expiration Times: Use short-lived tokens (minutes/hours).
  • Implement Refresh Tokens: For long-lived sessions.
  • Store Tokens Securely: Use secure, HTTP-only cookies.
  • Validate All Claims: Check iss, aud, and exp claims.
  • Monitor for JWT Revocation: Implement a revocation list.

2. Common Pitfalls

  • Overly long token expiration times
  • Including sensitive data in the payload
  • Lack of signature validation
  • Using weak signing algorithms (e.g., none)
  • Not using HTTPS

JWT Authentication and Authorization in ASP.NET Core Web API

1. Setting Up JWT Authentication

Step 1: Install NuGet Packages

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Step 2: Add JWT Settings to appsettings.json

"JwtSettings": {
    "SecretKey": "Your_Secret_Key_Here",
    "Issuer": "my_app",
    "Audience": "my_app_audience"
}

Step 3: Configure Authentication in Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
 
var builder = WebApplication.CreateBuilder(args);
 
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secretKey = jwtSettings.GetValue<string>("SecretKey");
 
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = jwtSettings.GetValue<string>("Issuer"),
        ValidAudience = jwtSettings.GetValue<string>("Audience"),
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey))
    };
});
 
builder.Services.AddAuthorization();

2. Protecting Endpoints with JWT Authorization

[ApiController]
[Route("api/[controller]")]
public class SecureDataController : ControllerBase
{
    [HttpGet("secure-info")]
    [Authorize]
    public IActionResult GetSecureInfo()
    {
        return Ok("This is a secure endpoint only accessible to authenticated users.");
    }
 
    [HttpGet("admin-data")]
    [Authorize(Roles = "Admin")]
    public IActionResult GetAdminData()
    {
        return Ok("This is an admin-protected endpoint.");
    }
}

3. Generating JWT Tokens for Users

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IConfiguration _configuration;
 
    public AuthController(IConfiguration configuration)
    {
        _configuration = configuration;
    }
 
    [HttpPost("login")]
    public IActionResult Login([FromBody] LoginModel model)
    {
        if (model.Username == "testuser" && model.Password == "password")
        {
            var token = GenerateJwtToken(model.Username);
            return Ok(new { Token = token });
        }
 
        return Unauthorized("Invalid credentials");
    }
 
    private string GenerateJwtToken(string username)
    {
        var jwtSettings = _configuration.GetSection("JwtSettings");
        var secretKey = jwtSettings.GetValue<string>("SecretKey");
 
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, username),
            new Claim("role", "User"),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };
 
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
 
        var token = new JwtSecurityToken(
            issuer: jwtSettings.GetValue<string>("Issuer"),
            audience: jwtSettings.GetValue<string>("Audience"),
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(30),
            signingCredentials: creds);
 
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Refresh Tokens

1. What is a Refresh Token?

A refresh token is a long-lived token issued alongside the JWT, allowing the client to request a new JWT when it expires without requiring the user to re-authenticate.

2. How Refresh Tokens Work

  1. User Authenticates: Server issues both a JWT and a refresh token.
  2. Using JWT: Client uses the JWT to access protected resources.
  3. Token Expiry and Refresh: When the JWT expires, the client sends the refresh token to obtain a new JWT.
  4. Logout: Refresh token is invalidated.

3. Implementing Refresh Tokens

Refresh Token Model:

public class RefreshToken
{
    public string Token { get; set; }
    public string UserId { get; set; }
    public DateTime ExpiryDate { get; set; }
    public bool IsRevoked { get; set; }
}

Generate Refresh Token:

private string GenerateRefreshToken()
{
    var randomNumber = new byte[32];
    using (var rng = RandomNumberGenerator.Create())
    {
        rng.GetBytes(randomNumber);
        return Convert.ToBase64String(randomNumber);
    }
}

Refresh Token Endpoint:

[HttpPost("refresh-token")]
public IActionResult RefreshToken([FromBody] RefreshTokenRequest request)
{
    var savedToken = GetStoredRefreshToken(request.RefreshToken);
    if (savedToken == null || savedToken.IsRevoked || savedToken.ExpiryDate < DateTime.UtcNow)
    {
        return Unauthorized("Invalid or expired refresh token.");
    }
 
    var newJwtToken = GenerateJwtToken(savedToken.UserId);
    var newRefreshToken = GenerateRefreshToken();
 
    UpdateStoredRefreshToken(savedToken, newRefreshToken);
 
    return Ok(new { Token = newJwtToken, RefreshToken = newRefreshToken });
}

4. Security Best Practices for Refresh Tokens

  • Store refresh tokens securely in HTTP-only cookies.
  • Rotate tokens each time a JWT is refreshed.
  • Limit token scope to only requesting new JWTs.
  • Keep JWTs short-lived.
  • Implement logout to invalidate refresh tokens.

Key Points to Remember

  1. JWT Structure: Header, payload, signature — ensures data integrity.
  2. Role of Refresh Tokens: Extend session duration without frequent re-authentication.
  3. Secure JWT and Refresh Tokens: Use best practices to store, validate, and revoke tokens.
  4. Code Flow: Know how to generate JWTs, secure endpoints, and implement refresh logic.
  5. ASP.NET Core Integration: Master setup including authentication, authorization, and token validation.