From a7009824b28dee079c608cda355500c7b484361b Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 15:01:55 +0000 Subject: [PATCH 01/10] Add new auth option "Microsoft Entra MFA" --- Lite/Models/ServerConnection.cs | 32 ++++++++++-- Lite/Services/ServerManager.cs | 27 ++++++++-- Lite/Windows/AddServerDialog.xaml | 21 +++++++- Lite/Windows/AddServerDialog.xaml.cs | 75 +++++++++++++++++++++++++--- 4 files changed, 138 insertions(+), 17 deletions(-) diff --git a/Lite/Models/ServerConnection.cs b/Lite/Models/ServerConnection.cs index c3a8a3fa..68e6ca25 100644 --- a/Lite/Models/ServerConnection.cs +++ b/Lite/Models/ServerConnection.cs @@ -19,6 +19,12 @@ public class ServerConnection public string ServerName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; public bool UseWindowsAuth { get; set; } = true; + + /// + /// Authentication type: "Windows", "SqlServer", or "EntraMFA" + /// + public string AuthenticationType { get; set; } = "Windows"; + public string? Description { get; set; } public DateTime CreatedDate { get; set; } = DateTime.Now; public DateTime LastConnected { get; set; } = DateTime.Now; @@ -48,7 +54,12 @@ public class ServerConnection /// Display-only property for showing authentication type in UI. /// [JsonIgnore] - public string AuthenticationDisplay => UseWindowsAuth ? "Windows" : "SQL Server"; + public string AuthenticationDisplay => AuthenticationType switch + { + "EntraMFA" => "Microsoft Entra MFA", + "SqlServer" => "SQL Server", + _ => "Windows" + }; /// /// Display-only property for showing status in UI. @@ -65,7 +76,7 @@ public string GetConnectionString(CredentialService credentialService) string? username = null; string? password = null; - if (!UseWindowsAuth) + if (AuthenticationType == "SqlServer") { var cred = credentialService.GetCredential(Id); if (cred.HasValue) @@ -102,16 +113,27 @@ private string BuildConnectionString(string? username, string? password) _ => SqlConnectionEncryptOption.Optional }; - if (UseWindowsAuth) + if (AuthenticationType == "Windows") { builder.IntegratedSecurity = true; } - else + else if (AuthenticationType == "SqlServer") { builder.IntegratedSecurity = false; builder.UserID = username ?? string.Empty; builder.Password = password ?? string.Empty; } + else if (AuthenticationType == "EntraMFA") + { + // Microsoft Entra MFA (Azure AD Interactive) + builder.IntegratedSecurity = false; + builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive; + // Optionally set UserID for account hint (email/UPN) + if (!string.IsNullOrWhiteSpace(username)) + { + builder.UserID = username; + } + } return builder.ConnectionString; } @@ -121,7 +143,7 @@ private string BuildConnectionString(string? username, string? password) /// public bool HasStoredCredentials(CredentialService credentialService) { - if (UseWindowsAuth) + if (AuthenticationType == "Windows" || AuthenticationType == "EntraMFA") { return true; } diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs index d14a313a..60c46881 100644 --- a/Lite/Services/ServerManager.cs +++ b/Lite/Services/ServerManager.cs @@ -104,13 +104,23 @@ public void AddServer(ServerConnection server, string? username = null, string? SaveServers(); } - if (!server.UseWindowsAuth && !string.IsNullOrEmpty(username) && password != null) + // Save credentials based on authentication type + if (server.AuthenticationType == "SqlServer" && !string.IsNullOrEmpty(username) && password != null) { + // For SQL Server auth, save both username and password if (!_credentialService.SaveCredential(server.Id, username, password)) { throw new InvalidOperationException("Failed to save credentials to Windows Credential Manager"); } } + else if (server.AuthenticationType == "EntraMFA" && !string.IsNullOrEmpty(username)) + { + // For MFA auth, save username as account hint (password can be empty) + if (!_credentialService.SaveCredential(server.Id, username, string.Empty)) + { + throw new InvalidOperationException("Failed to save account hint to Windows Credential Manager"); + } + } // Initialize status as unknown for new server _connectionStatuses[server.Id] = new ServerConnectionStatus { ServerId = server.Id }; @@ -136,15 +146,26 @@ public void UpdateServer(ServerConnection server, string? username = null, strin SaveServers(); } - if (!server.UseWindowsAuth && !string.IsNullOrEmpty(username) && password != null) + // Update credentials based on authentication type + if (server.AuthenticationType == "SqlServer" && !string.IsNullOrEmpty(username) && password != null) { + // For SQL Server auth, update both username and password if (!_credentialService.UpdateCredential(server.Id, username, password)) { throw new InvalidOperationException("Failed to update credentials in Windows Credential Manager"); } } - else if (server.UseWindowsAuth) + else if (server.AuthenticationType == "EntraMFA" && !string.IsNullOrEmpty(username)) + { + // For MFA auth, update username as account hint (password can be empty) + if (!_credentialService.UpdateCredential(server.Id, username, string.Empty)) + { + throw new InvalidOperationException("Failed to update account hint in Windows Credential Manager"); + } + } + else if (server.AuthenticationType == "Windows") { + // For Windows auth, remove any stored credentials _credentialService.DeleteCredential(server.Id); } diff --git a/Lite/Windows/AddServerDialog.xaml b/Lite/Windows/AddServerDialog.xaml index 730a8fe3..ce927322 100644 --- a/Lite/Windows/AddServerDialog.xaml +++ b/Lite/Windows/AddServerDialog.xaml @@ -43,7 +43,10 @@ + Checked="AuthMode_Changed" Margin="0,0,0,4"/> + @@ -56,6 +59,18 @@ Padding="6,4"/> + + + + + + + @@ -86,7 +101,9 @@ + VerticalAlignment="Bottom" Margin="0,0,0,8" + TextWrapping="Wrap" MinHeight="40" + ToolTip="{Binding Text, RelativeSource={RelativeSource Self}}"/> diff --git a/Lite/Windows/AddServerDialog.xaml.cs b/Lite/Windows/AddServerDialog.xaml.cs index 861452d8..49dfe347 100644 --- a/Lite/Windows/AddServerDialog.xaml.cs +++ b/Lite/Windows/AddServerDialog.xaml.cs @@ -51,13 +51,35 @@ public AddServerDialog(ServerManager serverManager, ServerConnection existing) : DescriptionTextBox.Text = existing.Description ?? ""; DatabaseNameBox.Text = existing.DatabaseName ?? ""; - if (existing.UseWindowsAuth) + // Set authentication mode + if (existing.AuthenticationType == "EntraMFA") { - WindowsAuthRadio.IsChecked = true; + EntraMfaAuthRadio.IsChecked = true; + + // Load account hint if stored + var credentialService = new CredentialService(); + var cred = credentialService.GetCredential(existing.Id); + if (cred.HasValue) + { + EntraMfaUsernameBox.Text = cred.Value.Username; + } } - else + else if (existing.AuthenticationType == "SqlServer") { SqlAuthRadio.IsChecked = true; + + // Load credentials if stored + var credentialService = new CredentialService(); + var cred = credentialService.GetCredential(existing.Id); + if (cred.HasValue) + { + UsernameBox.Text = cred.Value.Username; + PasswordBox.Password = cred.Value.Password; + } + } + else + { + WindowsAuthRadio.IsChecked = true; } AddedServer = existing; @@ -65,11 +87,17 @@ public AddServerDialog(ServerManager serverManager, ServerConnection existing) : private void AuthMode_Changed(object sender, RoutedEventArgs e) { - if (SqlCredentialsPanel != null) + if (SqlCredentialsPanel != null && EntraMfaPanel != null) { + // Show credentials panel for SQL Server authentication SqlCredentialsPanel.Visibility = SqlAuthRadio.IsChecked == true ? Visibility.Visible : Visibility.Collapsed; + + // Show MFA panel for Microsoft Entra MFA + EntraMfaPanel.Visibility = EntraMfaAuthRadio.IsChecked == true + ? Visibility.Visible + : Visibility.Collapsed; } } @@ -122,12 +150,27 @@ private async void TestButton_Click(object sender, RoutedEventArgs e) { builder.IntegratedSecurity = true; } - else + else if (SqlAuthRadio.IsChecked == true) { builder.IntegratedSecurity = false; builder.UserID = UsernameBox.Text.Trim(); builder.Password = PasswordBox.Password; } + else if (EntraMfaAuthRadio.IsChecked == true) + { + // Microsoft Entra MFA (Azure AD Interactive) + builder.IntegratedSecurity = false; + builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive; + + // Optional: Use username as account hint if provided + var username = EntraMfaUsernameBox.Text.Trim(); + if (!string.IsNullOrEmpty(username)) + { + builder.UserID = username; + } + + StatusText.Text = "Please complete authentication in the popup window..."; + } using var connection = new SqlConnection(builder.ConnectionString); await connection.OpenAsync(); @@ -163,12 +206,28 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) displayName = serverName; } - var useWindowsAuth = WindowsAuthRadio.IsChecked == true; + // Determine authentication type + string authenticationType; + bool useWindowsAuth; string? username = null; string? password = null; - if (!useWindowsAuth) + if (WindowsAuthRadio.IsChecked == true) + { + authenticationType = "Windows"; + useWindowsAuth = true; + } + else if (EntraMfaAuthRadio.IsChecked == true) + { + authenticationType = "EntraMFA"; + useWindowsAuth = false; + // Optionally store username as account hint for MFA + username = EntraMfaUsernameBox.Text.Trim(); + } + else // SQL Server Authentication { + authenticationType = "SqlServer"; + useWindowsAuth = false; username = UsernameBox.Text.Trim(); password = PasswordBox.Password; @@ -187,6 +246,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) AddedServer.ServerName = serverName; AddedServer.DisplayName = displayName; AddedServer.UseWindowsAuth = useWindowsAuth; + AddedServer.AuthenticationType = authenticationType; AddedServer.IsEnabled = EnabledCheckBox.IsChecked == true; AddedServer.TrustServerCertificate = TrustCertCheckBox.IsChecked == true; AddedServer.EncryptMode = GetSelectedEncryptMode(); @@ -204,6 +264,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) ServerName = serverName, DisplayName = displayName, UseWindowsAuth = useWindowsAuth, + AuthenticationType = authenticationType, IsEnabled = EnabledCheckBox.IsChecked == true, TrustServerCertificate = TrustCertCheckBox.IsChecked == true, EncryptMode = GetSelectedEncryptMode(), From dc82979f9a11145bb5ec7a7095dfe39d52101d80 Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 15:08:59 +0000 Subject: [PATCH 02/10] Remove StatusText changes --- Lite/Windows/AddServerDialog.xaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lite/Windows/AddServerDialog.xaml b/Lite/Windows/AddServerDialog.xaml index ce927322..cc5c8bd3 100644 --- a/Lite/Windows/AddServerDialog.xaml +++ b/Lite/Windows/AddServerDialog.xaml @@ -101,9 +101,7 @@ + VerticalAlignment="Bottom" Margin="0,0,0,8"/> From f1d93c1dff96f9f4b437d033cda18a7d0fce8096 Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 16:38:18 +0000 Subject: [PATCH 03/10] Handle MFA cancellation an retries. Also behaviour when app startup and 16 requests happen (added semaphore) --- Lite/MainWindow.xaml.cs | 12 ++- Lite/Models/ServerConnection.cs | 2 +- Lite/Models/ServerConnectionStatus.cs | 6 ++ Lite/Services/RemoteCollectorService.cs | 104 ++++++++++++++++++++-- Lite/Services/ServerManager.cs | 112 ++++++++++++++++++++++-- Lite/Windows/AddServerDialog.xaml | 13 +-- Lite/Windows/AddServerDialog.xaml.cs | 46 +++++++++- 7 files changed, 265 insertions(+), 30 deletions(-) diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index cfae29a2..f42e70ed 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -359,13 +359,23 @@ private async void ConnectToServer(ServerConnection server) return; } + // Clear MFA cancellation flag when user explicitly connects + // This gives them a fresh attempt at authentication + var currentStatus = _serverManager.GetConnectionStatus(server.Id); + if (server.AuthenticationType == "EntraMFA" && currentStatus.UserCancelledMfa) + { + currentStatus.UserCancelledMfa = false; + StatusText.Text = "Retrying MFA authentication..."; + } + // Ensure connection status is populated with UTC offset before opening tab // This is critical for timezone-correct chart display var status = _serverManager.GetConnectionStatus(server.Id); if (!status.UtcOffsetMinutes.HasValue) { StatusText.Text = "Checking server connection..."; - status = await _serverManager.CheckConnectionAsync(server.Id); + // Allow interactive auth (MFA) when user explicitly opens a server + status = await _serverManager.CheckConnectionAsync(server.Id, allowInteractiveAuth: true); } var utcOffset = status.UtcOffsetMinutes ?? 0; diff --git a/Lite/Models/ServerConnection.cs b/Lite/Models/ServerConnection.cs index 68e6ca25..c53c3514 100644 --- a/Lite/Models/ServerConnection.cs +++ b/Lite/Models/ServerConnection.cs @@ -128,7 +128,7 @@ private string BuildConnectionString(string? username, string? password) // Microsoft Entra MFA (Azure AD Interactive) builder.IntegratedSecurity = false; builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive; - // Optionally set UserID for account hint (email/UPN) + // Optionally set UserID (email/UPN) if (!string.IsNullOrWhiteSpace(username)) { builder.UserID = username; diff --git a/Lite/Models/ServerConnectionStatus.cs b/Lite/Models/ServerConnectionStatus.cs index 6aa365c3..16977173 100644 --- a/Lite/Models/ServerConnectionStatus.cs +++ b/Lite/Models/ServerConnectionStatus.cs @@ -88,6 +88,12 @@ public class ServerConnectionStatus /// public int? UtcOffsetMinutes { get; set; } + /// + /// Indicates whether the user has cancelled MFA authentication for this server. + /// When true, MFA popups will not be shown until the user explicitly tries to connect again. + /// + public bool UserCancelledMfa { get; set; } + /// /// Gets the status display text for the UI. /// diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs index 6dfb5353..60620b87 100644 --- a/Lite/Services/RemoteCollectorService.cs +++ b/Lite/Services/RemoteCollectorService.cs @@ -64,6 +64,12 @@ public partial class RemoteCollectorService /// private static readonly SemaphoreSlim s_connectionThrottle = new(7, 7); + /// + /// Serializes MFA authentication attempts to prevent multiple popups. + /// Only one MFA authentication can happen at a time. + /// + private static readonly SemaphoreSlim s_mfaAuthLock = new(1, 1); + /// /// Command timeout for DMV queries in seconds. /// @@ -254,6 +260,16 @@ public async Task RunCollectorAsync(ServerConnection server, string collectorNam return; } + // Skip MFA servers if user has cancelled authentication + // This prevents repeated popup dialogs during background data collection + if (server.AuthenticationType == "EntraMFA" && serverStatus.UserCancelledMfa) + { + AppLogger.Info("Collector", $" [{server.DisplayName}] {collectorName} SKIPPED - MFA authentication cancelled by user"); + _logger?.LogDebug("Skipping collector '{Collector}' for server '{Server}' - user cancelled MFA", + collectorName, server.DisplayName); + return; + } + _logger?.LogDebug("Running collector '{Collector}' for server '{Server}'", collectorName, server.DisplayName); @@ -314,6 +330,13 @@ public async Task RunCollectorAsync(ServerConnection server, string collectorNam collectorName, ex.Number, server.DisplayName); } } + catch (InvalidOperationException ex) when (ex.Message.Contains("MFA authentication cancelled")) + { + // User cancelled MFA - don't log as error, this is expected + status = "SKIPPED"; + errorMessage = "MFA authentication cancelled by user"; + AppLogger.Info("Collector", $" [{server.DisplayName}] {collectorName} SKIPPED - {errorMessage}"); + } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { status = "CANCELLED"; @@ -395,13 +418,36 @@ INSERT INTO collection_log (log_id, server_id, collector_name, collection_time, /// /// Creates a SQL connection to a remote server. + /// Throws InvalidOperationException if MFA authentication was cancelled by user. /// protected async Task CreateConnectionAsync(ServerConnection server, CancellationToken cancellationToken) { - await s_connectionThrottle.WaitAsync(cancellationToken); + // For MFA servers, serialize authentication attempts to prevent multiple popups + bool isMfaServer = server.AuthenticationType == "EntraMFA"; + bool mfaLockAcquired = false; + try { - var connectionString = server.GetConnectionString(_serverManager.CredentialService); + // Acquire MFA lock first (if applicable) to serialize authentication + if (isMfaServer) + { + await s_mfaAuthLock.WaitAsync(cancellationToken); + mfaLockAcquired = true; + + // Check if user already cancelled MFA for this server + var serverStatus = _serverManager.GetConnectionStatus(server.Id); + if (serverStatus.UserCancelledMfa) + { + AppLogger.Info("Collector", $" [{server.DisplayName}] MFA authentication already cancelled - aborting"); + throw new InvalidOperationException("MFA authentication cancelled by user. Please connect to the server explicitly to retry."); + } + } + + // Now acquire connection throttle + await s_connectionThrottle.WaitAsync(cancellationToken); + try + { + var connectionString = server.GetConnectionString(_serverManager.CredentialService); var builder = new SqlConnectionStringBuilder(connectionString) { @@ -410,19 +456,61 @@ protected async Task CreateConnectionAsync(ServerConnection serve var connStr = builder.ConnectionString; - return await RetryHelper.ExecuteWithRetryAsync(async () => + return await RetryHelper.ExecuteWithRetryAsync(async () => + { + var connection = new SqlConnection(connStr); + + try + { + await connection.OpenAsync(cancellationToken); + return connection; + } + catch (Exception ex) when (isMfaServer) + { + // Detect MFA cancellation and mark immediately so other waiting connections abort + if (IsMfaCancelledException(ex)) + { + var serverStatus = _serverManager.GetConnectionStatus(server.Id); + serverStatus.UserCancelledMfa = true; + AppLogger.Info("Collector", $" [{server.DisplayName}] MFA authentication cancelled by user"); + _logger?.LogInformation("MFA authentication cancelled by user for server '{DisplayName}' - flagging to abort other pending connections", server.DisplayName); + } + throw; + } + }, _logger, $"Connect to {server.DisplayName}", cancellationToken: cancellationToken); + } + finally { - var connection = new SqlConnection(connStr); - await connection.OpenAsync(cancellationToken); - return connection; - }, _logger, $"Connect to {server.DisplayName}", cancellationToken: cancellationToken); + s_connectionThrottle.Release(); + } } finally { - s_connectionThrottle.Release(); + // Release MFA lock if we acquired it + if (mfaLockAcquired) + { + s_mfaAuthLock.Release(); + } } } + /// + /// Checks if an exception indicates that the user cancelled MFA authentication. + /// + private static bool IsMfaCancelledException(Exception ex) + { + var message = ex.Message?.ToLowerInvariant() ?? string.Empty; + + // Common patterns when user cancels Azure AD authentication + return message.Contains("user canceled") || + message.Contains("user cancelled") || + message.Contains("authentication was cancelled") || + message.Contains("authentication was canceled") || + message.Contains("user intervention is required") || + message.Contains("aadsts50058") || // Need to select account + message.Contains("aadsts50126"); // Invalid credentials or cancelled + } + /// /// Generates a unique collection ID based on timestamp. /// diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs index 60c46881..569595f2 100644 --- a/Lite/Services/ServerManager.cs +++ b/Lite/Services/ServerManager.cs @@ -115,10 +115,10 @@ public void AddServer(ServerConnection server, string? username = null, string? } else if (server.AuthenticationType == "EntraMFA" && !string.IsNullOrEmpty(username)) { - // For MFA auth, save username as account hint (password can be empty) + // For MFA auth, save username (password can be empty) if (!_credentialService.SaveCredential(server.Id, username, string.Empty)) { - throw new InvalidOperationException("Failed to save account hint to Windows Credential Manager"); + throw new InvalidOperationException("Failed to save username to Windows Credential Manager"); } } @@ -157,10 +157,10 @@ public void UpdateServer(ServerConnection server, string? username = null, strin } else if (server.AuthenticationType == "EntraMFA" && !string.IsNullOrEmpty(username)) { - // For MFA auth, update username as account hint (password can be empty) + // For MFA auth, update username (password can be empty) if (!_credentialService.UpdateCredential(server.Id, username, string.Empty)) { - throw new InvalidOperationException("Failed to update account hint in Windows Credential Manager"); + throw new InvalidOperationException("Failed to update username in Windows Credential Manager"); } } else if (server.AuthenticationType == "Windows") @@ -247,7 +247,9 @@ public ServerConnectionStatus GetConnectionStatus(string serverId) /// /// Checks the connection status of a server. /// - public async Task CheckConnectionAsync(string serverId) + /// The server ID to check. + /// Whether to allow interactive authentication (e.g., MFA). Set to false for background checks. + public async Task CheckConnectionAsync(string serverId, bool allowInteractiveAuth = false) { var server = GetServerById(serverId); if (server == null) @@ -265,6 +267,58 @@ public async Task CheckConnectionAsync(string serverId) // Get previous status to detect status changes var previousStatus = GetConnectionStatus(serverId); + // Skip interactive authentication methods during background checks + if (!allowInteractiveAuth && server.AuthenticationType == "EntraMFA") + { + return new ServerConnectionStatus + { + ServerId = serverId, + IsOnline = previousStatus.IsOnline, + LastChecked = DateTime.Now, + StatusChangedAt = previousStatus.StatusChangedAt, + ErrorMessage = "Skipped - requires interactive authentication", + PreviousIsOnline = previousStatus.IsOnline, + UserCancelledMfa = previousStatus.UserCancelledMfa + }; + } + + // Don't retry MFA if user previously cancelled, unless this is an explicit connection attempt + if (server.AuthenticationType == "EntraMFA" && previousStatus.UserCancelledMfa && !allowInteractiveAuth) + { + return new ServerConnectionStatus + { + ServerId = serverId, + IsOnline = false, + LastChecked = DateTime.Now, + StatusChangedAt = previousStatus.StatusChangedAt, + ErrorMessage = "Authentication cancelled by user", + PreviousIsOnline = previousStatus.IsOnline, + UserCancelledMfa = true + }; + } + + // Clear cancellation flag when user explicitly tries to connect (allowInteractiveAuth = true) + // This gives them a fresh attempt at authentication + if (allowInteractiveAuth && previousStatus.UserCancelledMfa) + { + _logger?.LogDebug("Clearing MFA cancellation flag for server '{DisplayName}' - user is retrying", server.DisplayName); + } + + // CRITICAL: Prevent connection checks while Add/Edit dialog is open + // This prevents MFA popups when user is just configuring the server + if (Windows.AddServerDialog.IsDialogOpen && server.AuthenticationType == "EntraMFA") + { + return new ServerConnectionStatus + { + ServerId = serverId, + IsOnline = previousStatus.IsOnline, + LastChecked = DateTime.Now, + StatusChangedAt = previousStatus.StatusChangedAt, + ErrorMessage = "Skipped - dialog open", + PreviousIsOnline = previousStatus.IsOnline + }; + } + var status = new ServerConnectionStatus { ServerId = serverId, @@ -302,6 +356,7 @@ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds { status.IsOnline = true; status.ErrorMessage = null; + status.UserCancelledMfa = false; // Clear cancellation flag on successful connection if (!reader.IsDBNull(0)) { @@ -335,13 +390,35 @@ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds { status.IsOnline = false; status.ErrorMessage = ex.Message; - _logger?.LogWarning("Connectivity check failed for server '{DisplayName}': {Message}", server.DisplayName, ex.Message); + + // Detect MFA cancellation (error code 0 with specific message patterns) + if (server.AuthenticationType == "EntraMFA" && IsMfaCancelledException(ex)) + { + status.UserCancelledMfa = true; + status.ErrorMessage = "Authentication cancelled by user"; + _logger?.LogInformation("MFA authentication cancelled by user for server '{DisplayName}'", server.DisplayName); + } + else + { + _logger?.LogWarning("Connectivity check failed for server '{DisplayName}': {Message}", server.DisplayName, ex.Message); + } } catch (Exception ex) { status.IsOnline = false; status.ErrorMessage = ex.Message; - _logger?.LogWarning(ex, "Connectivity check error for server '{DisplayName}'", server.DisplayName); + + // Detect MFA cancellation from generic exceptions + if (server.AuthenticationType == "EntraMFA" && IsMfaCancelledException(ex)) + { + status.UserCancelledMfa = true; + status.ErrorMessage = "Authentication cancelled by user"; + _logger?.LogInformation("MFA authentication cancelled by user for server '{DisplayName}'", server.DisplayName); + } + else + { + _logger?.LogWarning(ex, "Connectivity check error for server '{DisplayName}'", server.DisplayName); + } } // Track when status changed (online to offline or vice versa) @@ -364,11 +441,13 @@ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds /// /// Checks the connection status of all servers. + /// Background operation - will skip servers requiring interactive authentication (e.g., MFA). /// public async Task CheckAllConnectionsAsync() { var servers = GetAllServers(); - var tasks = servers.Select(s => CheckConnectionAsync(s.Id)); + // Explicitly pass allowInteractiveAuth: false to prevent MFA popups during background checks + var tasks = servers.Select(s => CheckConnectionAsync(s.Id, allowInteractiveAuth: false)); await Task.WhenAll(tasks); } @@ -451,6 +530,23 @@ private void SaveServers() } } + /// + /// Checks if an exception indicates that the user cancelled MFA authentication. + /// + private static bool IsMfaCancelledException(Exception ex) + { + var message = ex.Message?.ToLowerInvariant() ?? string.Empty; + + // Common patterns when user cancels Azure AD authentication + return message.Contains("user canceled") || + message.Contains("user cancelled") || + message.Contains("authentication was cancelled") || + message.Contains("authentication was canceled") || + message.Contains("user intervention is required") || + message.Contains("aadsts50058") || // Need to select account + message.Contains("aadsts50126"); // Invalid credentials or cancelled + } + /// /// JSON wrapper for servers list. /// diff --git a/Lite/Windows/AddServerDialog.xaml b/Lite/Windows/AddServerDialog.xaml index cc5c8bd3..376d6f4a 100644 --- a/Lite/Windows/AddServerDialog.xaml +++ b/Lite/Windows/AddServerDialog.xaml @@ -46,7 +46,7 @@ Checked="AuthMode_Changed" Margin="0,0,0,4"/> + ToolTip="Interactive authentication with MFA for Azure SQL Database."/> @@ -59,16 +59,11 @@ Padding="6,4"/> - + - + - + ToolTip="Optional: Pre-populate the authentication dialog with this email"/> diff --git a/Lite/Windows/AddServerDialog.xaml.cs b/Lite/Windows/AddServerDialog.xaml.cs index 49dfe347..96485890 100644 --- a/Lite/Windows/AddServerDialog.xaml.cs +++ b/Lite/Windows/AddServerDialog.xaml.cs @@ -17,6 +17,12 @@ namespace PerformanceMonitorLite.Windows; public partial class AddServerDialog : Window { private readonly ServerManager _serverManager; + private static bool _isDialogOpen = false; + + /// + /// Indicates if any AddServerDialog is currently open. Used to prevent background connection checks. + /// + public static bool IsDialogOpen => _isDialogOpen; /// /// The server that was added, or null if the dialog was cancelled. @@ -27,6 +33,8 @@ public AddServerDialog(ServerManager serverManager) { InitializeComponent(); _serverManager = serverManager; + _isDialogOpen = true; + Closing += (s, e) => _isDialogOpen = false; } /// @@ -56,7 +64,7 @@ public AddServerDialog(ServerManager serverManager, ServerConnection existing) : { EntraMfaAuthRadio.IsChecked = true; - // Load account hint if stored + // Load username if stored var credentialService = new CredentialService(); var cred = credentialService.GetCredential(existing.Id); if (cred.HasValue) @@ -162,7 +170,7 @@ private async void TestButton_Click(object sender, RoutedEventArgs e) builder.IntegratedSecurity = false; builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive; - // Optional: Use username as account hint if provided + // Optional: Use username if provided var username = EntraMfaUsernameBox.Text.Trim(); if (!string.IsNullOrEmpty(username)) { @@ -180,10 +188,25 @@ private async void TestButton_Click(object sender, RoutedEventArgs e) var shortVersion = version?.Split('\n')[0] ?? "Connected"; StatusText.Text = $"Success: {shortVersion}"; + + // Clear any previous MFA cancellation flag on successful connection + if (AddedServer != null && EntraMfaAuthRadio.IsChecked == true) + { + var status = _serverManager.GetConnectionStatus(AddedServer.Id); + status.UserCancelledMfa = false; + } } catch (Exception ex) { StatusText.Text = $"Failed: {ex.Message}"; + + // Mark MFA as cancelled if user cancelled the authentication popup + if (AddedServer != null && EntraMfaAuthRadio.IsChecked == true && IsMfaCancelledException(ex)) + { + var status = _serverManager.GetConnectionStatus(AddedServer.Id); + status.UserCancelledMfa = true; + StatusText.Text = "Authentication cancelled by user. Click Test to try again."; + } } finally { @@ -221,7 +244,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) { authenticationType = "EntraMFA"; useWindowsAuth = false; - // Optionally store username as account hint for MFA + // Optionally store username for MFA username = EntraMfaUsernameBox.Text.Trim(); } else // SQL Server Authentication @@ -290,4 +313,21 @@ private void CancelButton_Click(object sender, RoutedEventArgs e) DialogResult = false; Close(); } + + /// + /// Checks if an exception indicates that the user cancelled MFA authentication. + /// + private static bool IsMfaCancelledException(Exception ex) + { + var message = ex.Message?.ToLowerInvariant() ?? string.Empty; + + // Common patterns when user cancels Azure AD authentication + return message.Contains("user canceled") || + message.Contains("user cancelled") || + message.Contains("authentication was cancelled") || + message.Contains("authentication was canceled") || + message.Contains("user intervention is required") || + message.Contains("aadsts50058") || // Need to select account + message.Contains("aadsts50126"); // Invalid credentials or cancelled + } } From 2a5d7967514aa5b5de0a3d9f4ca5be439d355a23 Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 17:49:51 +0000 Subject: [PATCH 04/10] Fix Critical: Backward compatibility break related to "AuthenticationType" --- Lite/Services/ServerManager.cs | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs index 569595f2..e66bb80d 100644 --- a/Lite/Services/ServerManager.cs +++ b/Lite/Services/ServerManager.cs @@ -473,6 +473,10 @@ private void LoadServers() try { File.Copy(_configFilePath, _configFilePath + ".bak", overwrite: true); } catch { /* best effort */ } + // MIGRATION: Backward compatibility for existing servers.json files + // Old configs only had UseWindowsAuth, new code uses AuthenticationType + MigrateServerAuthentication(_servers); + // Initialize status tracking for all loaded servers foreach (var server in _servers) { @@ -494,6 +498,10 @@ private void LoadServers() string bakJson = File.ReadAllText(bakPath); var bakConfig = JsonSerializer.Deserialize(bakJson); _servers = bakConfig?.Servers ?? new List(); + + // MIGRATION: Backward compatibility + MigrateServerAuthentication(_servers); + foreach (var server in _servers) { _connectionStatuses[server.Id] = new ServerConnectionStatus { ServerId = server.Id }; @@ -530,6 +538,35 @@ private void SaveServers() } } + /// + /// Migrates server authentication configuration for backward compatibility. + /// Old configs only had UseWindowsAuth, new code uses AuthenticationType. + /// If AuthenticationType is default "Windows" but UseWindowsAuth is false, set to "SqlServer". + /// + private void MigrateServerAuthentication(List servers) + { + foreach (var server in servers) + { + // If AuthenticationType is "Windows" (default) but UseWindowsAuth is false, + // this is a SQL Server auth server from an old config + if (server.AuthenticationType == "Windows" && !server.UseWindowsAuth) + { + server.AuthenticationType = "SqlServer"; + _logger?.LogInformation("Migrated server '{DisplayName}' authentication type from legacy UseWindowsAuth=false to AuthenticationType=SqlServer", + server.DisplayName); + } + // Ensure UseWindowsAuth stays in sync with AuthenticationType for consistency + else if (server.AuthenticationType == "SqlServer" || server.AuthenticationType == "EntraMFA") + { + server.UseWindowsAuth = false; + } + else if (server.AuthenticationType == "Windows") + { + server.UseWindowsAuth = true; + } + } + } + /// /// Checks if an exception indicates that the user cancelled MFA authentication. /// From 2d469d150bf9c2e92a4fb35af23d0727321e565a Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 17:51:41 +0000 Subject: [PATCH 05/10] Fix the "Bug: AADSTS50126 is not cancellation" --- Lite/Services/RemoteCollectorService.cs | 8 +++----- Lite/Services/ServerManager.cs | 8 +++----- Lite/Windows/AddServerDialog.xaml.cs | 8 +++----- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs index 60620b87..eb968cc6 100644 --- a/Lite/Services/RemoteCollectorService.cs +++ b/Lite/Services/RemoteCollectorService.cs @@ -501,14 +501,12 @@ private static bool IsMfaCancelledException(Exception ex) { var message = ex.Message?.ToLowerInvariant() ?? string.Empty; - // Common patterns when user cancels Azure AD authentication + // Only treat explicit user cancellation messages as cancellation + // Do NOT treat authentication errors (wrong password, account selection, etc.) as cancellation return message.Contains("user canceled") || message.Contains("user cancelled") || message.Contains("authentication was cancelled") || - message.Contains("authentication was canceled") || - message.Contains("user intervention is required") || - message.Contains("aadsts50058") || // Need to select account - message.Contains("aadsts50126"); // Invalid credentials or cancelled + message.Contains("authentication was canceled"); } /// diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs index e66bb80d..21310b89 100644 --- a/Lite/Services/ServerManager.cs +++ b/Lite/Services/ServerManager.cs @@ -574,14 +574,12 @@ private static bool IsMfaCancelledException(Exception ex) { var message = ex.Message?.ToLowerInvariant() ?? string.Empty; - // Common patterns when user cancels Azure AD authentication + // Only treat explicit user cancellation messages as cancellation + // Do NOT treat authentication errors (wrong password, account selection, etc.) as cancellation return message.Contains("user canceled") || message.Contains("user cancelled") || message.Contains("authentication was cancelled") || - message.Contains("authentication was canceled") || - message.Contains("user intervention is required") || - message.Contains("aadsts50058") || // Need to select account - message.Contains("aadsts50126"); // Invalid credentials or cancelled + message.Contains("authentication was canceled"); } /// diff --git a/Lite/Windows/AddServerDialog.xaml.cs b/Lite/Windows/AddServerDialog.xaml.cs index 96485890..3e16a996 100644 --- a/Lite/Windows/AddServerDialog.xaml.cs +++ b/Lite/Windows/AddServerDialog.xaml.cs @@ -321,13 +321,11 @@ private static bool IsMfaCancelledException(Exception ex) { var message = ex.Message?.ToLowerInvariant() ?? string.Empty; - // Common patterns when user cancels Azure AD authentication + // Only treat explicit user cancellation messages as cancellation + // Do NOT treat authentication errors (wrong password, account selection, etc.) as cancellation return message.Contains("user canceled") || message.Contains("user cancelled") || message.Contains("authentication was cancelled") || - message.Contains("authentication was canceled") || - message.Contains("user intervention is required") || - message.Contains("aadsts50058") || // Need to select account - message.Contains("aadsts50126"); // Invalid credentials or cancelled + message.Contains("authentication was canceled"); } } From ce72f13497213bf0a24b8db659ba2f51842a1b21 Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 17:54:04 +0000 Subject: [PATCH 06/10] Fix "Code duplication: IsMfaCancelledException appears 3 times" --- Lite/Helpers/MfaAuthenticationHelper.cs | 34 +++++++++++++++++++++++++ Lite/Services/RemoteCollectorService.cs | 18 ++----------- Lite/Services/ServerManager.cs | 20 +++------------ Lite/Windows/AddServerDialog.xaml.cs | 18 ++----------- 4 files changed, 41 insertions(+), 49 deletions(-) create mode 100644 Lite/Helpers/MfaAuthenticationHelper.cs diff --git a/Lite/Helpers/MfaAuthenticationHelper.cs b/Lite/Helpers/MfaAuthenticationHelper.cs new file mode 100644 index 00000000..dfedf948 --- /dev/null +++ b/Lite/Helpers/MfaAuthenticationHelper.cs @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor Lite. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +using System; + +namespace PerformanceMonitorLite.Helpers; + +/// +/// Helper utilities for Microsoft Entra MFA authentication. +/// +public static class MfaAuthenticationHelper +{ + /// + /// Checks if an exception indicates that the user cancelled MFA authentication. + /// + /// The exception to check. + /// True if the exception represents user cancellation, false otherwise. + public static bool IsMfaCancelledException(Exception ex) + { + var message = ex.Message?.ToLowerInvariant() ?? string.Empty; + + // Only treat explicit user cancellation messages as cancellation + // Do NOT treat authentication errors (wrong password, account selection, etc.) as cancellation + return message.Contains("user canceled") || + message.Contains("user cancelled") || + message.Contains("authentication was cancelled") || + message.Contains("authentication was canceled"); + } +} diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs index eb968cc6..47bc73ad 100644 --- a/Lite/Services/RemoteCollectorService.cs +++ b/Lite/Services/RemoteCollectorService.cs @@ -16,6 +16,7 @@ using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using PerformanceMonitorLite.Database; +using PerformanceMonitorLite.Helpers; using PerformanceMonitorLite.Models; @@ -468,7 +469,7 @@ protected async Task CreateConnectionAsync(ServerConnection serve catch (Exception ex) when (isMfaServer) { // Detect MFA cancellation and mark immediately so other waiting connections abort - if (IsMfaCancelledException(ex)) + if (MfaAuthenticationHelper.IsMfaCancelledException(ex)) { var serverStatus = _serverManager.GetConnectionStatus(server.Id); serverStatus.UserCancelledMfa = true; @@ -494,21 +495,6 @@ protected async Task CreateConnectionAsync(ServerConnection serve } } - /// - /// Checks if an exception indicates that the user cancelled MFA authentication. - /// - private static bool IsMfaCancelledException(Exception ex) - { - var message = ex.Message?.ToLowerInvariant() ?? string.Empty; - - // Only treat explicit user cancellation messages as cancellation - // Do NOT treat authentication errors (wrong password, account selection, etc.) as cancellation - return message.Contains("user canceled") || - message.Contains("user cancelled") || - message.Contains("authentication was cancelled") || - message.Contains("authentication was canceled"); - } - /// /// Generates a unique collection ID based on timestamp. /// diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs index 21310b89..afe7233e 100644 --- a/Lite/Services/ServerManager.cs +++ b/Lite/Services/ServerManager.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; +using PerformanceMonitorLite.Helpers; using PerformanceMonitorLite.Models; namespace PerformanceMonitorLite.Services; @@ -392,7 +393,7 @@ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds status.ErrorMessage = ex.Message; // Detect MFA cancellation (error code 0 with specific message patterns) - if (server.AuthenticationType == "EntraMFA" && IsMfaCancelledException(ex)) + if (server.AuthenticationType == "EntraMFA" && MfaAuthenticationHelper.IsMfaCancelledException(ex)) { status.UserCancelledMfa = true; status.ErrorMessage = "Authentication cancelled by user"; @@ -409,7 +410,7 @@ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds status.ErrorMessage = ex.Message; // Detect MFA cancellation from generic exceptions - if (server.AuthenticationType == "EntraMFA" && IsMfaCancelledException(ex)) + if (server.AuthenticationType == "EntraMFA" && MfaAuthenticationHelper.IsMfaCancelledException(ex)) { status.UserCancelledMfa = true; status.ErrorMessage = "Authentication cancelled by user"; @@ -567,21 +568,6 @@ private void MigrateServerAuthentication(List servers) } } - /// - /// Checks if an exception indicates that the user cancelled MFA authentication. - /// - private static bool IsMfaCancelledException(Exception ex) - { - var message = ex.Message?.ToLowerInvariant() ?? string.Empty; - - // Only treat explicit user cancellation messages as cancellation - // Do NOT treat authentication errors (wrong password, account selection, etc.) as cancellation - return message.Contains("user canceled") || - message.Contains("user cancelled") || - message.Contains("authentication was cancelled") || - message.Contains("authentication was canceled"); - } - /// /// JSON wrapper for servers list. /// diff --git a/Lite/Windows/AddServerDialog.xaml.cs b/Lite/Windows/AddServerDialog.xaml.cs index 3e16a996..d3800dd9 100644 --- a/Lite/Windows/AddServerDialog.xaml.cs +++ b/Lite/Windows/AddServerDialog.xaml.cs @@ -9,6 +9,7 @@ using System; using System.Windows; using Microsoft.Data.SqlClient; +using PerformanceMonitorLite.Helpers; using PerformanceMonitorLite.Models; using PerformanceMonitorLite.Services; @@ -201,7 +202,7 @@ private async void TestButton_Click(object sender, RoutedEventArgs e) StatusText.Text = $"Failed: {ex.Message}"; // Mark MFA as cancelled if user cancelled the authentication popup - if (AddedServer != null && EntraMfaAuthRadio.IsChecked == true && IsMfaCancelledException(ex)) + if (AddedServer != null && EntraMfaAuthRadio.IsChecked == true && MfaAuthenticationHelper.IsMfaCancelledException(ex)) { var status = _serverManager.GetConnectionStatus(AddedServer.Id); status.UserCancelledMfa = true; @@ -313,19 +314,4 @@ private void CancelButton_Click(object sender, RoutedEventArgs e) DialogResult = false; Close(); } - - /// - /// Checks if an exception indicates that the user cancelled MFA authentication. - /// - private static bool IsMfaCancelledException(Exception ex) - { - var message = ex.Message?.ToLowerInvariant() ?? string.Empty; - - // Only treat explicit user cancellation messages as cancellation - // Do NOT treat authentication errors (wrong password, account selection, etc.) as cancellation - return message.Contains("user canceled") || - message.Contains("user cancelled") || - message.Contains("authentication was cancelled") || - message.Contains("authentication was canceled"); - } } From 55e46f78dfed9a60d19f328509d1f8fb048fcd26 Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 17:57:14 +0000 Subject: [PATCH 07/10] Fix "Magic strings" --- Lite/MainWindow.xaml.cs | 2 +- Lite/Models/AuthenticationTypes.cs | 30 +++++++++++++++++++++++ Lite/Models/ServerConnection.cs | 18 +++++++------- Lite/Services/RemoteCollectorService.cs | 4 ++-- Lite/Services/ServerManager.cs | 32 ++++++++++++------------- Lite/Windows/AddServerDialog.xaml.cs | 10 ++++---- 6 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 Lite/Models/AuthenticationTypes.cs diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index f42e70ed..03aa2b23 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -362,7 +362,7 @@ private async void ConnectToServer(ServerConnection server) // Clear MFA cancellation flag when user explicitly connects // This gives them a fresh attempt at authentication var currentStatus = _serverManager.GetConnectionStatus(server.Id); - if (server.AuthenticationType == "EntraMFA" && currentStatus.UserCancelledMfa) + if (server.AuthenticationType == AuthenticationTypes.EntraMFA && currentStatus.UserCancelledMfa) { currentStatus.UserCancelledMfa = false; StatusText.Text = "Retrying MFA authentication..."; diff --git a/Lite/Models/AuthenticationTypes.cs b/Lite/Models/AuthenticationTypes.cs new file mode 100644 index 00000000..f2842e1a --- /dev/null +++ b/Lite/Models/AuthenticationTypes.cs @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Erik Darling, Darling Data LLC + * + * This file is part of the SQL Server Performance Monitor Lite. + * + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +namespace PerformanceMonitorLite.Models; + +/// +/// Constants for server authentication types. +/// +public static class AuthenticationTypes +{ + /// + /// Windows integrated authentication. + /// + public const string Windows = "Windows"; + + /// + /// SQL Server username/password authentication. + /// + public const string SqlServer = "SqlServer"; + + /// + /// Microsoft Entra MFA (Azure AD) interactive authentication. + /// + public const string EntraMFA = "EntraMFA"; +} diff --git a/Lite/Models/ServerConnection.cs b/Lite/Models/ServerConnection.cs index c53c3514..2d004bbc 100644 --- a/Lite/Models/ServerConnection.cs +++ b/Lite/Models/ServerConnection.cs @@ -21,9 +21,9 @@ public class ServerConnection public bool UseWindowsAuth { get; set; } = true; /// - /// Authentication type: "Windows", "SqlServer", or "EntraMFA" + /// Authentication type: Windows, SqlServer, or EntraMFA /// - public string AuthenticationType { get; set; } = "Windows"; + public string AuthenticationType { get; set; } = AuthenticationTypes.Windows; public string? Description { get; set; } public DateTime CreatedDate { get; set; } = DateTime.Now; @@ -56,8 +56,8 @@ public class ServerConnection [JsonIgnore] public string AuthenticationDisplay => AuthenticationType switch { - "EntraMFA" => "Microsoft Entra MFA", - "SqlServer" => "SQL Server", + AuthenticationTypes.EntraMFA => "Microsoft Entra MFA", + AuthenticationTypes.SqlServer => "SQL Server", _ => "Windows" }; @@ -76,7 +76,7 @@ public string GetConnectionString(CredentialService credentialService) string? username = null; string? password = null; - if (AuthenticationType == "SqlServer") + if (AuthenticationType == AuthenticationTypes.SqlServer) { var cred = credentialService.GetCredential(Id); if (cred.HasValue) @@ -113,17 +113,17 @@ private string BuildConnectionString(string? username, string? password) _ => SqlConnectionEncryptOption.Optional }; - if (AuthenticationType == "Windows") + if (AuthenticationType == AuthenticationTypes.Windows) { builder.IntegratedSecurity = true; } - else if (AuthenticationType == "SqlServer") + else if (AuthenticationType == AuthenticationTypes.SqlServer) { builder.IntegratedSecurity = false; builder.UserID = username ?? string.Empty; builder.Password = password ?? string.Empty; } - else if (AuthenticationType == "EntraMFA") + else if (AuthenticationType == AuthenticationTypes.EntraMFA) { // Microsoft Entra MFA (Azure AD Interactive) builder.IntegratedSecurity = false; @@ -143,7 +143,7 @@ private string BuildConnectionString(string? username, string? password) /// public bool HasStoredCredentials(CredentialService credentialService) { - if (AuthenticationType == "Windows" || AuthenticationType == "EntraMFA") + if (AuthenticationType == AuthenticationTypes.Windows || AuthenticationType == AuthenticationTypes.EntraMFA) { return true; } diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs index 47bc73ad..c9f5edd3 100644 --- a/Lite/Services/RemoteCollectorService.cs +++ b/Lite/Services/RemoteCollectorService.cs @@ -263,7 +263,7 @@ public async Task RunCollectorAsync(ServerConnection server, string collectorNam // Skip MFA servers if user has cancelled authentication // This prevents repeated popup dialogs during background data collection - if (server.AuthenticationType == "EntraMFA" && serverStatus.UserCancelledMfa) + if (server.AuthenticationType == AuthenticationTypes.EntraMFA && serverStatus.UserCancelledMfa) { AppLogger.Info("Collector", $" [{server.DisplayName}] {collectorName} SKIPPED - MFA authentication cancelled by user"); _logger?.LogDebug("Skipping collector '{Collector}' for server '{Server}' - user cancelled MFA", @@ -424,7 +424,7 @@ INSERT INTO collection_log (log_id, server_id, collector_name, collection_time, protected async Task CreateConnectionAsync(ServerConnection server, CancellationToken cancellationToken) { // For MFA servers, serialize authentication attempts to prevent multiple popups - bool isMfaServer = server.AuthenticationType == "EntraMFA"; + bool isMfaServer = server.AuthenticationType == AuthenticationTypes.EntraMFA; bool mfaLockAcquired = false; try diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs index afe7233e..26384d80 100644 --- a/Lite/Services/ServerManager.cs +++ b/Lite/Services/ServerManager.cs @@ -106,7 +106,7 @@ public void AddServer(ServerConnection server, string? username = null, string? } // Save credentials based on authentication type - if (server.AuthenticationType == "SqlServer" && !string.IsNullOrEmpty(username) && password != null) + if (server.AuthenticationType == AuthenticationTypes.SqlServer && !string.IsNullOrEmpty(username) && password != null) { // For SQL Server auth, save both username and password if (!_credentialService.SaveCredential(server.Id, username, password)) @@ -114,7 +114,7 @@ public void AddServer(ServerConnection server, string? username = null, string? throw new InvalidOperationException("Failed to save credentials to Windows Credential Manager"); } } - else if (server.AuthenticationType == "EntraMFA" && !string.IsNullOrEmpty(username)) + else if (server.AuthenticationType == AuthenticationTypes.EntraMFA && !string.IsNullOrEmpty(username)) { // For MFA auth, save username (password can be empty) if (!_credentialService.SaveCredential(server.Id, username, string.Empty)) @@ -148,7 +148,7 @@ public void UpdateServer(ServerConnection server, string? username = null, strin } // Update credentials based on authentication type - if (server.AuthenticationType == "SqlServer" && !string.IsNullOrEmpty(username) && password != null) + if (server.AuthenticationType == AuthenticationTypes.SqlServer && !string.IsNullOrEmpty(username) && password != null) { // For SQL Server auth, update both username and password if (!_credentialService.UpdateCredential(server.Id, username, password)) @@ -156,7 +156,7 @@ public void UpdateServer(ServerConnection server, string? username = null, strin throw new InvalidOperationException("Failed to update credentials in Windows Credential Manager"); } } - else if (server.AuthenticationType == "EntraMFA" && !string.IsNullOrEmpty(username)) + else if (server.AuthenticationType == AuthenticationTypes.EntraMFA && !string.IsNullOrEmpty(username)) { // For MFA auth, update username (password can be empty) if (!_credentialService.UpdateCredential(server.Id, username, string.Empty)) @@ -164,7 +164,7 @@ public void UpdateServer(ServerConnection server, string? username = null, strin throw new InvalidOperationException("Failed to update username in Windows Credential Manager"); } } - else if (server.AuthenticationType == "Windows") + else if (server.AuthenticationType == AuthenticationTypes.Windows) { // For Windows auth, remove any stored credentials _credentialService.DeleteCredential(server.Id); @@ -269,7 +269,7 @@ public async Task CheckConnectionAsync(string serverId, var previousStatus = GetConnectionStatus(serverId); // Skip interactive authentication methods during background checks - if (!allowInteractiveAuth && server.AuthenticationType == "EntraMFA") + if (!allowInteractiveAuth && server.AuthenticationType == AuthenticationTypes.EntraMFA) { return new ServerConnectionStatus { @@ -284,7 +284,7 @@ public async Task CheckConnectionAsync(string serverId, } // Don't retry MFA if user previously cancelled, unless this is an explicit connection attempt - if (server.AuthenticationType == "EntraMFA" && previousStatus.UserCancelledMfa && !allowInteractiveAuth) + if (server.AuthenticationType == AuthenticationTypes.EntraMFA && previousStatus.UserCancelledMfa && !allowInteractiveAuth) { return new ServerConnectionStatus { @@ -307,7 +307,7 @@ public async Task CheckConnectionAsync(string serverId, // CRITICAL: Prevent connection checks while Add/Edit dialog is open // This prevents MFA popups when user is just configuring the server - if (Windows.AddServerDialog.IsDialogOpen && server.AuthenticationType == "EntraMFA") + if (Windows.AddServerDialog.IsDialogOpen && server.AuthenticationType == AuthenticationTypes.EntraMFA) { return new ServerConnectionStatus { @@ -393,7 +393,7 @@ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds status.ErrorMessage = ex.Message; // Detect MFA cancellation (error code 0 with specific message patterns) - if (server.AuthenticationType == "EntraMFA" && MfaAuthenticationHelper.IsMfaCancelledException(ex)) + if (server.AuthenticationType == AuthenticationTypes.EntraMFA && MfaAuthenticationHelper.IsMfaCancelledException(ex)) { status.UserCancelledMfa = true; status.ErrorMessage = "Authentication cancelled by user"; @@ -410,7 +410,7 @@ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds status.ErrorMessage = ex.Message; // Detect MFA cancellation from generic exceptions - if (server.AuthenticationType == "EntraMFA" && MfaAuthenticationHelper.IsMfaCancelledException(ex)) + if (server.AuthenticationType == AuthenticationTypes.EntraMFA && MfaAuthenticationHelper.IsMfaCancelledException(ex)) { status.UserCancelledMfa = true; status.ErrorMessage = "Authentication cancelled by user"; @@ -542,26 +542,26 @@ private void SaveServers() /// /// Migrates server authentication configuration for backward compatibility. /// Old configs only had UseWindowsAuth, new code uses AuthenticationType. - /// If AuthenticationType is default "Windows" but UseWindowsAuth is false, set to "SqlServer". + /// If AuthenticationType is default Windows but UseWindowsAuth is false, set to SqlServer. /// private void MigrateServerAuthentication(List servers) { foreach (var server in servers) { - // If AuthenticationType is "Windows" (default) but UseWindowsAuth is false, + // If AuthenticationType is Windows (default) but UseWindowsAuth is false, // this is a SQL Server auth server from an old config - if (server.AuthenticationType == "Windows" && !server.UseWindowsAuth) + if (server.AuthenticationType == AuthenticationTypes.Windows && !server.UseWindowsAuth) { - server.AuthenticationType = "SqlServer"; + server.AuthenticationType = AuthenticationTypes.SqlServer; _logger?.LogInformation("Migrated server '{DisplayName}' authentication type from legacy UseWindowsAuth=false to AuthenticationType=SqlServer", server.DisplayName); } // Ensure UseWindowsAuth stays in sync with AuthenticationType for consistency - else if (server.AuthenticationType == "SqlServer" || server.AuthenticationType == "EntraMFA") + else if (server.AuthenticationType == AuthenticationTypes.SqlServer || server.AuthenticationType == AuthenticationTypes.EntraMFA) { server.UseWindowsAuth = false; } - else if (server.AuthenticationType == "Windows") + else if (server.AuthenticationType == AuthenticationTypes.Windows) { server.UseWindowsAuth = true; } diff --git a/Lite/Windows/AddServerDialog.xaml.cs b/Lite/Windows/AddServerDialog.xaml.cs index d3800dd9..05d11ddf 100644 --- a/Lite/Windows/AddServerDialog.xaml.cs +++ b/Lite/Windows/AddServerDialog.xaml.cs @@ -61,7 +61,7 @@ public AddServerDialog(ServerManager serverManager, ServerConnection existing) : DatabaseNameBox.Text = existing.DatabaseName ?? ""; // Set authentication mode - if (existing.AuthenticationType == "EntraMFA") + if (existing.AuthenticationType == AuthenticationTypes.EntraMFA) { EntraMfaAuthRadio.IsChecked = true; @@ -73,7 +73,7 @@ public AddServerDialog(ServerManager serverManager, ServerConnection existing) : EntraMfaUsernameBox.Text = cred.Value.Username; } } - else if (existing.AuthenticationType == "SqlServer") + else if (existing.AuthenticationType == AuthenticationTypes.SqlServer) { SqlAuthRadio.IsChecked = true; @@ -238,19 +238,19 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) if (WindowsAuthRadio.IsChecked == true) { - authenticationType = "Windows"; + authenticationType = AuthenticationTypes.Windows; useWindowsAuth = true; } else if (EntraMfaAuthRadio.IsChecked == true) { - authenticationType = "EntraMFA"; + authenticationType = AuthenticationTypes.EntraMFA; useWindowsAuth = false; // Optionally store username for MFA username = EntraMfaUsernameBox.Text.Trim(); } else // SQL Server Authentication { - authenticationType = "SqlServer"; + authenticationType = AuthenticationTypes.SqlServer; useWindowsAuth = false; username = UsernameBox.Text.Trim(); password = PasswordBox.Password; From beaca1a455a9ed711220b229c671b7142dc470c3 Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 17:58:35 +0000 Subject: [PATCH 08/10] fix "Dead code in CheckConnectionAsync" --- Lite/Services/ServerManager.cs | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs index 26384d80..dff17622 100644 --- a/Lite/Services/ServerManager.cs +++ b/Lite/Services/ServerManager.cs @@ -271,33 +271,23 @@ public async Task CheckConnectionAsync(string serverId, // Skip interactive authentication methods during background checks if (!allowInteractiveAuth && server.AuthenticationType == AuthenticationTypes.EntraMFA) { + // Determine appropriate message based on whether user cancelled + var errorMsg = previousStatus.UserCancelledMfa + ? "Authentication cancelled by user" + : "Skipped - requires interactive authentication"; + return new ServerConnectionStatus { ServerId = serverId, - IsOnline = previousStatus.IsOnline, + IsOnline = previousStatus.UserCancelledMfa ? false : previousStatus.IsOnline, LastChecked = DateTime.Now, StatusChangedAt = previousStatus.StatusChangedAt, - ErrorMessage = "Skipped - requires interactive authentication", + ErrorMessage = errorMsg, PreviousIsOnline = previousStatus.IsOnline, UserCancelledMfa = previousStatus.UserCancelledMfa }; } - // Don't retry MFA if user previously cancelled, unless this is an explicit connection attempt - if (server.AuthenticationType == AuthenticationTypes.EntraMFA && previousStatus.UserCancelledMfa && !allowInteractiveAuth) - { - return new ServerConnectionStatus - { - ServerId = serverId, - IsOnline = false, - LastChecked = DateTime.Now, - StatusChangedAt = previousStatus.StatusChangedAt, - ErrorMessage = "Authentication cancelled by user", - PreviousIsOnline = previousStatus.IsOnline, - UserCancelledMfa = true - }; - } - // Clear cancellation flag when user explicitly tries to connect (allowInteractiveAuth = true) // This gives them a fresh attempt at authentication if (allowInteractiveAuth && previousStatus.UserCancelledMfa) From 1ee09c4001cd21faed620aa6e2955859b5520fbf Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 18:02:14 +0000 Subject: [PATCH 09/10] Fix "UseWindowsAuth is now redundant" --- Lite/Models/ServerConnection.cs | 22 +++++++++++++++++++++- Lite/Services/ServerManager.cs | 27 +++++++-------------------- Lite/Windows/AddServerDialog.xaml.cs | 6 ------ 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/Lite/Models/ServerConnection.cs b/Lite/Models/ServerConnection.cs index 2d004bbc..34bd442e 100644 --- a/Lite/Models/ServerConnection.cs +++ b/Lite/Models/ServerConnection.cs @@ -18,7 +18,27 @@ public class ServerConnection public string Id { get; set; } = Guid.NewGuid().ToString(); public string ServerName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; - public bool UseWindowsAuth { get; set; } = true; + + /// + /// Backward compatibility property for old servers.json files. + /// Returns true if authentication type is Windows. + /// Setter updates AuthenticationType for migration from old configs. + /// + public bool UseWindowsAuth + { + get => AuthenticationType == AuthenticationTypes.Windows; + set + { + // During JSON deserialization of old configs, update AuthenticationType based on UseWindowsAuth + // Only apply this if AuthenticationType is still at default (indicating old JSON without that field) + if (AuthenticationType == AuthenticationTypes.Windows && !value) + { + // Old config with UseWindowsAuth=false -> SQL Server auth + AuthenticationType = AuthenticationTypes.SqlServer; + } + // If value is true, keep Windows (already the default) + } + } /// /// Authentication type: Windows, SqlServer, or EntraMFA diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs index dff17622..2363ae62 100644 --- a/Lite/Services/ServerManager.cs +++ b/Lite/Services/ServerManager.cs @@ -465,7 +465,7 @@ private void LoadServers() catch { /* best effort */ } // MIGRATION: Backward compatibility for existing servers.json files - // Old configs only had UseWindowsAuth, new code uses AuthenticationType + // Migration from old UseWindowsAuth property happens automatically during deserialization MigrateServerAuthentication(_servers); // Initialize status tracking for all loaded servers @@ -531,30 +531,17 @@ private void SaveServers() /// /// Migrates server authentication configuration for backward compatibility. - /// Old configs only had UseWindowsAuth, new code uses AuthenticationType. - /// If AuthenticationType is default Windows but UseWindowsAuth is false, set to SqlServer. + /// Note: Migration from old UseWindowsAuth property happens automatically via + /// the UseWindowsAuth setter during JSON deserialization. /// private void MigrateServerAuthentication(List servers) { + // Migration is now automatic via UseWindowsAuth property setter + // Just log the loaded servers for debugging foreach (var server in servers) { - // If AuthenticationType is Windows (default) but UseWindowsAuth is false, - // this is a SQL Server auth server from an old config - if (server.AuthenticationType == AuthenticationTypes.Windows && !server.UseWindowsAuth) - { - server.AuthenticationType = AuthenticationTypes.SqlServer; - _logger?.LogInformation("Migrated server '{DisplayName}' authentication type from legacy UseWindowsAuth=false to AuthenticationType=SqlServer", - server.DisplayName); - } - // Ensure UseWindowsAuth stays in sync with AuthenticationType for consistency - else if (server.AuthenticationType == AuthenticationTypes.SqlServer || server.AuthenticationType == AuthenticationTypes.EntraMFA) - { - server.UseWindowsAuth = false; - } - else if (server.AuthenticationType == AuthenticationTypes.Windows) - { - server.UseWindowsAuth = true; - } + _logger?.LogDebug("Server '{DisplayName}' loaded with AuthenticationType={AuthType}", + server.DisplayName, server.AuthenticationType); } } diff --git a/Lite/Windows/AddServerDialog.xaml.cs b/Lite/Windows/AddServerDialog.xaml.cs index 05d11ddf..9ad9a16d 100644 --- a/Lite/Windows/AddServerDialog.xaml.cs +++ b/Lite/Windows/AddServerDialog.xaml.cs @@ -232,26 +232,22 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) // Determine authentication type string authenticationType; - bool useWindowsAuth; string? username = null; string? password = null; if (WindowsAuthRadio.IsChecked == true) { authenticationType = AuthenticationTypes.Windows; - useWindowsAuth = true; } else if (EntraMfaAuthRadio.IsChecked == true) { authenticationType = AuthenticationTypes.EntraMFA; - useWindowsAuth = false; // Optionally store username for MFA username = EntraMfaUsernameBox.Text.Trim(); } else // SQL Server Authentication { authenticationType = AuthenticationTypes.SqlServer; - useWindowsAuth = false; username = UsernameBox.Text.Trim(); password = PasswordBox.Password; @@ -269,7 +265,6 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) /* Editing existing server */ AddedServer.ServerName = serverName; AddedServer.DisplayName = displayName; - AddedServer.UseWindowsAuth = useWindowsAuth; AddedServer.AuthenticationType = authenticationType; AddedServer.IsEnabled = EnabledCheckBox.IsChecked == true; AddedServer.TrustServerCertificate = TrustCertCheckBox.IsChecked == true; @@ -287,7 +282,6 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) { ServerName = serverName, DisplayName = displayName, - UseWindowsAuth = useWindowsAuth, AuthenticationType = authenticationType, IsEnabled = EnabledCheckBox.IsChecked == true, TrustServerCertificate = TrustCertCheckBox.IsChecked == true, From 28c8aea4dcc20f8d7b55e6ce7c2b1d542c300ac9 Mon Sep 17 00:00:00 2001 From: ClaudioESSilva Date: Fri, 13 Feb 2026 18:05:44 +0000 Subject: [PATCH 10/10] Fix "Minor: _isDialogOpen static flag" --- Lite/Windows/AddServerDialog.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lite/Windows/AddServerDialog.xaml.cs b/Lite/Windows/AddServerDialog.xaml.cs index 9ad9a16d..085c8960 100644 --- a/Lite/Windows/AddServerDialog.xaml.cs +++ b/Lite/Windows/AddServerDialog.xaml.cs @@ -35,7 +35,7 @@ public AddServerDialog(ServerManager serverManager) InitializeComponent(); _serverManager = serverManager; _isDialogOpen = true; - Closing += (s, e) => _isDialogOpen = false; + Closed += (s, e) => _isDialogOpen = false; } ///