JSON Web Tokens (JWT) have become the gold standard for securing modern APIs, providing a stateless, scalable authentication solution that enables seamless communication between distributed services. As web applications increasingly rely on APIs, implementing robust JWT authentication is crucial for protecting sensitive data and ensuring only authorized users can access your resources.
Understanding JWT Authentication
JSON Web Token (JWT) is an open standard that defines a compact, URL-safe method for securely transmitting information between parties as a JSON object. Unlike traditional session-based authentication that stores user state on the server, JWT authentication is stateless – all necessary information is contained within the token itself[4].
JWT Structure
A JWT consists of three parts separated by dots (.):
- Header - Contains metadata about the token type and signing algorithm.
- Payload - Contains claims (user information, permissions, expiration time)
- Signature - Ensures the token hasn't been tampered with
Example JWT structure:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Setting Up JWT Authentication in ASP.NET Core
Step 1: Create Your Project and Install Required Packages
Start by creating a new ASP.NET Core Web API project:
dotnet new webapi -n SecureJWTApi
cd SecureJWTApi
Install the required NuGet packages:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt
Step 2: Configure JWT Settings
Add JWT configuration to your appsettings.json:
{
"JwtSettings": {
"SecretKey": "YourSuperSecureSecretKeyThatIsAtLeast256BitsLong",
"Issuer": "https://your-api.com",
"Audience": "https://your-client-app.com",
"AccessTokenExpirationMinutes": 30,
"RefreshTokenExpirationDays": 7
}
}
Step 3: Configure JWT Authentication in Program.cs
Configure JWT authentication in your Program.cs file:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
// Configure JWT Authentication
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 // Remove default 5-minute tolerance
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHsts();
app.UseHttpsRedirection();
app.UseAuthentication(); // Must come before UseAuthorization
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 4: Create JWT Token Service
Create a service to generate and validate JWT tokens:
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
public interface ITokenService
{
string GenerateAccessToken(string userId, string email, string role);
string GenerateRefreshToken();
ClaimsPrincipal? GetPrincipalFromExpiredToken(string token);
}
public class TokenService : ITokenService
{
private readonly IConfiguration _configuration;
public TokenService(IConfiguration configuration)
{
_configuration = configuration;
}
public string GenerateAccessToken(string userId, string email, string role)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(_configuration["JwtSettings:SecretKey"]);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, userId),
new Claim(ClaimTypes.Email, email),
new Claim(ClaimTypes.Role, role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat,
new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(),
ClaimValueTypes.Integer64)
}),
Expires = DateTime.UtcNow.AddMinutes(
int.Parse(_configuration["JwtSettings:AccessTokenExpirationMinutes"])),
Issuer = _configuration["JwtSettings:Issuer"],
Audience = _configuration["JwtSettings:Audience"],
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
public ClaimsPrincipal? GetPrincipalFromExpiredToken(string token)
{
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_configuration["JwtSettings:SecretKey"])),
ValidateLifetime = false // We don't validate lifetime here
};
var tokenHandler = new JwtSecurityTokenHandler();
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters,
out SecurityToken securityToken);
if (securityToken is not JwtSecurityToken jwtSecurityToken ||
!jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
}
Step 5: Create Authentication Controller
Implement login and token refresh endpoints:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]//e.g. api/auth/login
public class AuthController : ControllerBase
{
private readonly ITokenService _tokenService;
public AuthController(ITokenService tokenService)
{
_tokenService = tokenService;
}
[HttpPost("login")]
public async Task<ActionResult<AuthResponse>> Login([FromBody] LoginRequest request)
{
// Validate user credentials (implement your own user validation logic)
var user = await ValidateUser(request.Email, request.Password);
if (user == null)
{
return Unauthorized(new { message = "Invalid credentials" });
}
var accessToken = _tokenService.GenerateAccessToken(
user.Id.ToString(), user.Email, user.Role);
var refreshToken = _tokenService.GenerateRefreshToken();
// Store refresh token in database associated with user
await StoreRefreshToken(user.Id, refreshToken);
return Ok(new AuthResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresIn = 1800 // 30 minutes
});
}
[HttpPost("refresh")]
public async Task<ActionResult<AuthResponse>> RefreshToken([FromBody] RefreshTokenRequest request)
{
var principal = _tokenService.GetPrincipalFromExpiredToken(request.AccessToken);
if (principal == null)
{
return BadRequest("Invalid access token");
}
var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Validate refresh token from database
var isValidRefreshToken = await ValidateRefreshToken(userId, request.RefreshToken);
if (!isValidRefreshToken)
{
return BadRequest("Invalid refresh token");
}
var user = await GetUserById(userId);
var newAccessToken = _tokenService.GenerateAccessToken(
user.Id.ToString(), user.Email, user.Role);
var newRefreshToken = _tokenService.GenerateRefreshToken();
// Update refresh token in database
await UpdateRefreshToken(user.Id, newRefreshToken);
return Ok(new AuthResponse
{
AccessToken = newAccessToken,
RefreshToken = newRefreshToken,
ExpiresIn = 1800
});
}
// Implement these methods based on your data access layer
private async Task<User?> ValidateUser(string email, string password) { /* Implementation */ }
private async Task StoreRefreshToken(int userId, string refreshToken) { /* Implementation */ }
private async Task<bool> ValidateRefreshToken(string userId, string refreshToken) { /* Implementation */ }
private async Task<User> GetUserById(string userId) { /* Implementation */ }
private async Task UpdateRefreshToken(int userId, string refreshToken) { /* Implementation */ }
}
// DTOs
public class LoginRequest
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
public class RefreshTokenRequest
{
public string AccessToken { get; set; } = string.Empty;
public string RefreshToken { get; set; } = string.Empty;
}
public class AuthResponse
{
public string AccessToken { get; set; } = string.Empty;
public string RefreshToken { get; set; } = string.Empty;
public int ExpiresIn { get; set; }
}
Step 6: Secure Your API Endpoints
Apply the [Authorize] attribute to protect your controllers:
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
// This endpoint now requires a valid JWT token
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var userRole = User.FindFirst(ClaimTypes.Role)?.Value;
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
}).ToArray();
}
[HttpGet("admin-only")]
[Authorize(Roles = "Admin")]
public IActionResult GetAdminData()
{
return Ok(new { message = "This is admin-only data" });
}
}
JWT Security Best Practices
- Use Strong Algorithms and Keys
Always use asymmetric algorithms like RS256 for production environments rather than symmetric algorithms like HS256. Use strong, randomly generated secret keys of at least 256 bits.
// For RS256 (recommended for production)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(GetRSAKey()), // Load from certificate
ValidateIssuer = true,
ValidIssuer = "https://your-identity-server.com",
ValidateAudience = true,
ValidAudiences = new[] { "api1", "api2" },
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
- Implement Short Token Lifespans
Use short expiration times for access tokens (15-30 minutes) and implement refresh tokens for maintaining sessions:
var tokenDescriptor = new SecurityTokenDescriptor
{
// ... other properties
Expires = DateTime.UtcNow.AddMinutes(15), // Short lifespan
NotBefore = DateTime.UtcNow // Token not valid before now
};
- Validate All Critical Claims
Ensure proper validation of issuer (iss), audience (aud), expiration (exp), and not-before (nbf) claims:
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://your-trusted-issuer.com",
ValidateAudience = true,
ValidAudience = "your-api-audience",
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.Zero // No tolerance for expired tokens
};
- Implement Refresh Token Rotation
Rotate refresh tokens on each use to limit the impact of compromised tokens:
[HttpPost("refresh")]
public async Task<ActionResult<AuthResponse>> RefreshToken([FromBody] RefreshTokenRequest request)
{
// Validate current refresh token
var isValid = await ValidateAndRevokeRefreshToken(request.RefreshToken);
if (!isValid)
{
return BadRequest("Invalid refresh token");
}
// Generate new tokens
var newAccessToken = GenerateAccessToken(userId);
var newRefreshToken = GenerateRefreshToken();
// Store new refresh token and invalidate old one
await StoreRefreshToken(userId, newRefreshToken);
return Ok(new AuthResponse
{
AccessToken = newAccessToken,
RefreshToken = newRefreshToken
});
}
- Secure Token Storage
Never store tokens in local storage or session storage. Use HTTP-only cookies with secure flags:
[HttpPost("login")]
public async Task<ActionResult> Login([FromBody] LoginRequest request)
{
// ... authentication logic
var cookieOptions = new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.AddDays(7)
};
Response.Cookies.Append("refreshToken", refreshToken, cookieOptions);
return Ok(new { accessToken });
}
- Implement Token Blacklisting
For high-security applications, implement a token revocation mechanism:
public class JwtMiddleware
{
private readonly RequestDelegate _next;
private readonly ITokenBlacklistService _blacklistService;
public async Task InvokeAsync(HttpContext context)
{
var token = ExtractTokenFromHeader(context.Request);
if (!string.IsNullOrEmpty(token) && await _blacklistService.IsBlacklisted(token))
{
context.Response.StatusCode = 401;
return;
}
await _next(context);
}
}
Common JWT Vulnerabilities to Avoid
- Algorithm Confusion Attacks
Never accept the "none" algorithm. Always specify allowed algorithms explicitly:
options.TokenValidationParameters = new TokenValidationParameters
{
ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256 }, // Specify allowed algorithms
// ... other validation parameters
};
- Weak Secret Keys
Avoid using weak or easily guessable secret keys. Use cryptographically secure random generators:
// Generate a secure key
var key = new byte[64];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(key);
}
var secretKey = Convert.ToBase64String(key);
- Missing Signature Verification
Always verify token signatures and validate all claims:
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var principal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken);
// Token is valid
}
catch (SecurityTokenException)
{
// Token is invalid
return Unauthorized();
}
Testing Your JWT-Secured ASP.NET Core API
Once your API is secured with JWT authentication, it's essential to test the endpoints to ensure everything works as expected. Here’s a step-by-step guide to testing your API using Postman, curl, or Swagger UI.
- Register a New User (Optional)
If your API supports registration, start here:
POST /api/auth/register
Body (JSON):
{
"email": "user@example.com",
"username": "user1",
"password": "P@ssw0rd!",
"firstName": "John",
"lastName": "Doe"
}
Expected Response:
201 Created or 200 OK with a confirmation message.
- Log In to Obtain a JWT Token
POST /api/auth/login
Body (JSON):
{
"email": "user@example.com",
"password": "P@ssw0rd!"
}
Expected Response:
{
"accessToken": "<your-jwt-token>",
"refreshToken": "<your-refresh-token>",
"expiresIn": 1800
}
Tip: Copy the accessToken for the next steps.
- Access a Protected Endpoint
GET /api/weatherforecast
Headers:
Authorization: Bearer <your-jwt-token>
Expected Response:
A list of weather forecasts (or your protected data).
If the token is missing or invalid:
You’ll get a 401 Unauthorized response.
- Test Role-Based Authorization
GET /api/weatherforecast/admin-only
Headers:
Authorization: Bearer <admin-jwt-token>
- If your token has the "Admin" role, you’ll see the admin data.
- Otherwise, you’ll get a 403 Forbidden
- Using Postman
- Set the request type and URL.
- For protected endpoints, go to the Authorization tab, choose Bearer Token, and paste your JWT.
- Alternatively, add the header manually:
Key: Authorization
Value: Bearer <your-jwt-token>
- Using Swagger UI
If you have Swagger enabled, click the Authorize button, enter your JWT as Bearer <your-jwt-token>, and test endpoints directly from the browser.
Common Testing Pitfalls
- Missing "Bearer" Prefix: Always include Bearer before your token.
- Expired Token: If you get 401, check if your token has expired.
- Role Issues: Ensure your user has the correct role for role-protected endpoints.
- HTTP vs HTTPS: Always use HTTPS in production to prevent token interception.
Error Responses to Expect
- 401 Unauthorized: Invalid, expired, or missing token.
- 403 Forbidden: Token is valid, but user lacks required role.
- 400 Bad Request: Malformed request or missing fields.
Related posts
-
What makes React.js so popular
What makes React so popular for frontend development?...
5 January 202604LikesBy Hiren Mahant -
ASP.NET Core security best practices
Why ASP.NET Core security matters...
5 January 202605LikesBy Hiren Mahant -
Next.js: The powerful framework built on React
You want performance, SEO, and full-stack capabilities?...
5 January 202604LikesBy Hiren Mahant