diff --git a/Dashboard/AddServerDialog.xaml b/Dashboard/AddServerDialog.xaml
index 24d1a05b..5d39a23f 100644
--- a/Dashboard/AddServerDialog.xaml
+++ b/Dashboard/AddServerDialog.xaml
@@ -26,6 +26,7 @@
+
@@ -50,29 +51,40 @@
-
+
+ Checked="AuthType_Changed" Margin="0,0,0,4"/>
+ Checked="AuthType_Changed" Margin="0,0,0,4"/>
+
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
diff --git a/Dashboard/AddServerDialog.xaml.cs b/Dashboard/AddServerDialog.xaml.cs
index f59c6f89..f4ad1a30 100644
--- a/Dashboard/AddServerDialog.xaml.cs
+++ b/Dashboard/AddServerDialog.xaml.cs
@@ -8,6 +8,9 @@
using System;
using System.Windows;
+using System.Windows.Controls;
+using Microsoft.Data.SqlClient;
+using PerformanceMonitorDashboard.Helpers;
using PerformanceMonitorDashboard.Models;
using PerformanceMonitorDashboard.Services;
@@ -49,11 +52,18 @@ public AddServerDialog(ServerConnection existingServer)
};
TrustServerCertificateCheckBox.IsChecked = existingServer.TrustServerCertificate;
- if (existingServer.UseWindowsAuth)
+ if (existingServer.AuthenticationType == AuthenticationTypes.EntraMFA)
{
- WindowsAuthRadio.IsChecked = true;
+ EntraMfaAuthRadio.IsChecked = true;
+
+ var credentialService = new CredentialService();
+ var cred = credentialService.GetCredential(existingServer.Id);
+ if (cred.HasValue && !string.IsNullOrEmpty(cred.Value.Username))
+ {
+ EntraMfaUsernameBox.Text = cred.Value.Username;
+ }
}
- else
+ else if (existingServer.AuthenticationType == AuthenticationTypes.SqlServer)
{
SqlAuthRadio.IsChecked = true;
@@ -65,17 +75,81 @@ public AddServerDialog(ServerConnection existingServer)
PasswordBox.Password = cred.Value.Password;
}
}
+ else
+ {
+ WindowsAuthRadio.IsChecked = true;
+ }
}
private void AuthType_Changed(object sender, RoutedEventArgs e)
{
- if (SqlAuthPanel != null)
+ if (SqlAuthPanel != null && EntraMfaPanel != null)
{
- SqlAuthPanel.IsEnabled = SqlAuthRadio.IsChecked == true;
+ SqlAuthPanel.Visibility = SqlAuthRadio.IsChecked == true
+ ? System.Windows.Visibility.Visible
+ : System.Windows.Visibility.Collapsed;
+
+ EntraMfaPanel.Visibility = EntraMfaAuthRadio.IsChecked == true
+ ? System.Windows.Visibility.Visible
+ : System.Windows.Visibility.Collapsed;
}
}
- private async void TestConnection_Click(object sender, RoutedEventArgs e)
+ private string GetSelectedEncryptMode()
+ {
+ return EncryptModeComboBox.SelectedIndex switch
+ {
+ 1 => "Mandatory",
+ 2 => "Strict",
+ _ => "Optional"
+ };
+ }
+
+ private static SqlConnectionEncryptOption ParseEncryptOption(string mode)
+ {
+ return mode switch
+ {
+ "Mandatory" => SqlConnectionEncryptOption.Mandatory,
+ "Strict" => SqlConnectionEncryptOption.Strict,
+ _ => SqlConnectionEncryptOption.Optional
+ };
+ }
+
+ private SqlConnectionStringBuilder BuildConnectionBuilder()
+ {
+ var builder = new SqlConnectionStringBuilder
+ {
+ DataSource = ServerNameTextBox.Text.Trim(),
+ InitialCatalog = "PerformanceMonitor",
+ ApplicationName = "PerformanceMonitorDashboard",
+ ConnectTimeout = 10,
+ TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true,
+ Encrypt = ParseEncryptOption(GetSelectedEncryptMode())
+ };
+
+ if (WindowsAuthRadio.IsChecked == true)
+ {
+ builder.IntegratedSecurity = true;
+ }
+ else if (SqlAuthRadio.IsChecked == true)
+ {
+ builder.IntegratedSecurity = false;
+ builder.UserID = UsernameTextBox.Text.Trim();
+ builder.Password = PasswordBox.Password;
+ }
+ else if (EntraMfaAuthRadio.IsChecked == true)
+ {
+ builder.IntegratedSecurity = false;
+ builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive;
+ var mfaUsername = EntraMfaUsernameBox.Text.Trim();
+ if (!string.IsNullOrEmpty(mfaUsername))
+ builder.UserID = mfaUsername;
+ }
+
+ return builder;
+ }
+
+ private bool ValidateInputs()
{
if (string.IsNullOrWhiteSpace(ServerNameTextBox.Text))
{
@@ -85,96 +159,159 @@ private async void TestConnection_Click(object sender, RoutedEventArgs e)
MessageBoxButton.OK,
MessageBoxImage.Warning
);
- return;
+ return false;
}
- if (SqlAuthRadio.IsChecked == true)
+ if (SqlAuthRadio.IsChecked == true && string.IsNullOrWhiteSpace(UsernameTextBox.Text))
{
- if (string.IsNullOrWhiteSpace(UsernameTextBox.Text))
- {
- MessageBox.Show(
- "Please enter a username for SQL Server authentication.",
- "Validation Error",
- MessageBoxButton.OK,
- MessageBoxImage.Warning
- );
- return;
- }
+ MessageBox.Show(
+ "Please enter a username for SQL Server authentication.",
+ "Validation Error",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning
+ );
+ return false;
}
- try
- {
- var testConnection = DatabaseService.BuildConnectionString(
- ServerNameTextBox.Text.Trim(),
- WindowsAuthRadio.IsChecked == true,
- UsernameTextBox.Text.Trim(),
- PasswordBox.Password,
- GetSelectedEncryptMode(),
- TrustServerCertificateCheckBox.IsChecked == true
- ).ConnectionString;
+ return true;
+ }
- var dbService = new DatabaseService(testConnection);
- bool connected = await dbService.TestConnectionAsync();
+ private async System.Threading.Tasks.Task<(bool Connected, string? ErrorMessage, bool MfaCancelled, string? ServerVersion)> RunConnectionTestAsync(Button triggerButton)
+ {
+ triggerButton.IsEnabled = false;
+ SaveButton.IsEnabled = false;
- if (connected)
- {
- MessageBox.Show(
- $"Successfully connected to {ServerNameTextBox.Text}!",
- "Connection Test Successful",
- MessageBoxButton.OK,
- MessageBoxImage.Information
- );
- }
- else
- {
- MessageBox.Show(
- $"Failed to connect to {ServerNameTextBox.Text}.\n\nPlease check:\n" +
- "• Server name/address is correct\n" +
- "• Server is accessible from this machine\n" +
- "• Firewall allows SQL Server connections\n" +
- "• SQL Server service is running",
- "Connection Test Failed",
- MessageBoxButton.OK,
- MessageBoxImage.Error
- );
- }
+ StatusText.Text = EntraMfaAuthRadio.IsChecked == true
+ ? "Testing connection — please complete authentication in the popup window..."
+ : "Testing connection...";
+ StatusText.Visibility = System.Windows.Visibility.Visible;
+
+ bool connected = false;
+ string? errorMessage = null;
+ bool mfaCancelled = false;
+ string? serverVersion = null;
+ try
+ {
+ await using var connection = new SqlConnection(BuildConnectionBuilder().ConnectionString);
+ await connection.OpenAsync();
+ using var cmd = new SqlCommand("SELECT @@VERSION", connection);
+ var version = await cmd.ExecuteScalarAsync() as string;
+ serverVersion = version?.Split('\n')[0]?.Trim();
+ connected = true;
}
catch (Exception ex)
{
- MessageBox.Show(
- $"Connection test failed:\n\n{ex.Message}",
- "Connection Test Error",
- MessageBoxButton.OK,
- MessageBoxImage.Error
- );
+ connected = false;
+ errorMessage = ex.Message;
+ if (EntraMfaAuthRadio.IsChecked == true && MfaAuthenticationHelper.IsMfaCancelledException(ex))
+ mfaCancelled = true;
+ }
+ finally
+ {
+ triggerButton.IsEnabled = true;
+ SaveButton.IsEnabled = true;
+ StatusText.Text = string.Empty;
+ StatusText.Visibility = System.Windows.Visibility.Collapsed;
}
+
+ return (connected, errorMessage, mfaCancelled, serverVersion);
}
- private void Save_Click(object sender, RoutedEventArgs e)
+ private async void TestConnection_Click(object sender, RoutedEventArgs e)
{
- if (string.IsNullOrWhiteSpace(ServerNameTextBox.Text))
+ if (!ValidateInputs()) return;
+
+ var (connected, errorMessage, mfaCancelled, serverVersion) = await RunConnectionTestAsync(TestConnectionButton);
+
+ if (connected)
{
+ var message = serverVersion != null
+ ? $"Successfully connected to {ServerNameTextBox.Text}!\n\n{serverVersion}"
+ : $"Successfully connected to {ServerNameTextBox.Text}!";
MessageBox.Show(
- "Please enter a server name or address.",
- "Validation Error",
+ message,
+ "Connection Successful",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information
+ );
+ }
+ else if (mfaCancelled)
+ {
+ MessageBox.Show(
+ "Authentication was cancelled. Click Test to try again.",
+ "Authentication Cancelled",
MessageBoxButton.OK,
MessageBoxImage.Warning
);
- return;
}
+ else
+ {
+ var detail = errorMessage != null ? $"\n\nError: {errorMessage}" : string.Empty;
+ MessageBox.Show(
+ $"Could not connect to {ServerNameTextBox.Text}.{detail}\n\nPlease check:\n" +
+ "• Server name/address is correct\n" +
+ "• Server is accessible from this machine\n" +
+ "• Firewall allows SQL Server connections\n" +
+ "• SQL Server service is running\n" +
+ "• You have the 'PerformanceMonitor' database and access to it",
+ "Connection Test Failed",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
+ }
+ }
- if (SqlAuthRadio.IsChecked == true)
+ private async void Save_Click(object sender, RoutedEventArgs e)
+ {
+ if (!ValidateInputs()) return;
+
+ var (connected, errorMessage, mfaCancelled, _) = await RunConnectionTestAsync(SaveButton);
+
+ if (!connected)
{
- if (string.IsNullOrWhiteSpace(UsernameTextBox.Text))
+ if (mfaCancelled)
{
MessageBox.Show(
- "Please enter a username for SQL Server authentication.",
- "Validation Error",
+ "Authentication was cancelled. Click Save to try again, or Cancel to abort.",
+ "Authentication Cancelled",
MessageBoxButton.OK,
MessageBoxImage.Warning
);
return;
}
+
+ var detail = errorMessage != null ? $"\n\nError: {errorMessage}" : string.Empty;
+ var result = MessageBox.Show(
+ $"Could not connect to {ServerNameTextBox.Text}.{detail}\n\n" +
+ "Do you still want to save this connection?",
+ "Connection Failed",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Warning
+ );
+
+ if (result != MessageBoxResult.Yes)
+ return;
+ }
+
+ // Determine authentication type and credentials
+ string authenticationType;
+ if (WindowsAuthRadio.IsChecked == true)
+ {
+ authenticationType = AuthenticationTypes.Windows;
+ Username = null;
+ Password = null;
+ }
+ else if (EntraMfaAuthRadio.IsChecked == true)
+ {
+ authenticationType = AuthenticationTypes.EntraMFA;
+ Username = EntraMfaUsernameBox.Text.Trim();
+ Password = null;
+ }
+ else
+ {
+ authenticationType = AuthenticationTypes.SqlServer;
+ Username = UsernameTextBox.Text.Trim();
+ Password = PasswordBox.Password;
}
// Use server name as display name if not provided
@@ -186,7 +323,7 @@ private void Save_Click(object sender, RoutedEventArgs e)
{
ServerConnection.DisplayName = displayName;
ServerConnection.ServerName = ServerNameTextBox.Text.Trim();
- ServerConnection.UseWindowsAuth = WindowsAuthRadio.IsChecked == true;
+ ServerConnection.AuthenticationType = authenticationType;
ServerConnection.Description = DescriptionTextBox.Text.Trim();
ServerConnection.IsFavorite = IsFavoriteCheckBox.IsChecked == true;
ServerConnection.EncryptMode = GetSelectedEncryptMode();
@@ -198,7 +335,7 @@ private void Save_Click(object sender, RoutedEventArgs e)
{
DisplayName = displayName,
ServerName = ServerNameTextBox.Text.Trim(),
- UseWindowsAuth = WindowsAuthRadio.IsChecked == true,
+ AuthenticationType = authenticationType,
Description = DescriptionTextBox.Text.Trim(),
IsFavorite = IsFavoriteCheckBox.IsChecked == true,
CreatedDate = DateTime.Now,
@@ -208,12 +345,6 @@ private void Save_Click(object sender, RoutedEventArgs e)
};
}
- if (SqlAuthRadio.IsChecked == true)
- {
- Username = UsernameTextBox.Text.Trim();
- Password = PasswordBox.Password;
- }
-
DialogResult = true;
Close();
}
@@ -224,14 +355,5 @@ private void Cancel_Click(object sender, RoutedEventArgs e)
Close();
}
- private string GetSelectedEncryptMode()
- {
- return EncryptModeComboBox.SelectedIndex switch
- {
- 1 => "Mandatory",
- 2 => "Strict",
- _ => "Optional"
- };
- }
}
}
diff --git a/Dashboard/Helpers/MfaAuthenticationHelper.cs b/Dashboard/Helpers/MfaAuthenticationHelper.cs
new file mode 100644
index 00000000..3f4b1efe
--- /dev/null
+++ b/Dashboard/Helpers/MfaAuthenticationHelper.cs
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+
+namespace PerformanceMonitorDashboard.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/Dashboard/Interfaces/IServerManager.cs b/Dashboard/Interfaces/IServerManager.cs
index c305ca8f..6e98649f 100644
--- a/Dashboard/Interfaces/IServerManager.cs
+++ b/Dashboard/Interfaces/IServerManager.cs
@@ -55,7 +55,7 @@ public interface IServerManager
///
/// Tests connectivity to a single server and updates its status.
///
- Task CheckConnectionAsync(string serverId);
+ Task CheckConnectionAsync(string serverId, bool allowInteractiveAuth = false);
///
/// Tests connectivity to all servers and updates their statuses.
diff --git a/Dashboard/MainWindow.xaml.cs b/Dashboard/MainWindow.xaml.cs
index 494b5c38..0908b527 100644
--- a/Dashboard/MainWindow.xaml.cs
+++ b/Dashboard/MainWindow.xaml.cs
@@ -711,7 +711,7 @@ private void AddServer_Click(object sender, RoutedEventArgs e)
MessageBox.Show(
$"Server '{server.DisplayName}' added successfully!\n\n" +
- (server.UseWindowsAuth ? "Using Windows Authentication" : "Credentials saved securely to Windows Credential Manager"),
+ (server.AuthenticationType == Models.AuthenticationTypes.Windows ? "Using Windows Authentication" : $"Using {server.AuthenticationDisplay} — credentials saved securely to Windows Credential Manager"),
"Server Added",
MessageBoxButton.OK,
MessageBoxImage.Information
@@ -757,7 +757,7 @@ private void EditServer_Click(object sender, RoutedEventArgs e)
MessageBox.Show(
$"Server '{updatedServer.DisplayName}' updated successfully!\n\n" +
- (updatedServer.UseWindowsAuth ? "Using Windows Authentication" : "Credentials updated securely in Windows Credential Manager"),
+ (updatedServer.AuthenticationType == Models.AuthenticationTypes.Windows ? "Using Windows Authentication" : $"Using {updatedServer.AuthenticationDisplay} — credentials updated securely in Windows Credential Manager"),
"Server Updated",
MessageBoxButton.OK,
MessageBoxImage.Information
diff --git a/Dashboard/Models/AuthenticationTypes.cs b/Dashboard/Models/AuthenticationTypes.cs
new file mode 100644
index 00000000..37767bf0
--- /dev/null
+++ b/Dashboard/Models/AuthenticationTypes.cs
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+namespace PerformanceMonitorDashboard.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/Dashboard/Models/ServerConnection.cs b/Dashboard/Models/ServerConnection.cs
index 701f1de5..3ef8dce6 100644
--- a/Dashboard/Models/ServerConnection.cs
+++ b/Dashboard/Models/ServerConnection.cs
@@ -8,6 +8,7 @@
using System;
using System.Text.Json.Serialization;
+using Microsoft.Data.SqlClient;
using PerformanceMonitorDashboard.Interfaces;
using PerformanceMonitorDashboard.Services;
@@ -18,7 +19,32 @@ 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.
+ ///
+ public string AuthenticationType { get; set; } = AuthenticationTypes.Windows;
public string? Description { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.Now;
public DateTime LastConnected { get; set; } = DateTime.Now;
@@ -40,7 +66,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
+ {
+ AuthenticationTypes.EntraMFA => "Microsoft Entra MFA",
+ AuthenticationTypes.SqlServer => "SQL Server",
+ _ => "Windows"
+ };
///
/// SECURITY: Credentials are NEVER serialized to JSON.
@@ -51,10 +82,38 @@ public class ServerConnection
/// Connection string for SQL Server
public string GetConnectionString(ICredentialService credentialService)
{
+ if (AuthenticationType == AuthenticationTypes.EntraMFA)
+ {
+ // Build MFA connection string with ActiveDirectoryInteractive
+ var mfaBuilder = new SqlConnectionStringBuilder
+ {
+ DataSource = ServerName,
+ InitialCatalog = "PerformanceMonitor",
+ ApplicationName = "PerformanceMonitorDashboard",
+ ConnectTimeout = 15,
+ MultipleActiveResultSets = true,
+ TrustServerCertificate = TrustServerCertificate,
+ Encrypt = EncryptMode switch
+ {
+ "Optional" => SqlConnectionEncryptOption.Optional,
+ "Strict" => SqlConnectionEncryptOption.Strict,
+ _ => SqlConnectionEncryptOption.Mandatory
+ },
+ Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive
+ };
+
+ // Optionally pre-populate username from credential store
+ var mfaCred = credentialService.GetCredential(Id);
+ if (mfaCred.HasValue && !string.IsNullOrEmpty(mfaCred.Value.Username))
+ mfaBuilder.UserID = mfaCred.Value.Username;
+
+ return mfaBuilder.ConnectionString;
+ }
+
string? username = null;
string? password = null;
- if (!UseWindowsAuth)
+ if (AuthenticationType == AuthenticationTypes.SqlServer)
{
var cred = credentialService.GetCredential(Id);
if (cred.HasValue)
@@ -79,10 +138,10 @@ public string GetConnectionString(ICredentialService credentialService)
/// Used to validate that SQL auth servers have credentials available.
///
/// The credential service to use for checking credentials
- /// True if Windows auth is used or if credentials exist in credential manager
+ /// True if Windows auth or MFA is used, or if credentials exist in credential manager
public bool HasStoredCredentials(ICredentialService credentialService)
{
- if (UseWindowsAuth)
+ if (AuthenticationType == AuthenticationTypes.Windows || AuthenticationType == AuthenticationTypes.EntraMFA)
{
return true;
}
diff --git a/Dashboard/Models/ServerConnectionStatus.cs b/Dashboard/Models/ServerConnectionStatus.cs
index 5000dd14..ac102bb7 100644
--- a/Dashboard/Models/ServerConnectionStatus.cs
+++ b/Dashboard/Models/ServerConnectionStatus.cs
@@ -68,6 +68,12 @@ public class ServerConnectionStatus
///
public bool IsAwsRds { get; set; }
+ ///
+ /// Whether the user cancelled MFA authentication for this server.
+ /// When true, background connectivity checks are skipped to avoid repeated authentication popups.
+ ///
+ public bool UserCancelledMfa { get; set; }
+
///
/// The server's UTC offset in minutes, queried via DATEDIFF(MINUTE, GETUTCDATE(), GETDATE()).
/// Used to convert UTC-stored collection_time values to server-local time for display.
diff --git a/Dashboard/Services/ServerManager.cs b/Dashboard/Services/ServerManager.cs
index cf9099dd..f0a1d667 100644
--- a/Dashboard/Services/ServerManager.cs
+++ b/Dashboard/Services/ServerManager.cs
@@ -82,13 +82,22 @@ public void AddServer(ServerConnection server, string? username = null, string?
SaveServersInternal();
}
- if (!server.UseWindowsAuth && !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))
{
throw new InvalidOperationException("Failed to save credentials to Windows Credential Manager");
}
}
+ else if (server.AuthenticationType == AuthenticationTypes.EntraMFA && !string.IsNullOrEmpty(username))
+ {
+ // For MFA auth, save username hint only (no password needed)
+ if (!_credentialService.SaveCredential(server.Id, username, string.Empty))
+ {
+ throw new InvalidOperationException("Failed to save username to Windows Credential Manager");
+ }
+ }
// Initialize status as unknown for new server
_connectionStatuses[server.Id] = new ServerConnectionStatus { ServerId = server.Id };
@@ -109,15 +118,25 @@ public void UpdateServer(ServerConnection server, string? username = null, strin
SaveServersInternal();
}
- if (!server.UseWindowsAuth && !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))
{
throw new InvalidOperationException("Failed to update credentials in Windows Credential Manager");
}
}
- else if (server.UseWindowsAuth)
+ else if (server.AuthenticationType == AuthenticationTypes.EntraMFA && !string.IsNullOrEmpty(username))
+ {
+ // For MFA auth, update username hint only (no password needed)
+ if (!_credentialService.UpdateCredential(server.Id, username, string.Empty))
+ {
+ throw new InvalidOperationException("Failed to update username in Windows Credential Manager");
+ }
+ }
+ else if (server.AuthenticationType == AuthenticationTypes.Windows)
{
+ // For Windows auth, remove any stored credentials
_credentialService.DeleteCredential(server.Id);
}
}
@@ -191,7 +210,7 @@ public ServerConnectionStatus GetConnectionStatus(string serverId)
return newStatus;
}
- public async Task CheckConnectionAsync(string serverId)
+ public async Task CheckConnectionAsync(string serverId, bool allowInteractiveAuth = false)
{
var server = GetServerById(serverId);
if (server == null)
@@ -209,6 +228,25 @@ 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 == AuthenticationTypes.EntraMFA)
+ {
+ var errorMsg = previousStatus.UserCancelledMfa
+ ? "Authentication cancelled by user"
+ : "Skipped — requires interactive authentication";
+
+ return new ServerConnectionStatus
+ {
+ ServerId = serverId,
+ IsOnline = previousStatus.UserCancelledMfa ? false : previousStatus.IsOnline,
+ LastChecked = DateTime.Now,
+ StatusChangedAt = previousStatus.StatusChangedAt,
+ ErrorMessage = errorMsg,
+ PreviousIsOnline = previousStatus.IsOnline,
+ UserCancelledMfa = previousStatus.UserCancelledMfa
+ };
+ }
+
var status = new ServerConnectionStatus
{
ServerId = serverId,
@@ -273,19 +311,40 @@ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds
else
{
Logger.Info($"Connectivity check passed for server '{server.DisplayName}'");
+ status.UserCancelledMfa = false; // Clear any previous cancellation flag
}
}
catch (SqlException ex)
{
status.IsOnline = false;
status.ErrorMessage = ex.Message;
- Logger.Warning($"Connectivity check failed for server '{server.DisplayName}': {ex.Message}");
+
+ if (server.AuthenticationType == AuthenticationTypes.EntraMFA && MfaAuthenticationHelper.IsMfaCancelledException(ex))
+ {
+ status.UserCancelledMfa = true;
+ status.ErrorMessage = "Authentication cancelled by user";
+ Logger.Info($"MFA authentication cancelled by user for server '{server.DisplayName}'");
+ }
+ else
+ {
+ Logger.Warning($"Connectivity check failed for server '{server.DisplayName}': {ex.Message}");
+ }
}
catch (Exception ex)
{
status.IsOnline = false;
status.ErrorMessage = ex.Message;
- Logger.Warning($"Connectivity check error for server '{server.DisplayName}': {ex.Message}");
+
+ if (server.AuthenticationType == AuthenticationTypes.EntraMFA && MfaAuthenticationHelper.IsMfaCancelledException(ex))
+ {
+ status.UserCancelledMfa = true;
+ status.ErrorMessage = "Authentication cancelled by user";
+ Logger.Info($"MFA authentication cancelled by user for server '{server.DisplayName}'");
+ }
+ else
+ {
+ Logger.Warning($"Connectivity check error for server '{server.DisplayName}': {ex.Message}");
+ }
}
// Track when status changed (online to offline or vice versa)
diff --git a/Lite/Windows/AddServerDialog.xaml.cs b/Lite/Windows/AddServerDialog.xaml.cs
index 085c8960..3e70d8af 100644
--- a/Lite/Windows/AddServerDialog.xaml.cs
+++ b/Lite/Windows/AddServerDialog.xaml.cs
@@ -130,92 +130,130 @@ private static SqlConnectionEncryptOption ParseEncryptOption(string mode)
};
}
- private async void TestButton_Click(object sender, RoutedEventArgs e)
+ private SqlConnectionStringBuilder BuildConnectionBuilder()
{
- var serverName = ServerNameBox.Text.Trim();
- if (string.IsNullOrEmpty(serverName))
+ var dbName = DatabaseNameBox.Text.Trim();
+ var builder = new SqlConnectionStringBuilder
{
- StatusText.Text = "Enter a server name first.";
- return;
+ DataSource = ServerNameBox.Text.Trim(),
+ InitialCatalog = string.IsNullOrEmpty(dbName) ? "master" : dbName,
+ ApplicationName = "PerformanceMonitorLite",
+ ConnectTimeout = 10,
+ TrustServerCertificate = TrustCertCheckBox.IsChecked == true,
+ Encrypt = ParseEncryptOption(GetSelectedEncryptMode())
+ };
+
+ if (WindowsAuthRadio.IsChecked == true)
+ {
+ builder.IntegratedSecurity = true;
+ }
+ else if (SqlAuthRadio.IsChecked == true)
+ {
+ builder.IntegratedSecurity = false;
+ builder.UserID = UsernameBox.Text.Trim();
+ builder.Password = PasswordBox.Password;
+ }
+ else if (EntraMfaAuthRadio.IsChecked == true)
+ {
+ builder.IntegratedSecurity = false;
+ builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive;
+ var mfaUsername = EntraMfaUsernameBox.Text.Trim();
+ if (!string.IsNullOrEmpty(mfaUsername))
+ builder.UserID = mfaUsername;
}
+ return builder;
+ }
+
+ private async System.Threading.Tasks.Task<(bool Connected, string? ErrorMessage, bool MfaCancelled, string? ServerVersion)> RunConnectionTestAsync()
+ {
TestButton.IsEnabled = false;
- StatusText.Text = "Testing connection...";
+ SaveButton.IsEnabled = false;
+
+ StatusText.Text = EntraMfaAuthRadio.IsChecked == true
+ ? "Testing connection — please complete authentication in the popup window..."
+ : "Testing connection...";
+
+ bool connected = false;
+ string? errorMessage = null;
+ bool mfaCancelled = false;
+ string? serverVersion = null;
try
{
- var dbName = DatabaseNameBox.Text.Trim();
- var builder = new SqlConnectionStringBuilder
- {
- DataSource = serverName,
- InitialCatalog = string.IsNullOrEmpty(dbName) ? "master" : dbName,
- ApplicationName = "PerformanceMonitorLite",
- ConnectTimeout = 10,
- TrustServerCertificate = TrustCertCheckBox.IsChecked == true,
- Encrypt = ParseEncryptOption(GetSelectedEncryptMode())
- };
-
- if (WindowsAuthRadio.IsChecked == true)
- {
- builder.IntegratedSecurity = true;
- }
- 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 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);
+ using var connection = new SqlConnection(BuildConnectionBuilder().ConnectionString);
await connection.OpenAsync();
-
using var cmd = new SqlCommand("SELECT @@VERSION", connection);
var version = await cmd.ExecuteScalarAsync() as string;
- var shortVersion = version?.Split('\n')[0] ?? "Connected";
+ serverVersion = version?.Split('\n')[0]?.Trim();
+ connected = true;
+ }
+ catch (Exception ex)
+ {
+ errorMessage = ex.Message;
+ if (EntraMfaAuthRadio.IsChecked == true && MfaAuthenticationHelper.IsMfaCancelledException(ex))
+ mfaCancelled = true;
+ }
+ finally
+ {
+ TestButton.IsEnabled = true;
+ SaveButton.IsEnabled = true;
+ StatusText.Text = string.Empty;
+ }
+
+ return (connected, errorMessage, mfaCancelled, serverVersion);
+ }
+
+ private async void TestButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (string.IsNullOrEmpty(ServerNameBox.Text.Trim()))
+ {
+ StatusText.Text = "Enter a server name first.";
+ return;
+ }
+
+ var (connected, errorMessage, mfaCancelled, serverVersion) = await RunConnectionTestAsync();
+
+ if (connected)
+ {
+ var message = serverVersion != null
+ ? $"Successfully connected to {ServerNameBox.Text.Trim()}!\n\n{serverVersion}"
+ : $"Successfully connected to {ServerNameBox.Text.Trim()}!";
+ MessageBox.Show(message, "Connection Successful", MessageBoxButton.OK, MessageBoxImage.Information);
- 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)
+ else if (mfaCancelled)
{
- StatusText.Text = $"Failed: {ex.Message}";
-
- // Mark MFA as cancelled if user cancelled the authentication popup
- if (AddedServer != null && EntraMfaAuthRadio.IsChecked == true && MfaAuthenticationHelper.IsMfaCancelledException(ex))
+ if (AddedServer != null)
{
var status = _serverManager.GetConnectionStatus(AddedServer.Id);
status.UserCancelledMfa = true;
- StatusText.Text = "Authentication cancelled by user. Click Test to try again.";
}
+ MessageBox.Show(
+ "Authentication was cancelled. Click Test to try again.",
+ "Authentication Cancelled",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning
+ );
}
- finally
+ else
{
- TestButton.IsEnabled = true;
+ var detail = errorMessage != null ? $"\n\nError: {errorMessage}" : string.Empty;
+ MessageBox.Show(
+ $"Could not connect to {ServerNameBox.Text.Trim()}.{detail}",
+ "Connection Failed",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
}
}
- private void SaveButton_Click(object sender, RoutedEventArgs e)
+ private async void SaveButton_Click(object sender, RoutedEventArgs e)
{
var serverName = ServerNameBox.Text.Trim();
if (string.IsNullOrEmpty(serverName))
@@ -226,9 +264,7 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
var displayName = DisplayNameBox.Text.Trim();
if (string.IsNullOrEmpty(displayName))
- {
displayName = serverName;
- }
// Determine authentication type
string authenticationType;
@@ -242,7 +278,6 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
else if (EntraMfaAuthRadio.IsChecked == true)
{
authenticationType = AuthenticationTypes.EntraMFA;
- // Optionally store username for MFA
username = EntraMfaUsernameBox.Text.Trim();
}
else // SQL Server Authentication
@@ -258,6 +293,41 @@ private void SaveButton_Click(object sender, RoutedEventArgs e)
}
}
+ // Test connection when data collection is enabled
+ if (EnabledCheckBox.IsChecked == true)
+ {
+ var (connected, errorMessage, mfaCancelled, _) = await RunConnectionTestAsync();
+
+ if (!connected)
+ {
+ if (mfaCancelled)
+ {
+ if (AddedServer != null)
+ {
+ var status = _serverManager.GetConnectionStatus(AddedServer.Id);
+ status.UserCancelledMfa = true;
+ }
+ MessageBox.Show(
+ "Authentication was cancelled. Click Save to try again, or uncheck \"Enable data collection for this server\" to save without connecting.",
+ "Authentication Cancelled",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning
+ );
+ }
+ else
+ {
+ var detail = errorMessage != null ? $"\n\nError: {errorMessage}" : string.Empty;
+ MessageBox.Show(
+ $"Could not connect to {ServerNameBox.Text.Trim()}.{detail}\n\nTo save this server without a working connection, uncheck \"Enable data collection for this server\".",
+ "Connection Failed",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error
+ );
+ }
+ return;
+ }
+ }
+
try
{
if (AddedServer != null && Title == "Edit Server")