How to check the source of PasswordChanged event in WPF PasswordBox?

59 Views Asked by At

I'm using a passwordbox and need to do some operation every time the password is changed from the UI. This passwordbox is part of a Page in WPF window, and when the page is changed/unloaded, the passwordchanged event seems to get fired with empty value for password. Is there any way for us to recognize that the source of this event is page unload and not the user changing the password in the UI?

Edit - Adding the sample code to reproduce the case. There is a MainWindow which contains a Frame element to render Pages. There are 2 sample pages Page1.xaml and Page2.xaml. Page1 has a passwordbox and textbox, when its values are changed the texts in the mainWindow changes and displays the new values. When the page is changed then also the password change event is triggered with "" value but the textchanged event is not triggered. Can I prevent pwdChanged event also from getting triggered here?

<Window x:Class="WpfPasswwordTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfPasswwordTest"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
    </Grid.RowDefinitions>
    <Label Grid.Row="0" Name="PasswordTextLabel" HorizontalContentAlignment="Center"></Label>
    <Label Grid.Row="1" Name="TextboxLabel" HorizontalContentAlignment="Center"></Label>
    <Button Grid.Row="2" Name="PageChangeButton" Height="50" Width="200" Content="Change page"/>
    <Frame Name="PageFrame"  Grid.Row="3" NavigationUIVisibility="Hidden">
    </Frame>
</Grid>
    public partial class MainWindow : Window
{
    public Label pwdLabel, txtboxLabel;
    private bool page1 = true;
    public MainWindow()
    {
        InitializeComponent();
        pwdLabel = PasswordTextLabel;
        txtboxLabel = TextboxLabel;
        Class1.Instance.mainWin = this;
        PasswordTextLabel.Content = "Pwd not changed yet";
        TextboxLabel.Content = "Textbox text not changed yet";
        PageFrame.Source = new Uri("Page1.xaml", UriKind.Relative);
        PageChangeButton.Click += pageChange;
    }
    private void pageChange(object s, EventArgs e)
    {
        if(page1)
            PageFrame.Source = new Uri("Page2.xaml", UriKind.Relative);
        else
            PageFrame.Source = new Uri("Page1.xaml", UriKind.Relative);
        page1 = !page1;
    }
}
<Page x:Class="WpfPasswwordTest.Page1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
  xmlns:local="clr-namespace:WpfPasswwordTest"
  mc:Ignorable="d" 
  d:DesignHeight="450" d:DesignWidth="800"
  Title="Page1">

<Grid Height="auto" Width="500">
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
    </Grid.RowDefinitions>
    <Label Grid.Row="0"> This is page 1</Label>
    <Label Name="SampleLabel" Grid.Row="1">Password box - </Label>
    <PasswordBox Name="SamplePwdBox" Grid.Row="2"></PasswordBox>
    <Label Name="SampleTextLabel" Grid.Row="3">Text box -</Label>
    <TextBox Name="SampleTxtBox" Grid.Row="4"></TextBox>
</Grid>
public partial class Page1 : Page
{
    public Page1()
    {
        InitializeComponent();
        SamplePwdBox.PasswordChanged += pwdChanged;
        SampleTxtBox.TextChanged += txtChanged;
    }
    private void pwdChanged(object s,EventArgs e)
    {
        Class1.Instance.mainWin.pwdLabel.Content = "pwd Changed to " + (s as PasswordBox)?.Password;
    }
    private void txtChanged(object s, EventArgs e)
    {
        Class1.Instance.mainWin.txtboxLabel.Content = "text Changed to " + (s as TextBox)?.Text;
    }
}
<Page x:Class="WpfPasswwordTest.Page2"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
  xmlns:local="clr-namespace:WpfPasswwordTest"
  mc:Ignorable="d" 
  d:DesignHeight="450" d:DesignWidth="800"
  Title="Page2">

<Grid Height="100" Width="500">
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Label Grid.Row="0">This is page 2</Label>
    <Label Name="SampleLabel" Grid.Row="1">Nothing to show here</Label>
</Grid>
1

There are 1 best solutions below

0
BionicCode On BEST ANSWER

The observed behavior is normal as the PasswordBox clears itself when being unloaded.

Some thoughts:

  • Microsoft explicitly recommends avoiding the PasswordBox as it encourages weak security management. Although SecureString is safer than a plain string it's far from being safe. The recommended solution is to use Windows authentication or a 3rd party OAuth API. For most scenarios, you don't have to care about authentication because the user is already authenticated by the OS when he logged in to his Windows account.
  • Don't allow the user to change screens unless he is fully authenticated
  • If you allow users to change screens then actively clear the password before leaving. Don't leave passwords in memory. If you adapt to this rule, you automatically fix your issue.
  • Handling PassworddBox.PasswordChanged event is generally pointless because you don't want to trigger the valdation system to validate each character input. Instead, you are interested in the final value. For this reason, you should handle the password when the user clicks the e.g. "Login" button or when the PasswordBox has lost focus (what suits your UI flow better). The common pattern is to have the user explicitly trigger the authentication by clicking a corresponding button (signaling that he is done with the input). This will fix your issues.

Suggested solution

IPasswordSource.cs

public interface IPasswordSource
{
  SecureString GetPassword();
  void ClearPassword();
  event EventHandler PasswordChanged;
}

MainWindow.xaml

<Window x:Class="WpfPasswwordTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfPasswwordTest"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
  <StackPanel>
    <Button Content="Change page"
            Click="OnChangePageButtonClicked" />
    <Frame Name="PageFrame" 
           Source="Page1.xaml"
           Navigated="OnNavigated"
           Loaded="OnFrameLoaded"
           NavigationUIVisibility="Hidden">
    </Frame>
</Grid>
</Window>

MainWindow.xaml.cs
Clear the password before navigating away. Alternatively block the navigation button until the user is authenticated.

public partial class MainWindow : Window
{
  private NavigationService PageNavigationService { get; set; }

  public MainWindow()
    => InitializeComponent();

  private void OnFrameLoaded(object sender, RoutedEventArgs e)
  {
    var frame = (Frame)sender;
    this.PageNavigationService = frame.NavigationService;
  }

  private void OnNavigated(object sender, NavigationEventArgs e)
  {
    var frame = (Frame)sender;
    if (frame.Content is IPasswordSource passwordSource)
    {
      passwordSource.PasswordChanged += OnPasswordChanged;
    }
  }

  private void OnPasswordChanged(object? sender, EventArgs e)
  {
    var passwordSource = (IPasswordSource)sender;
    var password = passwordSource.GetPassword();

    // TODO::Authenticate user by passing the password 
    // to the authentication component/service.
  }

  private void OnChangePageButtonClicked(object sender, EventArgs e)
  {
    // Before navigating away, we have to clear the password
    // to protect user data
    if (this.PageNavigationService.Content is IPasswordSource passwordSource)
    {
      passwordSource.ClearPassword();
      passwordSource.PasswordChanged -= OnPasswordChanged;
    }

    Uri nextPageUri;
    if (this.PageFrame.Content is Page1)
    {
      nextPageUri = new Uri("Page2.xaml", UriKind.Relative);
    }
    else
    {
      nextPageUri = new Uri("Page1.xaml", UriKind.Relative);
    }

    this.PageNavigationService.Navigate(nextPageUri);
  }
}

Page1.xaml.cs

public partial class Page1 : Page, IPasswordSource
{
  public event EventHandler PasswordChanged;

  public Page1() 
    => InitializeComponent();

  // One time get method that clears the PasswordBox
  // after extracting the SecureString password.
  public SecureString GetPassword()
  {
    SecureString password = this.SamplePwdBox.SecurePassword;
    ClearPassword();

    return password;
  }

  public void ClearPassword() 
    => this.SamplePwdBox.Clear();

  private void OnSendPasswordButtonClicked(object sender, RoutedEventArgs e)
    => OnPasswordChanged();

  protected virtual void OnPasswordChanged()
    => this.PasswordChanged?.Invoke(this, EventArgs.Empty);
}

Page1.xaml

<Page>
  <Grid Height="auto" Width="500">
    <Grid.RowDefinitions>
      <RowDefinition Height="auto"/>
      <RowDefinition Height="auto"/>
      <RowDefinition Height="auto"/>
      <RowDefinition Height="auto"/>
      <RowDefinition Height="auto"/>
    </Grid.RowDefinitions>

  <TextBlock Grid.Row="0" Text="This is page 1" />
  <TextBlock Grid.Row="1" Text="Password box -" />
  <StackPanel>
    <PasswordBox Grid.Row="2" Name="SamplePwdBox" />

    <!-- Explicitly trigger the authentication -->
    <Button Content="Login" Click="OnSendPasswordButtonClicked" />
  </StackPanel>
 </Grid>
</UserControl>