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
- User logs in with credentials (username/password).
- Server validates credentials, creates a JWT with claims and expiration, signs it, and returns token to client.
- Client sends token in
Authorization: Bearer <token>
header on subsequent API requests. - 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
- On login, issue access token + refresh token.
- Store refresh token in DB with user id, expiry, and a unique identifier (jti).
- Client stores access token (memory) and refresh token (secure cookie or storage as per app).
- When access token expires, client calls
/refresh
with refresh token. - 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
- Short-lived access tokens + refresh tokens stored server-side — keep access tokens short (5–15m). Server can revoke refresh tokens to stop future access.
- Token blacklist (store revoked token ids / jti in DB or cache) — check blacklist on each request (adds lookup overhead).
- Use reference tokens (store token server-side and pass opaque token to client). API validates by lookup — stateful but easy to revoke.
- 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.
- 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
andiss
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
Post a Comment