Now that the framework is configured to understand JWT tokens, we must build the mechanism that actually logs the user in and generates the token using System.IdentityModel.Tokens.Jwt.
The login endpoint must be entirely exempt from Security (using the [AllowAnonymous] attribute), otherwise no one could ever log in!
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _config;
private readonly IUserRepository _userRepo;
public AuthController(IConfiguration config, IUserRepository userRepo)
{
_config = config;
_userRepo = userRepo;
}
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginDto request)
{
// 1. Verify credentials against the DB (Using BCrypt/Identity Framework)
var user = await _userRepo.FindByEmailAsync(request.Email);
if (user == null || !BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash))
{
return Unauthorized(new { error = "Invalid Credentials" });
}
// 2. Generate the Token String!
var tokenString = GenerateJwtToken(user);
return Ok(new { token = tokenString });
}
}
Generating the token requires defining "Claims", setting an Expiration date, and digitally signing it using the Secret Key.
private string GenerateJwtToken(User user)
{
// 1. Establish the Cryptographic Keys
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
// 2. Define the Claims (The public data stored inside the token payload)
var claims = new[]
{
// Standard JWT identifiers
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
// Custom Role logic
new Claim(ClaimTypes.Role, user.Role) // e.g. "Admin" or "User"
};
// 3. Assemble the Token
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(60), // Token dies strictly in 60 mins
signingCredentials: credentials);
// 4. Serialize to string format: "eyJhb..."
return new JwtSecurityTokenHandler().WriteToken(token);
}
Once a user is successfully Authenticated and hits a protected endpoint like [Authorize], ASP.NET Core automatically unpacks their token and loads those claims into the User.Claims property inside the ControllerBase.
[ApiController]
[Route("api/profile")]
[Authorize] // Reject anyone without a valid token
public class ProfileController : ControllerBase
{
[HttpGet("my-data")]
public IActionResult GetMyProfile()
{
// Because the [Authorize] tag passed, we GUARANTEE the User object exists
// Extract the Subject (User ID) from the claims we injected earlier!
var userIdString = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var userEmail = User.FindFirst(ClaimTypes.Email)?.Value;
if (!int.TryParse(userIdString, out int userId)) return Unauthorized();
var bankingData = _db.BankAccounts.Where(b => b.OwnerId == userId);
return Ok(bankingData);
}
}
Q: "Why is it absolutely critical that you rely on the `ClaimTypes.NameIdentifier` extracted from the Context, rather than allowing the frontend to pass the user ID in the URL (`/api/profile/my-data?userId=5`)?
Architect Answer: "That is the definition of an Insecure Direct Object Reference (IDOR) vulnerability. If your endpoint accepts the ID from the URL or JSON body, a malicious user can log in, get a valid token, but simply change the URL from `userId=5` to `userId=6`. The server sees they have a valid token, queries the DB for ID=6, and returns someone else's data. Because JWT Cryptography is impenetrable, the Claims inside the token are the ONLY source of truth. We extract the ID directly from the secure Token Claims on the server-side, guaranteeing that a user can only ever access data specifically tied to the ID they were authenticated against."