Implementing token-based authentication in .NET Core typically involves using JSON Web Tokens (JWTs) to secure your APIs, allowing clients to receive an access token after successful login, which they then use to authenticate subsequent requests. This process includes generating, validating, and managing tokens, often alongside refresh tokens for enhanced security and user experience.
How to Implement Token-Based Authentication in .NET Core
Token-based authentication provides a stateless and scalable way to secure web APIs by issuing signed tokens that verify a user's identity and authorization claims without requiring session management on the server. Here’s a detailed guide on how to implement this using JWTs in .NET Core.
Understanding Token-Based Authentication and JWTs
Before diving into implementation, it's crucial to grasp the core components:
- Access Token (JWT): A compact, URL-safe means of representing claims to be transferred between two parties. It contains information about the user and their permissions, signed to prevent tampering.
- Refresh Token: A long-lived token used to obtain a new access token once the current one expires, without requiring the user to re-authenticate with their credentials. This enhances security by keeping access tokens short-lived.
A JWT consists of three parts, separated by dots: Header.Payload.Signature
.
Part | Description | Example Content |
---|---|---|
Header | Specifies the token type (JWT) and the signing algorithm used. | {"alg": "HS256", "typ": "JWT"} |
Payload | Contains the claims (assertions about the entity and additional data). | {"sub": "1234567890", "name": "John Doe", "iat": 1516239022, "exp": 1516242622} |
Signature | Used to verify that the sender of the JWT is who it says it is and that the message hasn't been changed along the way. | Cryptographic hash of header, payload, and a secret key. |
Step-by-Step Implementation
The implementation process covers creating a web application, setting up token-based authorization, and incorporating refresh tokens.
1. Create a .NET Core Web API Project
Begin by setting up a new ASP.NET Core Web API project. This will serve as the foundation for your authentication system.
dotnet new webapi -n MyAuthApi
cd MyAuthApi
2. Install Necessary NuGet Packages
You'll need the Microsoft.AspNetCore.Authentication.JwtBearer
package to enable JWT authentication in your application.
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.EntityFrameworkCore.InMemory # For simplicity, can use for user/refresh token storage
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore # If using ASP.NET Core Identity
3. Configure JWT Authentication
In your Program.cs
(for .NET 6+ Minimal APIs) or Startup.cs
(for older versions), configure the JWT Bearer authentication scheme. This involves defining how to validate incoming tokens.
// Program.cs
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();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// --- JWT Configuration ---
var jwtSettings = builder.Configuration.GetSection("Jwt");
var key = Encoding.ASCII.GetBytes(jwtSettings["Key"]);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false; // Set to true in production
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidIssuer = jwtSettings["Issuer"],
ValidateAudience = true,
ValidAudience = jwtSettings["Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero // The maximum allowed timespan between the token's expiration and the current time
};
});
builder.Services.AddAuthorization(); // Ensure authorization services are added
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// --- Enable Authentication and Authorization ---
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
You also need to add JWT configuration to your appsettings.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Jwt": {
"Key": "ThisIsMySuperSecretKeyForJWTAuth_DoNotShare!", // Keep this secret and long
"Issuer": "https://yourdomain.com",
"Audience": "https://yourdomain.com"
}
}
4. Implement User Registration and Login
Create an authentication controller (e.g., AuthController.cs
) with endpoints for user registration and login. Upon successful login, you'll generate a JWT for the user.
// Models for request/response
public class LoginRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
public class AuthResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; } // Will be added later
}
// AuthController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _configuration;
public AuthController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] LoginRequest model)
{
// For demonstration: In a real app, hash password and save to DB
if (model.Username == "test" && model.Password == "password")
{
return Ok("User registered successfully (demo)");
}
return BadRequest("Registration failed (demo)");
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest model)
{
// For demonstration: Validate user against a mock database
if (model.Username != "user" || model.Password != "pass")
{
return Unauthorized("Invalid credentials.");
}
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, "1"),
new Claim(ClaimTypes.Name, model.Username),
new Claim(ClaimTypes.Role, "Admin") // Example role
};
var accessToken = GenerateAccessToken(claims);
var refreshToken = GenerateRefreshToken(); // Placeholder for now
return Ok(new AuthResponse { AccessToken = accessToken, RefreshToken = refreshToken });
}
private string GenerateAccessToken(IEnumerable<Claim> claims)
{
var jwtSettings = _configuration.GetSection("Jwt");
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"]));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(15), // Short-lived access token
Issuer = jwtSettings["Issuer"],
Audience = jwtSettings["Audience"],
SigningCredentials = credentials
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
private string GenerateRefreshToken()
{
// Implement refresh token generation and storage here
// For now, return a simple GUID
return Guid.NewGuid().ToString();
}
}
5. Secure API Endpoints
Apply the [Authorize]
attribute to controllers or individual action methods you want to protect. This ensures that only requests with a valid JWT can access these resources.
// WeatherForecastController.cs (or any new secured controller)
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("[controller]")]
[Authorize] // This controller requires authentication
public class SecuredDataController : ControllerBase
{
[HttpGet]
public IActionResult GetSecuredData()
{
var userName = User.Identity.Name; // Access user claims from the token
return Ok($"Hello {userName}! You have access to secured data.");
}
[HttpGet("admin-only")]
[Authorize(Roles = "Admin")] // Requires 'Admin' role
public IActionResult GetAdminData()
{
return Ok("This data is only for administrators.");
}
}
6. Implement Refresh Tokens
Refresh tokens are crucial for improving security and user experience by allowing access tokens to be short-lived.
-
Generate and Store Refresh Tokens: When a user logs in, generate both an access token and a refresh token. The refresh token should be stored securely, typically in a database, along with its expiration date and associated user ID.
// Inside AuthController.cs (modified Login method) // ... private string GenerateRefreshToken(string userId) { var randomNumber = new byte[32]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber); } // In a real app, store this token with userId, creation date, expiry date, and revoked status in DB. // Example: _dbContext.RefreshTokens.Add(new RefreshToken { Token = token, UserId = userId, ExpiryDate = DateTime.UtcNow.AddDays(7) }); // _dbContext.SaveChanges(); } // ... // Update AuthResponse to hold RefreshToken public class AuthResponse { public string AccessToken { get; set; } public string RefreshToken { get; set; } } // In Login: var refreshToken = GenerateRefreshToken(claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value); // ... return new AuthResponse { AccessToken = accessToken, RefreshToken = refreshToken };
-
Refresh Token Endpoint: Create an endpoint (
/refresh
) where clients can send their expired access token and valid refresh token to obtain a new pair.using System.Security.Cryptography; // For RandomNumberGenerator // ... inside AuthController.cs public class RefreshTokenRequest { public string AccessToken { get; set; } public string RefreshToken { get; set; } } [HttpPost("refresh")] public async Task<IActionResult> Refresh([FromBody] RefreshTokenRequest request) { var principal = GetPrincipalFromExpiredToken(request.AccessToken); if (principal == null) { return BadRequest("Invalid access token."); } var userId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userId)) { return BadRequest("Invalid access token claims."); } // --- DEMO ONLY: In a real app, retrieve refresh token from DB and validate --- // var storedRefreshToken = _dbContext.RefreshTokens.SingleOrDefault(rt => rt.Token == request.RefreshToken && rt.UserId == userId); // if (storedRefreshToken == null || !storedRefreshToken.IsActive) // { // return BadRequest("Invalid or expired refresh token."); // } // Generate new access token and refresh token var newAccessToken = GenerateAccessToken(principal.Claims); var newRefreshToken = GenerateRefreshToken(userId); // --- DEMO ONLY: In a real app, update stored refresh token --- // storedRefreshToken.RevokedDate = DateTime.UtcNow; // Revoke old refresh token // _dbContext.RefreshTokens.Add(new RefreshToken { Token = newRefreshToken, UserId = userId, ExpiryDate = DateTime.UtcNow.AddDays(7) }); // await _dbContext.SaveChangesAsync(); return Ok(new AuthResponse { AccessToken = newAccessToken, RefreshToken = newRefreshToken }); } private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) { var jwtSettings = _configuration.GetSection("Jwt"); 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 need to validate expiration here, but not reject if expired }; var tokenHandler = new JwtSecurityTokenHandler(); SecurityToken securityToken; var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken); var jwtSecurityToken = securityToken as JwtSecurityToken; if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) { throw new SecurityTokenException("Invalid token."); } return principal; }
7. Testing the API
Use tools like Postman or Swagger UI (which you enabled with AddSwaggerGen()
) to test your authentication flow:
- Register: Send a POST request to
/api/auth/register
to create a mock user. - Login: Send a POST request to
/api/auth/login
with user credentials. You should receive anaccessToken
andrefreshToken
. - Access Secured Endpoint: Take the
accessToken
and include it in theAuthorization
header as a Bearer token (e.g.,Authorization: Bearer <your_access_token>
) for a GET request to/secureddata
. - Refresh Token: Once the access token expires (or if you manually set a very short expiry for testing), send a POST request to
/api/auth/refresh
with the expired access token and the valid refresh token to get a new pair. - Access Admin-Only Endpoint: If your user has the "Admin" role, try accessing
/secureddata/admin-only
with the appropriate access token.
Practical Considerations and Best Practices
- HTTPS: Always use HTTPS in production to protect tokens from interception.
- Secret Key Management: The JWT secret key (
Jwt:Key
) must be strong and kept confidential. Never hardcode it in production; use environment variables or a secure key vault. - Token Expiration:
- Access Tokens: Keep them short-lived (e.g., 15-30 minutes) to minimize the impact of compromise.
- Refresh Tokens: Make them longer-lived (e.g., days or weeks) and implement a robust revocation mechanism.
- Refresh Token Revocation: Allow users to revoke their refresh tokens (e.g., when logging out of all devices). This is critical for security.
- Token Storage on Client: Store access tokens and refresh tokens securely on the client-side. For web applications,
HttpOnly
cookies for refresh tokens andlocalStorage
/sessionStorage
for access tokens (with XSS precautions) are common approaches. - Claims: Be mindful of what information you put into JWT claims. Avoid sensitive data, and keep the token payload minimal to reduce its size.
- Error Handling: Implement robust error handling for authentication failures and token validation issues.
By following these steps, you can effectively implement secure token-based authentication, including refresh tokens, in your ASP.NET Core applications, providing a robust and scalable solution for API security.