AndanDo uses a robust JWT (JSON Web Token) authentication system built on ASP.NET Core Identity principles. The system provides secure user registration, login, password management, and session persistence using PBKDF2 password hashing.
JWT Tokens
Stateless authentication with cryptographically signed tokens
PBKDF2 Hashing
100,000 iterations with SHA256 for secure password storage
public bool VerifyPassword(string password, string passwordHash){ var parts = passwordHash.Split(':'); if (parts.Length != 2) return false; var salt = Convert.FromBase64String(parts[0]); var storedHash = Convert.FromBase64String(parts[1]); var computed = Rfc2898DeriveBytes.Pbkdf2( password, salt, Pbkdf2Iterations, HashAlgorithmName.SHA256, Pbkdf2KeySize); return CryptographicOperations.FixedTimeEquals(storedHash, computed);}
CryptographicOperations.FixedTimeEquals prevents timing attacks by ensuring comparison takes constant time regardless of match position.
Blazor Server uses a scoped UserSession service to maintain authentication state:
UserSession.cs
public sealed class UserSession{ public AuthResult? Current { get; private set; } public bool IsAuthenticated => Current is not null; public void SetUser(AuthResult authResult) { Current = authResult; } public void Clear() { Current = null; }}
public async Task<bool> IsUserInRoleAsync( int userId, int roleId, CancellationToken cancellationToken = default){ if (userId <= 0) { return false; } await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(cancellationToken); const string sql = """ SELECT 1 FROM UsuarioRoles WHERE UsuarioId = @UsuarioId AND RolId = @RolId """; await using var command = new SqlCommand(sql, connection); command.Parameters.Add(new SqlParameter("@UsuarioId", SqlDbType.Int) { Value = userId }); command.Parameters.Add(new SqlParameter("@RolId", SqlDbType.Int) { Value = roleId }); var result = await command.ExecuteScalarAsync(cancellationToken); return result is not null;}
public async Task ChangePasswordAsync( ChangePasswordRequest request, CancellationToken cancellationToken = default){ await using var connection = new SqlConnection(_connectionString); await connection.OpenAsync(cancellationToken); // 1. Fetch current password hash const string getSql = """ SELECT TOP 1 ContrasenaHash, EstaActivo, EstaBloqueado FROM Usuarios WHERE UsuarioId = @UsuarioId """; await using var getCommand = new SqlCommand(getSql, connection); getCommand.Parameters.Add(new SqlParameter("@UsuarioId", SqlDbType.Int) { Value = request.UsuarioId }); await using var reader = await getCommand.ExecuteReaderAsync(cancellationToken); if (!await reader.ReadAsync(cancellationToken)) { throw new InvalidOperationException("Usuario no encontrado."); } var currentHash = reader.GetString(reader.GetOrdinal("ContrasenaHash")); var activo = reader.GetBoolean(reader.GetOrdinal("EstaActivo")); var bloqueado = reader.GetBoolean(reader.GetOrdinal("EstaBloqueado")); if (!activo || bloqueado) { throw new InvalidOperationException("La cuenta no está disponible."); } // 2. Verify current password if (!VerifyPassword(request.PasswordActual, currentHash)) { throw new InvalidOperationException("La contraseña actual es incorrecta."); } await reader.CloseAsync(); // 3. Hash new password var newHash = HashPassword(request.PasswordNueva); // 4. Update database const string updateSql = """ UPDATE Usuarios SET ContrasenaHash = @ContrasenaHash, FechaActualizacion = SYSUTCDATETIME() WHERE UsuarioId = @UsuarioId """; await using var updateCommand = new SqlCommand(updateSql, connection); updateCommand.Parameters.Add(new SqlParameter("@UsuarioId", SqlDbType.Int) { Value = request.UsuarioId }); updateCommand.Parameters.Add(new SqlParameter("@ContrasenaHash", SqlDbType.NVarChar, 300) { Value = newHash }); var rows = await updateCommand.ExecuteNonQueryAsync(cancellationToken); if (rows == 0) { throw new InvalidOperationException("No se pudo actualizar la contraseña."); }}