Skip to content

Commit 17df4a9

Browse files
Add MFA to Dashboard
1 parent f99e81e commit 17df4a9

9 files changed

Lines changed: 328 additions & 51 deletions

Dashboard/AddServerDialog.xaml

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,29 +51,40 @@
5151
<!-- Authentication Type -->
5252
<TextBlock Text="Authentication:" FontWeight="Bold" Margin="0,0,0,5"
5353
Foreground="{DynamicResource ForegroundBrush}"/>
54-
<StackPanel Orientation="Horizontal" Margin="0,0,0,15">
54+
<StackPanel Margin="0,0,0,15">
5555
<RadioButton x:Name="WindowsAuthRadio" Content="Windows Authentication"
5656
GroupName="Auth" IsChecked="True"
5757
Foreground="{DynamicResource ForegroundBrush}"
58-
Checked="AuthType_Changed" Margin="0,0,20,0"/>
58+
Checked="AuthType_Changed" Margin="0,0,0,4"/>
5959
<RadioButton x:Name="SqlAuthRadio" Content="SQL Server Authentication"
6060
GroupName="Auth"
6161
Foreground="{DynamicResource ForegroundBrush}"
62-
Checked="AuthType_Changed"/>
62+
Checked="AuthType_Changed" Margin="0,0,0,4"/>
63+
<RadioButton x:Name="EntraMfaAuthRadio" Content="Microsoft Entra MFA"
64+
GroupName="Auth"
65+
Foreground="{DynamicResource ForegroundBrush}"
66+
Checked="AuthType_Changed"
67+
ToolTip="Interactive authentication with MFA for Azure SQL Database."/>
6368
</StackPanel>
6469

65-
<!-- SQL Authentication Fields (initially disabled) -->
66-
<Border x:Name="SqlAuthPanel" IsEnabled="False">
67-
<StackPanel>
68-
<TextBlock Text="Username:" FontWeight="Bold" Margin="0,0,0,5"
69-
Foreground="{DynamicResource ForegroundBrush}"/>
70-
<TextBox x:Name="UsernameTextBox" Height="25" Margin="0,0,0,15"/>
70+
<!-- SQL Authentication Fields -->
71+
<StackPanel x:Name="SqlAuthPanel" Visibility="Collapsed" Margin="0,0,0,15">
72+
<TextBlock Text="Username:" FontWeight="Bold" Margin="0,0,0,5"
73+
Foreground="{DynamicResource ForegroundBrush}"/>
74+
<TextBox x:Name="UsernameTextBox" Height="25" Margin="0,0,0,15"/>
7175

72-
<TextBlock Text="Password:" FontWeight="Bold" Margin="0,0,0,5"
73-
Foreground="{DynamicResource ForegroundBrush}"/>
74-
<PasswordBox x:Name="PasswordBox" Height="25" Margin="0,0,0,15"/>
75-
</StackPanel>
76-
</Border>
76+
<TextBlock Text="Password:" FontWeight="Bold" Margin="0,0,0,5"
77+
Foreground="{DynamicResource ForegroundBrush}"/>
78+
<PasswordBox x:Name="PasswordBox" Height="25" Margin="0,0,0,0"/>
79+
</StackPanel>
80+
81+
<!-- Microsoft Entra MFA Fields -->
82+
<StackPanel x:Name="EntraMfaPanel" Visibility="Collapsed" Margin="0,0,0,15">
83+
<TextBlock Text="Username (optional):" FontWeight="Bold" Margin="0,0,0,5"
84+
Foreground="{DynamicResource ForegroundBrush}"/>
85+
<TextBox x:Name="EntraMfaUsernameBox" Height="25" Margin="0,0,0,0"
86+
ToolTip="Optional: Pre-populate the authentication dialog with this email address"/>
87+
</StackPanel>
7788

7889
<!-- Description -->
7990
<TextBlock Text="Description (optional):" FontWeight="Bold" Margin="0,0,0,5"

Dashboard/AddServerDialog.xaml.cs

Lines changed: 99 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Windows;
1111
using System.Windows.Controls;
1212
using Microsoft.Data.SqlClient;
13+
using PerformanceMonitorDashboard.Helpers;
1314
using PerformanceMonitorDashboard.Models;
1415
using PerformanceMonitorDashboard.Services;
1516

@@ -51,11 +52,18 @@ public AddServerDialog(ServerConnection existingServer)
5152
};
5253
TrustServerCertificateCheckBox.IsChecked = existingServer.TrustServerCertificate;
5354

54-
if (existingServer.UseWindowsAuth)
55+
if (existingServer.AuthenticationType == AuthenticationTypes.EntraMFA)
5556
{
56-
WindowsAuthRadio.IsChecked = true;
57+
EntraMfaAuthRadio.IsChecked = true;
58+
59+
var credentialService = new CredentialService();
60+
var cred = credentialService.GetCredential(existingServer.Id);
61+
if (cred.HasValue && !string.IsNullOrEmpty(cred.Value.Username))
62+
{
63+
EntraMfaUsernameBox.Text = cred.Value.Username;
64+
}
5765
}
58-
else
66+
else if (existingServer.AuthenticationType == AuthenticationTypes.SqlServer)
5967
{
6068
SqlAuthRadio.IsChecked = true;
6169

@@ -67,13 +75,23 @@ public AddServerDialog(ServerConnection existingServer)
6775
PasswordBox.Password = cred.Value.Password;
6876
}
6977
}
78+
else
79+
{
80+
WindowsAuthRadio.IsChecked = true;
81+
}
7082
}
7183

7284
private void AuthType_Changed(object sender, RoutedEventArgs e)
7385
{
74-
if (SqlAuthPanel != null)
86+
if (SqlAuthPanel != null && EntraMfaPanel != null)
7587
{
76-
SqlAuthPanel.IsEnabled = SqlAuthRadio.IsChecked == true;
88+
SqlAuthPanel.Visibility = SqlAuthRadio.IsChecked == true
89+
? System.Windows.Visibility.Visible
90+
: System.Windows.Visibility.Collapsed;
91+
92+
EntraMfaPanel.Visibility = EntraMfaAuthRadio.IsChecked == true
93+
? System.Windows.Visibility.Visible
94+
: System.Windows.Visibility.Collapsed;
7795
}
7896
}
7997

@@ -106,15 +124,27 @@ private SqlConnectionStringBuilder BuildConnectionBuilder()
106124
ApplicationName = "PerformanceMonitorDashboard",
107125
ConnectTimeout = 10,
108126
TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true,
109-
Encrypt = ParseEncryptOption(GetSelectedEncryptMode()),
110-
IntegratedSecurity = WindowsAuthRadio.IsChecked == true
127+
Encrypt = ParseEncryptOption(GetSelectedEncryptMode())
111128
};
112129

113-
if (WindowsAuthRadio.IsChecked != true)
130+
if (WindowsAuthRadio.IsChecked == true)
131+
{
132+
builder.IntegratedSecurity = true;
133+
}
134+
else if (SqlAuthRadio.IsChecked == true)
114135
{
136+
builder.IntegratedSecurity = false;
115137
builder.UserID = UsernameTextBox.Text.Trim();
116138
builder.Password = PasswordBox.Password;
117139
}
140+
else if (EntraMfaAuthRadio.IsChecked == true)
141+
{
142+
builder.IntegratedSecurity = false;
143+
builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive;
144+
var mfaUsername = EntraMfaUsernameBox.Text.Trim();
145+
if (!string.IsNullOrEmpty(mfaUsername))
146+
builder.UserID = mfaUsername;
147+
}
118148

119149
return builder;
120150
}
@@ -146,15 +176,19 @@ private bool ValidateInputs()
146176
return true;
147177
}
148178

149-
private async System.Threading.Tasks.Task<(bool Connected, string? ErrorMessage, string? ServerVersion)> RunConnectionTestAsync(Button triggerButton)
179+
private async System.Threading.Tasks.Task<(bool Connected, string? ErrorMessage, bool MfaCancelled, string? ServerVersion)> RunConnectionTestAsync(Button triggerButton)
150180
{
151181
triggerButton.IsEnabled = false;
152182
SaveButton.IsEnabled = false;
153-
StatusText.Text = "Testing connection...";
183+
184+
StatusText.Text = EntraMfaAuthRadio.IsChecked == true
185+
? "Testing connection — please complete authentication in the popup window..."
186+
: "Testing connection...";
154187
StatusText.Visibility = System.Windows.Visibility.Visible;
155188

156189
bool connected = false;
157190
string? errorMessage = null;
191+
bool mfaCancelled = false;
158192
string? serverVersion = null;
159193
try
160194
{
@@ -169,6 +203,8 @@ private bool ValidateInputs()
169203
{
170204
connected = false;
171205
errorMessage = ex.Message;
206+
if (EntraMfaAuthRadio.IsChecked == true && MfaAuthenticationHelper.IsMfaCancelledException(ex))
207+
mfaCancelled = true;
172208
}
173209
finally
174210
{
@@ -178,14 +214,14 @@ private bool ValidateInputs()
178214
StatusText.Visibility = System.Windows.Visibility.Collapsed;
179215
}
180216

181-
return (connected, errorMessage, serverVersion);
217+
return (connected, errorMessage, mfaCancelled, serverVersion);
182218
}
183219

184220
private async void TestConnection_Click(object sender, RoutedEventArgs e)
185221
{
186222
if (!ValidateInputs()) return;
187223

188-
var (connected, errorMessage, serverVersion) = await RunConnectionTestAsync(TestConnectionButton);
224+
var (connected, errorMessage, mfaCancelled, serverVersion) = await RunConnectionTestAsync(TestConnectionButton);
189225

190226
if (connected)
191227
{
@@ -199,12 +235,26 @@ private async void TestConnection_Click(object sender, RoutedEventArgs e)
199235
MessageBoxImage.Information
200236
);
201237
}
238+
else if (mfaCancelled)
239+
{
240+
MessageBox.Show(
241+
"Authentication was cancelled. Click Test to try again.",
242+
"Authentication Cancelled",
243+
MessageBoxButton.OK,
244+
MessageBoxImage.Warning
245+
);
246+
}
202247
else
203248
{
204249
var detail = errorMessage != null ? $"\n\nError: {errorMessage}" : string.Empty;
205250
MessageBox.Show(
206-
$"Could not connect to {ServerNameTextBox.Text}.{detail}",
207-
"Connection Failed",
251+
$"Could not connect to {ServerNameTextBox.Text}.{detail}\n\nPlease check:\n" +
252+
"• Server name/address is correct\n" +
253+
"• Server is accessible from this machine\n" +
254+
"• Firewall allows SQL Server connections\n" +
255+
"• SQL Server service is running\n" +
256+
"• You have the 'PerformanceMonitor' database and access to it",
257+
"Connection Test Failed",
208258
MessageBoxButton.OK,
209259
MessageBoxImage.Error
210260
);
@@ -215,10 +265,21 @@ private async void Save_Click(object sender, RoutedEventArgs e)
215265
{
216266
if (!ValidateInputs()) return;
217267

218-
var (connected, errorMessage, _) = await RunConnectionTestAsync(SaveButton);
268+
var (connected, errorMessage, mfaCancelled, _) = await RunConnectionTestAsync(SaveButton);
219269

220270
if (!connected)
221271
{
272+
if (mfaCancelled)
273+
{
274+
MessageBox.Show(
275+
"Authentication was cancelled. Click Save to try again, or Cancel to abort.",
276+
"Authentication Cancelled",
277+
MessageBoxButton.OK,
278+
MessageBoxImage.Warning
279+
);
280+
return;
281+
}
282+
222283
var detail = errorMessage != null ? $"\n\nError: {errorMessage}" : string.Empty;
223284
var result = MessageBox.Show(
224285
$"Could not connect to {ServerNameTextBox.Text}.{detail}\n\n" +
@@ -232,6 +293,27 @@ private async void Save_Click(object sender, RoutedEventArgs e)
232293
return;
233294
}
234295

296+
// Determine authentication type and credentials
297+
string authenticationType;
298+
if (WindowsAuthRadio.IsChecked == true)
299+
{
300+
authenticationType = AuthenticationTypes.Windows;
301+
Username = null;
302+
Password = null;
303+
}
304+
else if (EntraMfaAuthRadio.IsChecked == true)
305+
{
306+
authenticationType = AuthenticationTypes.EntraMFA;
307+
Username = EntraMfaUsernameBox.Text.Trim();
308+
Password = null;
309+
}
310+
else
311+
{
312+
authenticationType = AuthenticationTypes.SqlServer;
313+
Username = UsernameTextBox.Text.Trim();
314+
Password = PasswordBox.Password;
315+
}
316+
235317
// Use server name as display name if not provided
236318
var displayName = string.IsNullOrWhiteSpace(DisplayNameTextBox.Text)
237319
? ServerNameTextBox.Text.Trim()
@@ -241,7 +323,7 @@ private async void Save_Click(object sender, RoutedEventArgs e)
241323
{
242324
ServerConnection.DisplayName = displayName;
243325
ServerConnection.ServerName = ServerNameTextBox.Text.Trim();
244-
ServerConnection.UseWindowsAuth = WindowsAuthRadio.IsChecked == true;
326+
ServerConnection.AuthenticationType = authenticationType;
245327
ServerConnection.Description = DescriptionTextBox.Text.Trim();
246328
ServerConnection.IsFavorite = IsFavoriteCheckBox.IsChecked == true;
247329
ServerConnection.EncryptMode = GetSelectedEncryptMode();
@@ -253,7 +335,7 @@ private async void Save_Click(object sender, RoutedEventArgs e)
253335
{
254336
DisplayName = displayName,
255337
ServerName = ServerNameTextBox.Text.Trim(),
256-
UseWindowsAuth = WindowsAuthRadio.IsChecked == true,
338+
AuthenticationType = authenticationType,
257339
Description = DescriptionTextBox.Text.Trim(),
258340
IsFavorite = IsFavoriteCheckBox.IsChecked == true,
259341
CreatedDate = DateTime.Now,
@@ -263,12 +345,6 @@ private async void Save_Click(object sender, RoutedEventArgs e)
263345
};
264346
}
265347

266-
if (SqlAuthRadio.IsChecked == true)
267-
{
268-
Username = UsernameTextBox.Text.Trim();
269-
Password = PasswordBox.Password;
270-
}
271-
272348
DialogResult = true;
273349
Close();
274350
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2026 Erik Darling, Darling Data LLC
3+
*
4+
* This file is part of the SQL Server Performance Monitor.
5+
*
6+
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
7+
*/
8+
9+
using System;
10+
11+
namespace PerformanceMonitorDashboard.Helpers
12+
{
13+
/// <summary>
14+
/// Helper utilities for Microsoft Entra MFA authentication.
15+
/// </summary>
16+
public static class MfaAuthenticationHelper
17+
{
18+
/// <summary>
19+
/// Checks if an exception indicates that the user cancelled MFA authentication.
20+
/// </summary>
21+
/// <param name="ex">The exception to check.</param>
22+
/// <returns>True if the exception represents user cancellation, false otherwise.</returns>
23+
public static bool IsMfaCancelledException(Exception ex)
24+
{
25+
var message = ex.Message?.ToLowerInvariant() ?? string.Empty;
26+
27+
// Only treat explicit user cancellation messages as cancellation
28+
// Do NOT treat authentication errors (wrong password, account selection, etc.) as cancellation
29+
return message.Contains("user canceled") ||
30+
message.Contains("user cancelled") ||
31+
message.Contains("authentication was cancelled") ||
32+
message.Contains("authentication was canceled");
33+
}
34+
}
35+
}

Dashboard/Interfaces/IServerManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public interface IServerManager
5555
/// <summary>
5656
/// Tests connectivity to a single server and updates its status.
5757
/// </summary>
58-
Task<ServerConnectionStatus> CheckConnectionAsync(string serverId);
58+
Task<ServerConnectionStatus> CheckConnectionAsync(string serverId, bool allowInteractiveAuth = false);
5959

6060
/// <summary>
6161
/// Tests connectivity to all servers and updates their statuses.

Dashboard/MainWindow.xaml.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ private void AddServer_Click(object sender, RoutedEventArgs e)
711711

712712
MessageBox.Show(
713713
$"Server '{server.DisplayName}' added successfully!\n\n" +
714-
(server.UseWindowsAuth ? "Using Windows Authentication" : "Credentials saved securely to Windows Credential Manager"),
714+
(server.AuthenticationType == Models.AuthenticationTypes.Windows ? "Using Windows Authentication" : $"Using {server.AuthenticationDisplay} — credentials saved securely to Windows Credential Manager"),
715715
"Server Added",
716716
MessageBoxButton.OK,
717717
MessageBoxImage.Information
@@ -757,7 +757,7 @@ private void EditServer_Click(object sender, RoutedEventArgs e)
757757

758758
MessageBox.Show(
759759
$"Server '{updatedServer.DisplayName}' updated successfully!\n\n" +
760-
(updatedServer.UseWindowsAuth ? "Using Windows Authentication" : "Credentials updated securely in Windows Credential Manager"),
760+
(updatedServer.AuthenticationType == Models.AuthenticationTypes.Windows ? "Using Windows Authentication" : $"Using {updatedServer.AuthenticationDisplay} — credentials updated securely in Windows Credential Manager"),
761761
"Server Updated",
762762
MessageBoxButton.OK,
763763
MessageBoxImage.Information
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2026 Erik Darling, Darling Data LLC
3+
*
4+
* This file is part of the SQL Server Performance Monitor.
5+
*
6+
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
7+
*/
8+
9+
namespace PerformanceMonitorDashboard.Models
10+
{
11+
/// <summary>
12+
/// Constants for server authentication types.
13+
/// </summary>
14+
public static class AuthenticationTypes
15+
{
16+
/// <summary>
17+
/// Windows integrated authentication.
18+
/// </summary>
19+
public const string Windows = "Windows";
20+
21+
/// <summary>
22+
/// SQL Server username/password authentication.
23+
/// </summary>
24+
public const string SqlServer = "SqlServer";
25+
26+
/// <summary>
27+
/// Microsoft Entra MFA (Azure AD) interactive authentication.
28+
/// </summary>
29+
public const string EntraMFA = "EntraMFA";
30+
}
31+
}

0 commit comments

Comments
 (0)