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(),
|
||||
PasswordHash = passwordHash,
|
||||
CurrentLevel = "A1",
|
||||
Role = "User", // Explicitly set default role
|
||||
Streak = 0,
|
||||
TotalPoints = 0,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public static class SeedDataExtension
|
|||
"admin@deutschlernen.com",
|
||||
"$2a$11$N9qo8uLOickgx2ZMRZoMy.Mrq9Hq4yVJyX2sX7z3J0J9z6J9Z2K4a" // bcrypt hash of "Admin@123!"
|
||||
);
|
||||
adminUser.AssignAdminRole(); // Set role to Admin
|
||||
adminUser.UpdateLevel("C1");
|
||||
dbContext.Users.Add(adminUser);
|
||||
|
||||
|
|
|
|||
|
|
@ -49,11 +49,12 @@ public class AuthService : IAuthService
|
|||
/// </summary>
|
||||
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))
|
||||
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");
|
||||
|
||||
// Hash password and create user
|
||||
|
|
@ -89,7 +90,10 @@ public class AuthService : IAuthService
|
|||
/// </summary>
|
||||
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)
|
||||
throw new UnauthorizedAccessException("Invalid email or password");
|
||||
|
|
@ -241,11 +245,12 @@ public class AuthService : IAuthService
|
|||
if (await _dbContext.Users.AnyAsync(u => u.Role == "Admin"))
|
||||
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))
|
||||
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");
|
||||
|
||||
// Hash password and create user
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public class AuthController : ControllerBase
|
|||
/// <param name="registerDto">Registration data</param>
|
||||
/// <returns>Authentication response with JWT token</returns>
|
||||
[HttpPost("register")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(AuthResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
|
||||
public async Task<IActionResult> Register([FromBody] RegisterDto registerDto)
|
||||
|
|
@ -52,6 +53,7 @@ public class AuthController : ControllerBase
|
|||
/// <param name="loginDto">Login data</param>
|
||||
/// <returns>Authentication response with JWT token</returns>
|
||||
[HttpPost("login")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(AuthResponse), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(string), (int)HttpStatusCode.Unauthorized)]
|
||||
public async Task<IActionResult> Login([FromBody] LoginDto loginDto)
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ try
|
|||
ValidIssuer = jwtIssuer,
|
||||
ValidAudience = jwtAudience,
|
||||
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