As a full-stack .NET developer, securing APIs is a critical task. Leveraging JWT (JSON Web Token) for authentication and authorization is a common and effective strategy. This article will guide you through implementing JWT access tokens and refresh tokens in the ASP.NET Core Web API.
Why use JWT?
JWT tokens are compact URL security tokens that are easily transferred between parties. They are self-contained, which means they carry information internally, reducing the need for server-side session storage.
Set up the project
First, create a new ASP.NET Core Web API project:
dotnet new webapi -n JwtAuthDemo
cd JwtAuthDemo
Add the necessary NuGet packages:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.IdentityModel.Tokens
dotnet add package System.IdentityModel.Tokens.Jwt
Configure JWT authentication
In , add the JWT settings :appsettings.json
{
"JwtSettings": {
"SecretKey": "your_secret_key",
"Issuer": "your_issuer",
"Audience": "your_audience",
"AccessTokenExpirationMinutes": 30,
"RefreshTokenExpirationDays": 7
}
}
更新以配置 JWT 身份验证:Program.cs
var builder = WebApplication.CreateBuilder(args);
// Load JWT settings
var jwtSettings = builder.Configuration.GetSection("JwtSettings").Get<JwtSettings>();
var secretKey = Encoding.UTF8.GetBytes(jwtSettings.SecretKey);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = jwtSettings.Issuer,
ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(secretKey)
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Create a JWT token service
Create a new class to handle token creation and validation: JwtTokenService
public class JwtTokenService
{
private readonly JwtSettings _jwtSettings;
public JwtTokenService(IOptions<JwtSettings> jwtSettings)
{
_jwtSettings = jwtSettings.Value;
}
public string GenerateAccessToken(IEnumerable<Claim> claims)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _jwtSettings.Issuer,
audience: _jwtSettings.Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(_jwtSettings.AccessTokenExpirationMinutes),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
}
Implement authentication endpoints
Create a new controller to handle login and token refresh: AuthController
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly JwtTokenService _jwtTokenService;
public AuthController(JwtTokenService jwtTokenService)
{
_jwtTokenService = jwtTokenService;
}
[HttpPost("login")]
public IActionResult Login([FromBody] LoginRequest request)
{
// Assuming the user is authenticated successfully
var claims = new[]
{
new Claim(ClaimTypes.Name, request.Username),
// Add other claims as needed
};
var accessToken = _jwtTokenService.GenerateAccessToken(claims);
var refreshToken = _jwtTokenService.GenerateRefreshToken();
// Save or update the refresh token in the database
return Ok(new { AccessToken = accessToken, RefreshToken = refreshToken });
}
[HttpPost("refresh")]
public IActionResult Refresh([FromBody] TokenRequest request)
{
// Validate the refresh token and generate a new access token
// This should include checking the refresh token against the stored value in the database
var claims = new[]
{
new Claim(ClaimTypes.Name, "username") // Replace with actual username from the refresh token
};
var newAccessToken = _jwtTokenService.GenerateAccessToken(claims);
Refresh the token endpoint
In , we need to implement the refresh token logic. This typically involves validating the refresh token, ensuring that it hasn't expired, and then generating a new access token. Here's how you can do it: AuthController
[HttpPost("refresh")]
public IActionResult Refresh([FromBody] TokenRequest request)
{
// Validate the refresh token (retrieve the stored refresh token from the database)
var storedRefreshToken = GetStoredRefreshToken(request.RefreshToken);
if (storedRefreshToken == || storedRefreshToken.ExpirationDate < DateTime.UtcNow)
{
return Unauthorized("Invalid or expired refresh token.");
}
// Assuming you have the username or user ID stored with the refresh token
var claims = new[]
{
new Claim(ClaimTypes.Name, storedRefreshToken.Username)
// Add other claims as needed
};
var newAccessToken = _jwtTokenService.GenerateAccessToken(claims);
var newRefreshToken = _jwtTokenService.GenerateRefreshToken();
// Update the stored refresh token
storedRefreshToken.Token = newRefreshToken;
storedRefreshToken.ExpirationDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays);
SaveRefreshToken(storedRefreshToken);
return Ok(new { AccessToken = newAccessToken, RefreshToken = newRefreshToken });
}
model
Create the necessary models for your requests and settings. For example, , , and :LoginRequestTokenRequestJwtSettings
public class LoginRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
public class TokenRequest
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
}
public class JwtSettings
{
public string SecretKey { get; set; }
public string Issuer { get; set; }
public string Audience { get; set; }
public int AccessTokenExpirationMinutes { get; set; }
public int RefreshTokenExpirationDays { get; set; }
}
Store and validate refresh tokens
For the sake of simplicity, let's assume you have a way to store and retrieve refresh tokens from the database. Here's a basic example of using a hypothetical data access layer:
public class RefreshToken
{
public string Token { get; set; }
public string Username { get; set; }
public DateTime ExpirationDate { get; set; }
}
public RefreshToken GetStoredRefreshToken(string refreshToken)
{
// Implement your logic to retrieve the refresh token from the database
// For example, using Entity Framework Core:
// return _context.RefreshTokens.SingleOrDefault(rt => rt.Token == refreshToken);
return ;
}
public void SaveRefreshToken(RefreshToken refreshToken)
{
// Implement your logic to save the refresh token to the database
// For example, using Entity Framework Core:
// _context.RefreshTokens.Update(refreshToken);
// _context.SaveChanges();
}
summary
With these components in place, you can provide a robust setup for JWT-based authentication in the ASP.NET Core Web API. Here's a brief overview of what we've covered:
- Project Setup: A new ASP.NET Core Web API project was created and the necessary NuGet packages were added.
- JWT configuration: JWT settings are configured in and . appsettings.jsonProgram.cs
- Token Service: Used to generate and validate tokens. JwtTokenService
- Authentication endpoint: Use sign-in and token refresh endpoint creation. AuthController
- Model and storage: The model for the request is defined and the methods for storing and retrieving refresh tokens are implemented.
By following these steps, you can ensure that your API is secure and that users can maintain their sessions without having to constantly re-authenticate thanks to the refresh token mechanism. This setup not only enhances security, but also improves the user experience by providing seamless access to protected resources.
Protect your endpoints
Once you've configured JWT authentication, you'll need to secure your API endpoints to ensure that only authenticated users can access them. You can use this property to protect a controller or a specific operation. [Authorize]
例如,要保护整个:WeatherForecastController
[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
// Your actions here
}
Handle token expiration
JWT has an expiration time, and once it expires, the client needs to use the refresh token to obtain a new access token. Properly handling token expiration is critical to maintaining security and user experience.
Here's an example of how token expiration can be handled on the client side (for example, using JavaScript/TypeScript in a front-end application):
async function fetchWithAuth(url, options = {}) {
let response = await fetch(url, options);
if (response.status === 401) {
// Token might be expired, try to refresh it
const refreshResponse = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
accessToken: localStorage.getItem('accessToken'),
refreshToken: localStorage.getItem('refreshToken')
})
});
if (refreshResponse.ok) {
const { accessToken, refreshToken } = await refreshResponse.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
// Retry the original request
options.headers = {
...options.headers,
'Authorization': `Bearer ${accessToken}`
};
response = await fetch(url, options);
}
}
return response;
}
Best practices for managing refresh tokens
- Store refresh tokens securely: Refresh tokens are sensitive and should be stored securely. It's best to use HTTP-only cookies to store refresh tokens, as they are less vulnerable to XSS attacks.
- Rotate refresh tokens: Each time a refresh token is used, a new pair of access and refresh tokens is generated. This limits the lifecycle of a stolen refresh token.
- Used token blacklist: Keep a list of invalid tokens to prevent reuse. This can be achieved using a database or in-memory storage.
- Limit the number of refresh attempts: Implement a mechanism to limit the number of refresh tokens that can be used to prevent abuse.
An example of using HTTP-only cookies
The following example illustrates how to issue and process refresh tokens using HTTP-only cookies in HTTP: AuthController
[HttpPost("login")]
public IActionResult Login([FromBody] LoginRequest request)
{
// Assuming the user is authenticated successfully
var claims = new[]
{
new Claim(ClaimTypes.Name, request.Username),
// Add other claims as needed
};
var accessToken = _jwtTokenService.GenerateAccessToken(claims);
var refreshToken = _jwtTokenService.GenerateRefreshToken();
// Save or update the refresh token in the database
SaveRefreshToken(new RefreshToken
{
Token = refreshToken,
Username = request.Username,
ExpirationDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays)
});
// Set the refresh token as an HTTP-only cookie
Response.Cookies.Append("refreshToken", refreshToken, new CookieOptions
{
HttpOnly = true,
Secure = true, // Set to true in production
Expires = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays)
});
return Ok(new { AccessToken = accessToken });
}
[HttpPost("refresh")]
public IActionResult Refresh()
{
var refreshToken = Request.Cookies["refreshToken"];
if (string.IsOrEmpty(refreshToken))
{
return Unauthorized("Refresh token is missing.");
}
var storedRefreshToken = GetStoredRefreshToken(refreshToken);
if (storedRefreshToken == || storedRefreshToken.ExpirationDate < DateTime.UtcNow)
{
return Unauthorized("Invalid or expired refresh token.");
}
var claims = new[]
{
new Claim(ClaimTypes.Name, storedRefreshToken.Username)
};
var newAccessToken = _jwtTokenService.GenerateAccessToken(claims);
var newRefreshToken = _jwtTokenService.GenerateRefreshToken
Rotate refresh tokens
As mentioned earlier, it's a good practice to rotate the refresh token every time you use it. This means that every time you use a refresh token to get a new access token, you should also generate a new refresh token and send it to the client. This limits the potential damage if the refresh token is compromised.
Update the refresh token endpoint in to handle token rotation in AuthController
[HttpPost("refresh")]
public IActionResult Refresh()
{
var oldRefreshToken = Request.Cookies["refreshToken"];
if (string.IsOrEmpty(oldRefreshToken))
{
return Unauthorized("Refresh token is missing.");
}
var storedRefreshToken = GetStoredRefreshToken(oldRefreshToken);
if (storedRefreshToken == || storedRefreshToken.ExpirationDate < DateTime.UtcNow)
{
return Unauthorized("Invalid or expired refresh token.");
}
var claims = new[]
{
new Claim(ClaimTypes.Name, storedRefreshToken.Username)
};
var newAccessToken = _jwtTokenService.GenerateAccessToken(claims);
var newRefreshToken = _jwtTokenService.GenerateRefreshToken();
// Update the stored refresh token
storedRefreshToken.Token = newRefreshToken;
storedRefreshToken.ExpirationDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays);
SaveRefreshToken(storedRefreshToken);
// Set the new refresh token as an HTTP-only cookie
Response.Cookies.Append("refreshToken", newRefreshToken, new CookieOptions
{
HttpOnly = true,
Secure = true, // Set to true in production
Expires = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays)
});
return Ok(new { AccessToken = newAccessToken });
}
Handle user logouts
When a user logs out, you should invalidate their refresh token to prevent it from being used to generate a new access token. You can do this by removing the refresh token from the database and clearing the HTTP-only cookies.
Add the logout endpoint to your: AuthController
[HttpPost("logout")]
public IActionResult Logout()
{
var refreshToken = Request.Cookies["refreshToken"];
if (!string.IsOrEmpty(refreshToken))
{
// Remove the refresh token from the database
RemoveRefreshToken(refreshToken);
// Clear the HTTP-only cookie
Response.Cookies.Delete("refreshToken");
}
return NoContent();
}
Improve security
Here are some other safety considerations to keep in mind:
- Use HTTPS: Always use HTTPS to encrypt data in transit between the client and server, including tokens. This prevents man-in-the-middle attacks.
- Short-term access token: Keep the short-term access token for the access token (for example, 15-30 minutes). This limits the time window for attackers to manage to steal access tokens.
- Revoke tokens when changing passwords: If a user changes a password or performs a sensitive action, revoke all refresh tokens associated with the user to prevent unauthorized access.
- Rate limiting: Implement rate limiting on authentication endpoints to prevent brute force attacks.
示例:Entity Framework Core 集成
For real-world applications, a database is typically used to store refresh tokens. Here's an example of how to integrate Entity Framework Core to manage refresh tokens:
- Define the RefreshToken entity:
public class RefreshToken
{
public int Id { get; set; }
public string Token { get; set; }
public string Username { get; set; }
public DateTime ExpirationDate { get; set; }
}
2. 将 DbSet 添加到 DbContext:
public class ApplicationDbContext : DbContext
{
public DbSet<RefreshToken> RefreshTokens { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
3. Implement a way to manage refresh tokens:
public class RefreshTokenService
{
private readonly ApplicationDbContext _context;
public RefreshTokenService(ApplicationDbContext context)
{
_context = context;
}
public void SaveRefreshToken(RefreshToken refreshToken)
{
_context.RefreshTokens.Update(refreshToken);
_context.SaveChanges();
}
public RefreshToken GetStoredRefreshToken(string token)
{
return _context.RefreshTokens.SingleOrDefault(rt => rt.Token == token);
}
public void RemoveRefreshToken(string token)
{
var refreshToken = GetStoredRefreshToken(token);
if (refreshToken != )
{
_context.RefreshTokens.Remove(refreshToken
If you like my article, please give me a like! Thank you