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

๐Ÿ—️ Deep Dive: Understanding Every Concept in Microsoft Entra API Onboarding for .NET Developers

When working with Microsoft Entra (formerly Azure Active Directory), you’ll hear terms like App Registration, Tenant, Client ID, Audience, Scopes, Roles, Tokens, OBO flow , and more. If you’re new, it can feel overwhelming. This guide breaks down every key term and concept , with definitions, examples, and how they connect when you onboard and consume a new API. ๐Ÿ”น 1. Tenant Definition : A tenant in Entra ID is your organization’s dedicated, isolated instance of Microsoft Entra. Think of it like : Your company’s identity directory. Example : contoso.onmicrosoft.com is a tenant for Contoso Ltd. ๐Ÿ”น 2. App Registration Definition : The process of registering an application in Entra to give it an identity and permission to use Microsoft identity platform. Why needed : Without registration, Entra doesn’t know about your app. What it creates : Application (Client) ID – unique identifier for your app Directory (Tenant) ID – your organization’s ID Types of apps : Web ...

๐Ÿ—‘️ Garbage Collection & Resource Management in .NET (C#) — Beginner Friendly Guide

When you start working with .NET and C#, one of the biggest advantages is that you don’t need to manually manage memory like in C or C++. The Garbage Collector (GC) does most of the work for you. But here’s the catch — not everything is managed automatically. Some resources like files, database connections, sockets, and native memory still need special handling. This blog will help you understand: ✔ How the GC works ✔ What are managed vs unmanaged resources ✔ The difference between Dispose , Finalize , and using ✔ The Dispose pattern with examples ✔ Best practices every C# developer should know 1) How Garbage Collection Works in .NET Managed resources → Normal .NET objects (string, List, etc.). GC frees them automatically. Unmanaged resources → External resources like file handles, database connections, sockets, native memory. GC cannot clean them up — you must do it. ๐Ÿ‘‰ GC uses a Generational Model for performance: Gen 0 : Short-lived objects (local variables, t...

☁️ 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...