Custom authorization in wpf | magnus montin

Custom authorization in wpf

This post provides a code sample on how to implement your own custom authentication and authorization in a WPF application by implementing classes that derive from the IIdentity and IPrincipal interfaces and overriding the application thread’s default identity.

It is very common for business applications to provide access to data or resources based on the credentials supplied by the user and these kinds of applications typically check the role of a user and provide access to various resources based on that role. The .NET Framework uses the System.Security.Principal.IIdentity and System.Security.Principal .IPrincipal interfaces as the basis for authentication and authorization and by implementing these fairly simple interfaces you can apply your own custom authentication in your applications.

The sample code in this post uses the MVVM design pattern and the solution consists of a simple window with basic login and logout functionality and some buttons to display windows protected with the PrincipalPermissionAttribute, a simple authentication service class to authenticate users based on a supplied username and a password, the actual implementation of the interfaces and some additional helper classes and interfaces.

To get started, create a new WPF application in Visual Studio (File->New->Project), remove the automatically generated MainWindow.xaml, add a new class (Project->Add class) called CustomIdentity and implement it as below.

using System.Security.Principal;

namespace WpfApplication
{
  public class CustomIdentity : IIdentity
  {
      public CustomIdentity(string name, string email, string[] roles)
      {
          Name = name;
          Email = email;
          Roles = roles;
      }

      public string Name { get; private set; }
      public string Email { get; private set; }
      public string[] Roles { get; private set; }

      #region IIdentity Members
      public string AuthenticationType { get { return "Custom authentication"; } }

      public bool IsAuthenticated { get { return !string.IsNullOrEmpty(Name); } }
    #endregion
  }
}

The IIdentity interface encapsulates a user’s identity and the custom implementation above exposes three properties – Name, Email and Roles – to be passed to the constructor when an instance is created. Note that the implementation of the IIdentity.IsAuthenticated property means that a user is considered to be authenticated once the name property has been set.

Похожее:  МОСКВА ВХОД В ЛИЧНЫЙ КАБИНЕТ И РЕГИСТРАЦИЯ НА САЙТЕ

Next, we add an additional class called AnonymousIdentity that extends CustomIdentity to represent an unauthenticated user, i.e. a user with an empty name.

namespace WpfApplication
{
    public class AnonymousIdentity : CustomIdentity
    {
        public AnonymousIdentity()
            : base(string.Empty, string.Empty, new string[] { })
        { }
    }
}

Once we have the CustomIdentity class we need to implement a class that derives from IPrincipal why we add a new class called CustomPrincipal to the application.

using System.Linq;
using System.Security.Principal;

namespace WpfApplication
{
  public class CustomPrincipal : IPrincipal
  {
    private CustomIdentity _identity;

    public CustomIdentity Identity
    {
        get { return _identity ?? new AnonymousIdentity(); }
        set { _identity = value; }
    }
    
    #region IPrincipal Members
    IIdentity IPrincipal.Identity
    {
        get { return this.Identity; } 
    }

    public bool IsInRole(string role)
    {
        return _identity.Roles.Contains(role);
    }
    #endregion
  }
}

A principal has an identity associated with it and returns instances of this through the IPrincipal.Identity property. In the custom implementation above we provide our own Identity property to be able to set the principal’s identity to an instance of our CustomIdentity class. Note that until the property has been set, i.e. as long as the private member variable _identity is NULL, it will return an anonymous (unauthenticated) identity.

To be able to authenticate users we then add a simple AuthentationService class, along with an interface and a type for the return data, to validate credentials supplied by users. In a real world scenario you would probably store the credentials and any additional information associated with a user in a SQL Server database or some other persistent storage but in the demo sample implementation below the values are stored in a static list inside the class.

using System.Linq;
using System.Text;
using System.Security.Cryptography;

namespace WpfApplication
{
    public interface IAuthenticationService
    {
        User AuthenticateUser(string username, string password);
    }

    public class AuthenticationService : IAuthenticationService
    {
        private class InternalUserData
        {
            public InternalUserData(string username, string email, string hashedPassword, string[] roles)
            {
                Username = username;
                Email = email;
                HashedPassword = hashedPassword;
                Roles = roles;
            }
            public string Username
            {
                get;
                private set;
            }

            public string Email
            {
                get;
                private set;
            }

            public string HashedPassword
            {
                get;
                private set;
            }

            public string[] Roles
            {
                get;
                private set;
            }
        }

        private static readonly List<InternalUserData> _users = new List<InternalUserData>() 
        { 
            new InternalUserData("Mark", "[email protected]", 
            "MB5PYIsbI2YzCUe34Q5ZU2VferIoI4Ttd ydolWV0OE=", new string[] { "Administrators" }), 
            new InternalUserData("John", "[email protected]", 
            "hMaLizwzOQ5LeOnMuj C6W75Zl5CXXYbwDSHWW9ZOXc=", new string[] { })
        };

        public User AuthenticateUser(string username, string clearTextPassword)
        {
            InternalUserData userData = _users.FirstOrDefault(u => u.Username.Equals(username) 
                && u.HashedPassword.Equals(CalculateHash(clearTextPassword, u.Username)));
            if (userData == null)
                throw new UnauthorizedAccessException("Access denied. Please provide some valid credentials.");

            return new User(userData.Username, userData.Email, userData.Roles);
        }

        private string CalculateHash(string clearTextPassword, string salt)
        {
            // Convert the salted password to a byte array
            byte[] saltedHashBytes = Encoding.UTF8.GetBytes(clearTextPassword   salt);
            // Use the hash algorithm to calculate the hash
            HashAlgorithm algorithm = new SHA256Managed();
            byte[] hash = algorithm.ComputeHash(saltedHashBytes);
            // Return the hash as a base64 encoded string to be compared to the stored password
            return Convert.ToBase64String(hash);
        }
    }

    public class User
    {
        public User(string username, string email, string[] roles)
        {
            Username = username;
            Email = email;
            Roles = roles;
        }
        public string Username
        {
            get;
            set;
        }

        public string Email
        {
            get;
            set;
        }

        public string[] Roles
        {
            get;
            set;
        }
    }
}

As it’s considered a bad practice to store passwords in clear text for security reasons, each instance of the InternalUserData helper class contains a one-way hashed and salted password with both users in the sample code having a valid password identical to their username, e.g. Mark’s password is “Mark” and John’s is “John”. The private helper method “CalulcateHash” is used to hash the user supplied password before it’s compared to the one stored in the private list.

The next step is to implement the viewmodel to expose the authentication service to the yet to be implemented login window. We add a new class called AuthenticationViewModel and implement it as below.

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Controls;
using System.Security;

namespace WpfApplication
{
  public interface IViewModel { }

  public class AuthenticationViewModel : IViewModel, INotifyPropertyChanged
  {
    private readonly IAuthenticationService _authenticationService;
    private readonly DelegateCommand _loginCommand;
    private readonly DelegateCommand _logoutCommand;
    private readonly DelegateCommand _showViewCommand;
    private string _username;
    private string _status;

    public AuthenticationViewModel(IAuthenticationService authenticationService)
    {
        _authenticationService = authenticationService;
        _loginCommand = new DelegateCommand(Login, CanLogin);
        _logoutCommand = new DelegateCommand(Logout, CanLogout);
        _showViewCommand = new DelegateCommand(ShowView, null);
    }

    #region Properties
    public string Username
    {
        get { return _username;}
        set { _username = value; NotifyPropertyChanged("Username"); }
    }

    public string AuthenticatedUser {
        get
        {
            if (IsAuthenticated)
                return string.Format("Signed in as {0}. {1}",
                      Thread.CurrentPrincipal.Identity.Name,
                      Thread.CurrentPrincipal.IsInRole("Administrators") ? "You are an administrator!"
                          : "You are NOT a member of the administrators group.");

            return "Not authenticated!";
        }
    }

    public string Status
    {
        get { return _status; }
        set { _status = value; NotifyPropertyChanged("Status"); }
    }
    #endregion

    #region Commands
    public DelegateCommand LoginCommand { get { return _loginCommand; } }

    public DelegateCommand LogoutCommand { get { return _logoutCommand; } }

    public DelegateCommand ShowViewCommand { get { return _showViewCommand; } }
    #endregion

    private void Login(object parameter)
    {
        PasswordBox passwordBox = parameter as PasswordBox;
        string clearTextPassword = passwordBox.Password;
        try
        {
            //Validate credentials through the authentication service
            User user = _authenticationService.AuthenticateUser(Username, clearTextPassword);

            //Get the current principal object
            CustomPrincipal customPrincipal = Thread.CurrentPrincipal as CustomPrincipal;
            if (customPrincipal == null)
                throw new ArgumentException("The application's default thread principal must be set to a CustomPrincipal object on startup.");

            //Authenticate the user
            customPrincipal.Identity = new CustomIdentity(user.Username, user.Email, user.Roles);

            //Update UI
            NotifyPropertyChanged("AuthenticatedUser");
            NotifyPropertyChanged("IsAuthenticated");
            _loginCommand.RaiseCanExecuteChanged();
            _logoutCommand.RaiseCanExecuteChanged();
            Username = string.Empty; //reset
            passwordBox.Password = string.Empty; //reset
            Status = string.Empty;
        }
        catch (UnauthorizedAccessException)
        {
            Status = "Login failed! Please provide some valid credentials.";
        }
        catch (Exception ex)
        {
            Status = string.Format("ERROR: {0}", ex.Message);
        }
    }

    private bool CanLogin(object parameter)
    {
        return !IsAuthenticated;
    }

    private void Logout(object parameter) {
      CustomPrincipal customPrincipal = Thread.CurrentPrincipal as CustomPrincipal;
      if (customPrincipal != null)
      {
          customPrincipal.Identity = new AnonymousIdentity();
          NotifyPropertyChanged("AuthenticatedUser");
          NotifyPropertyChanged("IsAuthenticated");
          _loginCommand.RaiseCanExecuteChanged();
          _logoutCommand.RaiseCanExecuteChanged();
          Status = string.Empty;
      }
    }

    private bool CanLogout(object parameter)
    {
        return IsAuthenticated;
    }

    public bool IsAuthenticated
    {
        get { return Thread.CurrentPrincipal.Identity.IsAuthenticated; }
    }

    private void ShowView(object parameter) {
        try
        {
            Status = string.Empty;
            IView view;
            if (parameter == null)
                view = new SecretWindow();
            else
                view = new AdminWindow();

            view.Show();
        }
        catch (SecurityException)
        {
            Status = "You are not authorized!";
        }
    }


    #region INotifyPropertyChanged Members
    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(string propertyName) {
      if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
  }
}

When a user clicks a login button in the view (the window), a command on the viewmodel executes to perform the actual authentication by validating the supplied credentials against our authentication service and, in case of a successful validation, setting the Identity property of the CustomPrincipal instance associated with the currently executing thread to an instance of our CustomIdentity class. For this to work, we must configure our WPF application to use our CustomPrincipal . This is done once when the application starts by overriding the OnStartup method in App.xaml.cs and removing the StartupUri attribute from the XAML.

<Application x:Class="WpfApplication.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>
using System;
using System.Windows;

namespace WpfApplication
{
  /// <summary>
  /// Interaction logic for App.xaml
  /// </summary>
  public partial class App : Application
  {
    protected override void OnStartup(StartupEventArgs e) {
      
      //Create a custom principal with an anonymous identity at startup
      CustomPrincipal customPrincipal = new CustomPrincipal();
      AppDomain.CurrentDomain.SetThreadPrincipal(customPrincipal);
      
      base.OnStartup(e);

      //Show the login view
      AuthenticationViewModel viewModel = new AuthenticationViewModel(new AuthenticationService());
      IView loginWindow = new LoginWindow(viewModel);
      loginWindow.Show();

    }
  }
}

It’s important to note that you must only call AppDomain.CurrentDomain.SetThreadPrincipal once during your application’s lifetime. If you try to call the same method again any time during the execution of the application you will get a PolicyException saying “Default principal object cannot be set twice”. Because of this it is not an option to reset the thread’s principal once its default identity has been initially set.

The DelegateCommand type used for the commands in the viewmodel are a common implementation of System.Windows.Input.ICommand that simply invokes delegates when executing and querying executable status. It doesn’t come with WPF but you can easy implement your own like below or use the one provided by Prism, the framework and guidance for building WPF and Silverlight applications from the Microsoft Patterns and Practices Team.

using System;
using System.Windows.Input;
namespace WpfApplication
{
  public class DelegateCommand : ICommand
  {
    private readonly Predicate<object> _canExecute;
    private readonly Action<object> _execute;

    public event EventHandler CanExecuteChanged;

    public DelegateCommand(Action<object> execute)
      : this(execute, null) {
    }

    public DelegateCommand(Action<object> execute, Predicate<object> canExecute) {
      _execute = execute;
      _canExecute = canExecute;
    }

    public bool CanExecute(object parameter) {
      if (_canExecute == null)
        return true;

      return _canExecute(parameter);
    }

    public void Execute(object parameter) {
      _execute(parameter);
    }

    public void RaiseCanExecuteChanged() {
      if (CanExecuteChanged != null)
        CanExecuteChanged(this, EventArgs.Empty);
    }
  }
}

With this in place, now add a new window called LoginWindow.xaml (Project->Add window) to the application and implement the markup and code-behind as below.

<Window x:Class="WpfApplication.LoginWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LoginWindow" Height="300" Width="600">
  <Window.Resources>
    <BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter"/>
  </Window.Resources>
    <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <TextBlock Text="{Binding AuthenticatedUser}" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
               FontSize="14" HorizontalAlignment="Right" TextWrapping="Wrap" FontWeight="Bold"
               Margin="2,2,2,2"/>
    <TextBlock Text="Username:" Grid.Row="1" Grid.Column="0" />
    <TextBlock Text="Password:" Grid.Row="2" Grid.Column="0" />
    <TextBox Text="{Binding Username}" Grid.Row="1" Grid.Column="1" />
    <PasswordBox x:Name="passwordBox" Grid.Row="2" Grid.Column="1" />
    <StackPanel Orientation="Horizontal" Grid.Row="3" Grid.Column="1">
      <Button Content="Log in" Command="{Binding LoginCommand, Mode=OneTime}"
            CommandParameter="{Binding ElementName=passwordBox}"
            HorizontalAlignment="Center"/>
      <Button Content="Log out" Command="{Binding LogoutCommand, Mode=OneTime}"
            Visibility="{Binding IsAuthenticated, Converter={StaticResource booleanToVisibilityConverter}}"
            HorizontalAlignment="Center" Margin="2,0,0,0"/>
    </StackPanel>
    <TextBlock Text="{Binding Status}" Grid.Row="4" Grid.Column="1"
               HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="Red" TextWrapping="Wrap" />
    <StackPanel Grid.Row="5" Grid.Column="1" VerticalAlignment="Center">
      <Button Content="Show secret view" Command="{Binding ShowViewCommand}"
              HorizontalAlignment="Center" />
      <Button Content="Show admin view" Command="{Binding ShowViewCommand}" CommandParameter="Admin"
              HorizontalAlignment="Center" Margin="2,2,0,0" />
    </StackPanel>
  </Grid>
</Window>
using System.Windows;

namespace WpfApplication
{
    public interface IView
    {
        IViewModel ViewModel
        {
            get;
            set;
        }

        void Show();
    }

    /// <summary>
    /// Interaction logic for LoginWindow.xaml
    /// </summary>
    public partial class LoginWindow : Window, IView
    {
        public LoginWindow(AuthenticationViewModel viewModel)
        {
            ViewModel = viewModel;
            InitializeComponent();
        }

        #region IView Members
        public IViewModel ViewModel
        {
            get { return DataContext as IViewModel; }
            set { DataContext = value; }
        }
        #endregion
    }
}

The last step will be to add some protected views to able to verify that the authorization works as expected. The first one, called SecretWindow below, will be accessible by all authenticated users regardless of which group(s) they belong to, i.e. no roles are specified for the PrincipalPermissionAttribute, while the second one will by accessible only for members of the administrator group. Remember that the users and their respective group belongings are defined within the AuthenticationService.

using System.Windows;
using System.Security.Permissions;

namespace WpfApplication
{
  /// <summary>
  /// Interaction logic for SecretWindow.xaml
  /// </summary>
  [PrincipalPermission(SecurityAction.Demand)]
  public partial class SecretWindow : Window, IView
  {
    public SecretWindow() {
      InitializeComponent();
    }

    #region IView Members

    public IViewModel ViewModel {
      get {
        return DataContext as IViewModel;
      }
      set {
        DataContext = value;
      }
    }

    #endregion
  }
}
<Window x:Class="WpfApplication.SecretWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="SecretWindow" Height="300" Width="300">
    <Grid>
    <TextBlock Text="This window is only accessible for authenticated users..."/>
  </Grid>
</Window>
using System.Windows;
using System.Security.Permissions;

namespace WpfApplication
{
  /// <summary>
  /// Interaction logic for AdminWindow.xaml
  /// </summary>
  [PrincipalPermission(SecurityAction.Demand, Role = "Administrators")]
  public partial class AdminWindow : Window, IView
  {
    public AdminWindow() {
      InitializeComponent();
    }

    #region IView Members
    public IViewModel ViewModel {
      get {
        return DataContext as IViewModel;
      }
      set {
        DataContext = value;
      }
    }
    #endregion
  }
}
<Window x:Class="WpfApplication.AdminWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="AdminWindow" Height="300" Width="300">
    <Grid>
    <TextBlock Text="This window is only accessible for admninistrators..."/>
  </Grid>
</Window>

When compiling and running the sample code you will notice that none of the windows will be shown until the user has logged in at the top of the window and only when logged in as “Mark” you will be able to display the administrator window.


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *