fix(backend/auth): Fix authentication bugs
- Fix case-insensitive email lookups in AuthService (RegisterAsync, LoginAsync, CreateAdminUserAsync) - Fix User.Create factory method to explicitly set Role property (C# object initializer behavior) - Assign Admin role to seeded admin user in SeedDataExtension - Add [AllowAnonymous] to Register and Login endpoints in AuthController (defensive programming) - Increase JWT ClockSkew to 5 minutes for clock difference tolerance These fixes resolve issues where: - Login fails with 401 due to case-sensitive email comparison - User role is null instead of 'User' or 'Admin' - JWT token validation too strict on clock synchronization Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
70af326ca7
commit
42778ec34b
5 changed files with 15 additions and 6 deletions
|
|
@ -31,6 +31,7 @@ public class User
|
||||||
Email = email.ToLowerInvariant(),
|
Email = email.ToLowerInvariant(),
|
||||||
PasswordHash = passwordHash,
|
PasswordHash = passwordHash,
|
||||||
CurrentLevel = "A1",
|
CurrentLevel = "A1",
|
||||||
|
Role = "User", // Explicitly set default role
|
||||||
Streak = 0,
|
Streak = 0,
|
||||||
TotalPoints = 0,
|
TotalPoints = 0,
|
||||||
CreatedAt = DateTime.UtcNow
|
CreatedAt = DateTime.UtcNow
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ public static class SeedDataExtension
|
||||||
"admin@deutschlernen.com",
|
"admin@deutschlernen.com",
|
||||||
"$2a$11$N9qo8uLOickgx2ZMRZoMy.Mrq9Hq4yVJyX2sX7z3J0J9z6J9Z2K4a" // bcrypt hash of "Admin@123!"
|
"$2a$11$N9qo8uLOickgx2ZMRZoMy.Mrq9Hq4yVJyX2sX7z3J0J9z6J9Z2K4a" // bcrypt hash of "Admin@123!"
|
||||||
);
|
);
|
||||||
|
adminUser.AssignAdminRole(); // Set role to Admin
|
||||||
adminUser.UpdateLevel("C1");
|
adminUser.UpdateLevel("C1");
|
||||||
dbContext.Users.Add(adminUser);
|
dbContext.Users.Add(adminUser);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,12 @@ public class AuthService : IAuthService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<AuthResponse> RegisterAsync(RegisterDto registerDto)
|
public async Task<AuthResponse> RegisterAsync(RegisterDto registerDto)
|
||||||
{
|
{
|
||||||
// Check if username or email already exists
|
// Check if username or email already exists (case-insensitive for email)
|
||||||
if (await _dbContext.Users.AnyAsync(u => u.Username == registerDto.Username))
|
if (await _dbContext.Users.AnyAsync(u => u.Username == registerDto.Username))
|
||||||
throw new InvalidOperationException("Username already taken");
|
throw new InvalidOperationException("Username already taken");
|
||||||
|
|
||||||
if (await _dbContext.Users.AnyAsync(u => u.Email == registerDto.Email))
|
var normalizedEmail = registerDto.Email?.ToLowerInvariant() ?? string.Empty;
|
||||||
|
if (await _dbContext.Users.AnyAsync(u => u.Email == normalizedEmail))
|
||||||
throw new InvalidOperationException("Email already in use");
|
throw new InvalidOperationException("Email already in use");
|
||||||
|
|
||||||
// Hash password and create user
|
// Hash password and create user
|
||||||
|
|
@ -89,7 +90,10 @@ public class AuthService : IAuthService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<AuthResponse> LoginAsync(LoginDto loginDto)
|
public async Task<AuthResponse> LoginAsync(LoginDto loginDto)
|
||||||
{
|
{
|
||||||
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Email == loginDto.Email);
|
// Normalize email to lowercase for case-insensitive matching
|
||||||
|
// Emails are stored in lowercase in the database
|
||||||
|
var normalizedEmail = loginDto.Email?.ToLowerInvariant() ?? string.Empty;
|
||||||
|
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Email == normalizedEmail);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new UnauthorizedAccessException("Invalid email or password");
|
throw new UnauthorizedAccessException("Invalid email or password");
|
||||||
|
|
@ -241,11 +245,12 @@ public class AuthService : IAuthService
|
||||||
if (await _dbContext.Users.AnyAsync(u => u.Role == "Admin"))
|
if (await _dbContext.Users.AnyAsync(u => u.Role == "Admin"))
|
||||||
throw new InvalidOperationException("Admin user already exists. Bootstrap endpoint can only be used once.");
|
throw new InvalidOperationException("Admin user already exists. Bootstrap endpoint can only be used once.");
|
||||||
|
|
||||||
// Check if username or email already exists
|
// Check if username or email already exists (case-insensitive for email)
|
||||||
if (await _dbContext.Users.AnyAsync(u => u.Username == registerDto.Username))
|
if (await _dbContext.Users.AnyAsync(u => u.Username == registerDto.Username))
|
||||||
throw new InvalidOperationException("Username already taken");
|
throw new InvalidOperationException("Username already taken");
|
||||||
|
|
||||||
if (await _dbContext.Users.AnyAsync(u => u.Email == registerDto.Email))
|
var normalizedEmail = registerDto.Email?.ToLowerInvariant() ?? string.Empty;
|
||||||
|
if (await _dbContext.Users.AnyAsync(u => u.Email == normalizedEmail))
|
||||||
throw new InvalidOperationException("Email already in use");
|
throw new InvalidOperationException("Email already in use");
|
||||||
|
|
||||||
// Hash password and create user
|
// Hash password and create user
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ public class AuthController : ControllerBase
|
||||||
/// <param name="registerDto">Registration data</param>
|
/// <param name="registerDto">Registration data</param>
|
||||||
/// <returns>Authentication response with JWT token</returns>
|
/// <returns>Authentication response with JWT token</returns>
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
|
[AllowAnonymous]
|
||||||
[ProducesResponseType(typeof(AuthResponse), (int)HttpStatusCode.OK)]
|
[ProducesResponseType(typeof(AuthResponse), (int)HttpStatusCode.OK)]
|
||||||
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
|
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
|
||||||
public async Task<IActionResult> Register([FromBody] RegisterDto registerDto)
|
public async Task<IActionResult> Register([FromBody] RegisterDto registerDto)
|
||||||
|
|
@ -52,6 +53,7 @@ public class AuthController : ControllerBase
|
||||||
/// <param name="loginDto">Login data</param>
|
/// <param name="loginDto">Login data</param>
|
||||||
/// <returns>Authentication response with JWT token</returns>
|
/// <returns>Authentication response with JWT token</returns>
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
|
[AllowAnonymous]
|
||||||
[ProducesResponseType(typeof(AuthResponse), (int)HttpStatusCode.OK)]
|
[ProducesResponseType(typeof(AuthResponse), (int)HttpStatusCode.OK)]
|
||||||
[ProducesResponseType(typeof(string), (int)HttpStatusCode.Unauthorized)]
|
[ProducesResponseType(typeof(string), (int)HttpStatusCode.Unauthorized)]
|
||||||
public async Task<IActionResult> Login([FromBody] LoginDto loginDto)
|
public async Task<IActionResult> Login([FromBody] LoginDto loginDto)
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ try
|
||||||
ValidIssuer = jwtIssuer,
|
ValidIssuer = jwtIssuer,
|
||||||
ValidAudience = jwtAudience,
|
ValidAudience = jwtAudience,
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
|
||||||
ClockSkew = TimeSpan.Zero
|
ClockSkew = TimeSpan.FromMinutes(5) // Allow 5 minutes of clock difference
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue