Skip to main content

๐Ÿ” JWT Authentication in .NET — Complete Guide

JWT (JSON Web Token) is a compact, URL-safe token format for representing claims between two parties. In modern APIs it’s commonly used for stateless authentication.

This guide covers:

  • What a JWT is (structure & signing)
  • How JWT authentication works (flow)
  • Implementation in ASP.NET Core (issue, validate, protect)
  • Refresh tokens and revocation strategies
  • Security best practices

๐Ÿ”Ž What is a JWT?

A JWT has three parts separated by dots:

HEADER.PAYLOAD.SIGNATURE
  • Header — algorithm and token type, e.g. {"alg":"HS256","typ":"JWT"}
  • Payload (Claims) — JSON object with claims (e.g. sub, iat, exp, custom claims)
  • Signature — signs base64(header) + '.' + base64(payload) with a secret or private key

Example header+payload (base64) + signature → single token string.

Signing algorithms

  • Symmetric: HS256 (HMAC + shared secret) — simple, good for single-server or trusted env.
  • Asymmetric: RS256 (RSA) or ES256 (ECDSA) — recommended for distributed systems where auth server signs and APIs verify with public key.

๐Ÿ” Typical JWT Auth Flow

  1. User logs in with credentials (username/password).
  2. Server validates credentials, creates a JWT with claims and expiration, signs it, and returns token to client.
  3. Client sends token in Authorization: Bearer <token> header on subsequent API requests.
  4. API validates token (signature, issuer, audience, expiry) and authorizes access based on claims.

Optionally:

  • Server also issues a refresh token (longer-lived, stored securely server-side or as a secure cookie) to obtain new access tokens after expiry.

⚙️ ASP.NET Core Setup (modern Program.cs style)

1) Add packages (if not already)

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt

2) Configuration (appsettings.json)

"Jwt": {
  "Key": "very_long_random_secret_here_change_in_production",
  "Issuer": "yourapp.example",
  "Audience": "yourapp.clients",
  "AccessTokenExpiresMinutes": 15,
  "RefreshTokenExpiresDays": 30
}

In production store the key in a secrets manager (Azure Key Vault, AWS Secrets Manager, environment variable).

3) Program.cs — configure authentication

var builder = WebApplication.CreateBuilder(args);

// bind config
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>();
builder.Services.AddSingleton(jwtSettings);

// Add authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = jwtSettings.Issuer,

        ValidateAudience = true,
        ValidAudience = jwtSettings.Audience,

        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)),

        ValidateLifetime = true,
        ClockSkew = TimeSpan.FromSeconds(30) // small leeway
    };

    // Optional: events for logging/refresh token flow / custom validation
    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = ctx => {
            // log if needed
            return Task.CompletedTask;
        },
        OnTokenValidated = ctx => {
            // custom checks (e.g., check user still active)
            return Task.CompletedTask;
        }
    };
});

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

JwtSettings class

public class JwtSettings
{
    public string Key { get; set; }
    public string Issuer { get; set; }
    public string Audience { get; set; }
    public int AccessTokenExpiresMinutes { get; set; }
    public int RefreshTokenExpiresDays { get; set; }
}

๐Ÿงฉ Creating (Issuing) JWTs — TokenService Example

Create a service that issues access tokens and refresh tokens.

public interface ITokenService
{
    string CreateAccessToken(IdentityUser user, IEnumerable<Claim> additionalClaims = null);
    RefreshToken CreateRefreshToken();
}

public class TokenService : ITokenService
{
    private readonly JwtSettings _jwtSettings;
    public TokenService(IOptions<JwtSettings> jwtOptions)
    {
        _jwtSettings = jwtOptions.Value;
    }

    public string CreateAccessToken(IdentityUser user, IEnumerable<Claim> additionalClaims = null)
    {
        var claims = new List<Claim>
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id),
            new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName),
            new Claim(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty),
            // add roles or custom claims as needed
        };

        if (additionalClaims != null)
            claims.AddRange(additionalClaims);

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _jwtSettings.Issuer,
            audience: _jwtSettings.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_jwtSettings.AccessTokenExpiresMinutes),
            signingCredentials: creds
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public RefreshToken CreateRefreshToken()
    {
        var randomBytes = new byte[64];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomBytes);
        return new RefreshToken
        {
            Token = Convert.ToBase64String(randomBytes),
            Expires = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpiresDays),
            Created = DateTime.UtcNow
        };
    }
}

public class RefreshToken
{
    public string Token { get; set; }
    public DateTime Expires { get; set; }
    public DateTime Created { get; set; }
    public bool IsExpired => DateTime.UtcNow >= Expires;
}

Register service

builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("Jwt"));
builder.Services.AddScoped<ITokenService, TokenService>();

๐Ÿ” Refresh Tokens — Pattern & Example

Why refresh tokens?
Access tokens are short-lived (e.g., 15 minutes). Refresh tokens allow obtaining new access tokens without re-authenticating the user.

Where to store refresh tokens?

  • Server-side (DB) tied to user, device, and metadata (IP, user-agent). Recommended for revocation and rotation.
  • HTTP-only secure cookie for browsers to reduce XSS risk.
  • Avoid storing long-lived tokens in browser localStorage.

Flow

  1. On login, issue access token + refresh token.
  2. Store refresh token in DB with user id, expiry, and a unique identifier (jti).
  3. Client stores access token (memory) and refresh token (secure cookie or storage as per app).
  4. When access token expires, client calls /refresh with refresh token.
  5. Server validates refresh token (exists, not expired, not revoked), issues new access token and optionally a new refresh token (rotation), invalidates the old refresh token.

Refresh endpoint example

[HttpPost("refresh")]
public async Task<IActionResult> Refresh([FromBody] RefreshRequest request)
{
    var principal = GetPrincipalFromExpiredToken(request.AccessToken);
    if (principal == null) return BadRequest("Invalid access token");

    var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier) ?? principal.FindFirstValue(JwtRegisteredClaimNames.Sub);

    var storedRefresh = await _db.RefreshTokens.SingleOrDefaultAsync(rt => rt.Token == request.RefreshToken);
    if (storedRefresh == null || storedRefresh.UserId != userId || storedRefresh.IsRevoked || storedRefresh.IsExpired)
        return Unauthorized();

    // Option: rotate - invalidate old token, create new refresh token
    storedRefresh.IsRevoked = true;
    storedRefresh.Revoked = DateTime.UtcNow;

    var newRefresh = _tokenService.CreateRefreshToken();
    storedRefresh.ReplacedBy = newRefresh.Token;
    _db.RefreshTokens.Add(new RefreshTokenEntity { UserId = userId, Token = newRefresh.Token, Expires = newRefresh.Expires, Created = newRefresh.Created });

    // Create new access token
    var user = await _userManager.FindByIdAsync(userId);
    var newAccessToken = _tokenService.CreateAccessToken(user /* add claims if needed */);

    await _db.SaveChangesAsync();

    return Ok(new { accessToken = newAccessToken, refreshToken = newRefresh.Token });
}

Helper to read expired token

private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
    var tokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = true,
        ValidAudience = _jwtSettings.Audience,
        ValidateIssuer = true,
        ValidIssuer = _jwtSettings.Issuer,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key)),
        ValidateLifetime = false // we want to get claims from expired token
    };

    var tokenHandler = new JwtSecurityTokenHandler();
    var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
    if (securityToken is not JwtSecurityToken jwtSecurityToken ||
        !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
        throw new SecurityTokenException("Invalid token");

    return principal;
}

๐Ÿ”’ Token Revocation & Security Strategies

JWTs by design are stateless — once issued, servers don’t need to store them. But this makes revocation hard.

Common strategies

  1. Short-lived access tokens + refresh tokens stored server-side — keep access tokens short (5–15m). Server can revoke refresh tokens to stop future access.
  2. Token blacklist (store revoked token ids / jti in DB or cache) — check blacklist on each request (adds lookup overhead).
  3. Use reference tokens (store token server-side and pass opaque token to client). API validates by lookup — stateful but easy to revoke.
  4. Token versioning — store a token version or security stamp on user; include it in token and validate current version in DB. When you want to revoke tokens, bump version.
  5. Refresh token rotation — issue a new refresh token every time; revoke previous one on reuse, making replay attacks detectable.

Protect refresh tokens

  • Store refresh tokens securely (HTTP-only secure cookie for browser).
  • Bind refresh token to device/user agent and IP if needed.
  • Use refresh token expiration and rotation.

✅ Claims, Roles & Policies

  • Use claims for user identity (sub, email, role, permissions).
  • Map roles: new Claim(ClaimTypes.Role, "Admin") and use [Authorize(Roles = "Admin")].
  • Use policies for granular access control:
services.AddAuthorization(options =>
{
    options.AddPolicy("CanEditProducts", policy =>
        policy.RequireClaim("permission", "product:edit"));
});

Then:

[Authorize(Policy = "CanEditProducts")]
public IActionResult Edit(...) { ... }

๐Ÿงฐ Additional Practical Tips

  • Store signing keys securely — environment variables or secret store. Rotate keys occasionally.
  • Prefer RS256 if you need multiple APIs to validate tokens but only auth server signs them. Publish public keys via JWKS endpoint.
  • Set aud and iss claims and validate them to avoid token replay across different APIs.
  • Minimize claims to only what you need to reduce token size and sensitive data exposure.
  • Use ClockSkew small value (30s) to accommodate time drift.
  • Log auth events: login success/failure, refresh, revoke — useful for audits.
  • Enable HTTPS always.
  • Prevent CSRF when using cookies for tokens: use SameSite, CSRF tokens, or use Authorization header instead.

๐Ÿ” Example: Full Login Controller (simplified)

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly ITokenService _tokenService;
    private readonly AppDbContext _db;

    public AuthController(UserManager<IdentityUser> userManager, ITokenService tokenService, AppDbContext db)
    {
        _userManager = userManager;
        _tokenService = tokenService;
        _db = db;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] LoginDto dto)
    {
        var user = await _userManager.FindByNameAsync(dto.Username);
        if (user == null) return Unauthorized();

        if (!await _userManager.CheckPasswordAsync(user, dto.Password))
            return Unauthorized();

        var accessToken = _tokenService.CreateAccessToken(user);
        var refreshToken = _tokenService.CreateRefreshToken();

        // Save refresh token in DB
        _db.RefreshTokens.Add(new RefreshTokenEntity {
            UserId = user.Id,
            Token = refreshToken.Token,
            Expires = refreshToken.Expires,
            Created = refreshToken.Created
        });
        await _db.SaveChangesAsync();

        return Ok(new { accessToken, refreshToken = refreshToken.Token });
    }
}

๐Ÿงพ Summary Checklist

  • [ ] Use HTTPS and store secrets centrally (Key Vault)
  • [ ] Short-lived access tokens, longer refresh tokens with rotation
  • [ ] Store refresh tokens server-side and tie to device/user
  • [ ] Validate iss, aud, exp, and signature on server
  • [ ] Use RS256 when separate signing/validation responsibilities exist
  • [ ] Implement token revocation/rotation strategy (refresh token rotation or token versioning)
  • [ ] Log and monitor auth events, use policies for granular access control

Comments

Popular posts from this blog

☁️ Azure Key vault Short Notes

๐ŸŸข What is Azure Key Vault? A cloud service for securely storing and accessing secrets, keys, and certificates . Removes the need to keep secrets (like connection strings, passwords, API keys) inside code or config files. Provides centralized secret management, encryption, and access control . ๐Ÿ‘‰ Think of it like a secure password manager but for your applications. ๐ŸŸข Key Features Secrets → store text values (e.g., DB connection string, API key). Keys → store cryptographic keys (RSA, EC) for encryption, signing. Certificates → store/manage SSL/TLS certificates. Access Control → Access Policies (older model). Azure RBAC (modern, preferred). Integration → works with App Service, Functions, AKS, VMs, SQL DB, etc. Logging → audit who accessed secrets via Azure Monitor / Diagnostic Logs. ๐ŸŸข Why Use Key Vault? Security → secrets are encrypted with HSM (Hardware Security Modules). Compliance → meet industry standards (PCI-DSS, ISO, GDPR). Automation → aut...