AuthService implements centralized error handling through the ExceptionHandlingMiddleware, which catches all unhandled exceptions and returns consistent, secure error responses.
ArgumentException is typically thrown during input validation. While not explicitly thrown in the current codebase, you can add validation like:
if (string.IsNullOrEmpty(request.Email)) throw new ArgumentException("Email is required.");if (!IsValidEmail(request.Email)) throw new ArgumentException("Invalid email format.");
The service uses generic error messages like “Credenciales inválidas” to avoid leaking information about which part failed (email vs password). This prevents attackers from enumerating valid email addresses (AuthService.cs:72-76).
Usage: Unexpected server errorTriggered by: Any unhandled exception typeCommon Scenarios:
Database connection failure
Null reference exceptions
Unexpected runtime errors
Example Response (ExceptionHandlingMiddleware.cs:40-42):
{ "status": 500, "message": "Ocurrió un error interno.", "timestamp": "2026-03-10T14:15:00Z"}
500 errors are logged with full stack trace (ExceptionHandlingMiddleware.cs:40-41), but only a generic message is returned to the client to avoid leaking sensitive information:
if (statusCode == HttpStatusCode.InternalServerError) _logger.LogError(exception, "Error no controlado");
if (user == null){ throw new UnauthorizedAccessException("Credenciales inválidas.");}if (!_passwordService.Verify(request.Password, user.PasswordHash)){ user.FailedLoginAttempts++; // ... throw new UnauthorizedAccessException("Credenciales inválidas.");}
Response: 401 Unauthorized
2
Account locked
Code: AuthService.cs:81-86
if (user.IsLockedOut()){ var remaining = (int)(user.LockoutEnd!.Value - DateTime.UtcNow).TotalMinutes + 1; throw new UnauthorizedAccessException( $"Cuenta bloqueada temporalmente. Intenta en {remaining} minuto(s).");}
Response: 401 Unauthorized with lockout duration
Failed login attempts are tracked in AuthService.cs:90. After 5 attempts, the account is locked for 15 minutes (AuthService.cs:26-27).
3
Account deactivated
Code: AuthService.cs:78-79
if (!user.IsActive) throw new UnauthorizedAccessException("Cuenta desactivada.");
if (storedToken == null) throw new UnauthorizedAccessException("Refresh token inválido.");
Response: 401 Unauthorized
2
Token reuse attack detected
Code: AuthService.cs:125-132
if (storedToken.IsRevoked){ _logger.LogWarning( "Posible token reuse attack detectado para usuario {UserId}", storedToken.UserId); await RevokeDescendantTokensAsync(storedToken, "Token reuse detectado"); await _db.SaveChangesAsync(); throw new UnauthorizedAccessException("Refresh token revocado.");}
Response: 401 Unauthorized
If a revoked token is reused, the service detects a potential attack and revokes all tokens in that family. This is part of the refresh token rotation security strategy.
3
Expired refresh token
Code: AuthService.cs:134-135
if (storedToken.IsExpired) throw new UnauthorizedAccessException("Refresh token expirado.");
Response: 401 Unauthorized
Refresh tokens expire after 7 days by default (configurable via Jwt:RefreshTokenExpiryDays).
// 401 Unauthorizedthrow new UnauthorizedAccessException("Invalid token");// 404 Not Foundthrow new KeyNotFoundException("User not found");// 409 Conflictthrow new InvalidOperationException("Email already exists");// 400 Bad Requestthrow new ArgumentException("Invalid email format");
2
Add custom exception types for complex cases
For scenarios not covered by standard exceptions:
public class RateLimitExceededException : Exception{ public RateLimitExceededException(string message) : base(message) { }}// Update middleware:RateLimitExceededException => (HttpStatusCode.TooManyRequests, exception.Message),
3
Log contextual information
Include relevant data in logs:
_logger.LogWarning( "Failed login attempt for {Email} from {IP}", email, ipAddress);
public class RateLimitExceededException : Exception{ public int RetryAfterSeconds { get; } public RateLimitExceededException(int retryAfter) : base($"Rate limit exceeded. Retry after {retryAfter} seconds.") { RetryAfterSeconds = retryAfter; }}