Implement a Custom Security System User Based on an Existing Business Class
- 13 minutes to read
Consider the following situation. You have an unsecure XAF application, and its business model includes the Employee business class. This class exposes information such as personal data, the associated department, and assigned tasks. When enabling the Security System, a User object is added to the business model, but the Users who log in to your application are Employees. This topic explains how to merge the User and Employee into a single entity. For this purpose, several security-related interfaces in the Employee class will be supported, and as a result, the Security System will recognize the Employee type as one of the possible User types. You will assign the Employee type to the SecurityStrategy.UserType property in the Application Designer. As an additional benefit, it will be possible to use the CurrentUserId() Function Criteria Operator to get the identifier of the current Employee (for example, to define a “tasks assigned to me” List View filter).
Tip
A complete sample project is available in the DevExpress Code Examples database at https://supportcenter.devexpress.com/ticket/details/e4160/xaf-how-to-implement-a-security-system-user-based-on-an-existing-business-class.
Note
- A similar example for Entity Framework Core is available at https://github.com/DevExpress-Examples/xaf-how-to-implement-a-security-system-user-based-on-an-existing-business-class.
- As an alternative to the technique described in this topic, you can inherit the Employee class from PermissionPolicyUser. To see an example, refer to the following topic: How to: Implement Custom Security Objects (Users, Roles, Operation Permissions).
Initial Business Model
Start with a new XAF solution. Add the following Employee and EmployeeTask business classes to the module project.
[DefaultClassOptions]
public class Employee : Person {
public virtual IList<EmployeeTask> OwnTasks { get; set; } = new ObservableCollection<EmployeeTask>();
}
public class Person : BaseObject {
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
}
[DefaultClassOptions,ImageName("BO_Task")]
public class EmployeeTask : BaseObject {
public virtual string Subject { get; set; }
public virtual Employee Owner { get; set; }
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
Support the ISecurityUser Interface
Add a reference to the DevExpress.ExpressApp.Security.v24.1.dll assembly for the project that contains the Employee class. Extend the Employee class with the following code:
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Validation;
// ...
public class Employee : Person, ISecurityUser {
// ...
#region ISecurityUser Members
public virtual bool IsActive { get; set; } = true;
[RuleRequiredField("EmployeeUserNameRequired", DefaultContexts.Save)]
[RuleUniqueValue("EmployeeUserNameIsUnique", DefaultContexts.Save,
"The login with the entered user name was already registered within the system.")]
public virtual string UserName { get; set; }
#endregion
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
Refer to the ISecurityUser interface description for details on this interface and its members.
Support the IAuthenticationStandardUser Interface
Note
If you are not planning to use the AuthenticationStandard authentication type, skip this section.
Extend the Employee class with the following code:
using System.ComponentModel;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
// ...
public class Employee : Person, ISecurityUser, IAuthenticationStandardUser {
// ...
#region IAuthenticationStandardUser Members
public virtual bool ChangePasswordOnFirstLogon { get; set; }
[Browsable(false), FieldSize(FieldSizeAttribute.Unlimited), SecurityBrowsable]
public virtual string StoredPassword { get; set; }
public bool ComparePassword(string password) {
return PasswordCryptographer.VerifyHashedPasswordDelegate(this.StoredPassword, password);
}
public void SetPassword(string password) {
this.StoredPassword = PasswordCryptographer.HashPasswordDelegate(password);
}
#endregion
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
Refer to the IAuthenticationStandardUser interface description for details on this interface and its members.
Support the IAuthenticationActiveDirectoryUser Interface
Note
If you are not planning to use the AuthenticationActiveDirectory authentication type, skip this section.
Add the IAuthenticationActiveDirectoryUser interface to the supported interfaces list of the Employee class.
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
// ...
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser {
// ...
}
The IAuthenticationActiveDirectoryUser.UserName property declared by this interface has already been implemented in your code as a part of the ISecurityUser interface.
Support the ISecurityUserWithRoles Interface
Extend the Employee class with the following code:
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
using DevExpress.Persistent.Validation;
using System.Collections.ObjectModel;
//...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser, ISecurityUserWithRoles {
// ...
#region ISecurityUserWithRoles Members
IList<ISecurityRole> ISecurityUserWithRoles.Roles {
get {
IList<ISecurityRole> result = new List<ISecurityRole>();
foreach (EmployeeRole role in EmployeeRoles) {
result.Add(role);
}
return result;
}
}
[RuleRequiredField("EmployeeRoleIsRequired", DefaultContexts.Save,
TargetCriteria = "IsActive",
CustomMessageTemplate = "An active employee must have at least one role assigned")]
public virtual IList<EmployeeRole> EmployeeRoles { get; set; } = new ObservableCollection<EmployeeRole>();
#endregion
}
Refer to the ISecurityUserWithRoles interface description for details on this interface and its members.
A many-to-many association with the built-in PermissionPolicyRole class cannot be defined (this class is already associated with the PermissionPolicyUser), which is why the following custom Role class should be implemented in the module project.
using System.Linq;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.PermissionPolicy;
using System.Collections.ObjectModel;
// ...
[ImageName("BO_Role")]
public class EmployeeRole : PermissionPolicyRoleBase, IPermissionPolicyRoleWithUsers {
public virtual IList<Employee> Employees { get; set; } = new ObservableCollection<Employee>();
IEnumerable<IPermissionPolicyUser> IPermissionPolicyRoleWithUsers.Users {
get { return Employees.OfType<IPermissionPolicyUser>(); }
}
}
Support the IPermissionPolicyUser Interface
Extend the Employee class with the following code:
using System.Linq;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
// ...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser {
// ...
#region IPermissionPolicyUser Members
IEnumerable<IPermissionPolicyRole> IPermissionPolicyUser.Roles {
get { return EmployeeRoles.OfType<IPermissionPolicyRole>(); }
}
#endregion
}
Refer to the IPermissionPolicyUser interface description for details on this interface and its members.
Support the ICanInitialize Interface
The ICanInitialize.Initialize method is used to assign the default role when you use the AuthenticationActiveDirectory authentication and set the AuthenticationActiveDirectory.CreateUserAutomatically property to true
. If you do not need to support user autocreation, skip this step. Otherwise, extend the Employee class with the following code:
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Data.Filtering;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
// ...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser, ICanInitialize {
// ...
#region ICanInitialize Members
void ICanInitialize.Initialize(IObjectSpace objectSpace, SecurityStrategyComplex security) {
EmployeeRole newUserRole = (EmployeeRole)objectSpace.FirstOrDefault<EmployeeRole>(role => role.Name == security.NewUserRoleName);
if (newUserRole == null) {
newUserRole = objectSpace.CreateObject<EmployeeRole>();
newUserRole.Name = security.NewUserRoleName;
newUserRole.IsAdministrative = true;
newUserRole.Employees.Add(this);
}
}
#endregion
}
Support the ISecurityUserWithLoginInfo Interface
In applications that support multiple authentication schemes, a user type must implement the ISecurityUserWithLoginInfo interface so that a user account record can store login information for all available schemes. Implement this interface as follows:
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Data.Filtering;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Base.Security;
using System.ComponentModel.DataAnnotations;
// ...
[DefaultClassOptions]
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser, ICanInitialize, ISecurityUserWithLoginInfo {
// ...
#region ISecurityUserWithLoginInfo Members
public Employee() : base() {
// ...
EmployeeLogins = new ObservableCollection<EmployeeLoginInfo>();
}
[Browsable(false)]
[DevExpress.ExpressApp.DC.Aggregated]
public virtual IList<EmployeeLoginInfo> EmployeeLogins { get; set; }
IEnumerable<ISecurityUserLoginInfo> IOAuthSecurityUser.UserLogins => EmployeeLogins.OfType<ISecurityUserLoginInfo>();
ISecurityUserLoginInfo ISecurityUserWithLoginInfo.CreateUserLoginInfo(string loginProviderName, string providerUserKey) {
EmployeeLoginInfo result = ((IObjectSpaceLink)this).ObjectSpace.CreateObject<EmployeeLoginInfo>();
result.LoginProviderName = loginProviderName;
result.ProviderUserKey = providerUserKey;
result.User = this;
return result;
}
#endregion
}
public class EmployeeLoginInfo : ISecurityUserLoginInfo {
public EmployeeLoginInfo() { }
[Browsable(false)]
public virtual Guid ID { get; protected set; }
[Appearance("PasswordProvider", Enabled = false, Criteria = "!(IsNewObject(this)) and LoginProviderName == '" + SecurityDefaults.PasswordAuthentication + "'", Context = "DetailView")]
public virtual string LoginProviderName { get; set; }
[Appearance("PasswordProviderUserKey", Enabled = false, Criteria = "!(IsNewObject(this)) and LoginProviderName == '" + SecurityDefaults.PasswordAuthentication + "'", Context = "DetailView")]
public virtual string ProviderUserKey { get; set; }
[Browsable(false)]
public virtual Guid UserForeignKey { get; set; }
[Required]
[ForeignKey(nameof(UserForeignKey))]
public virtual Employee User { get; set; }
object ISecurityUserLoginInfo.User => User;
}
Support the ISecurityUserLockout Interface (.NET 6+)
Implement the ISecurityUserLockout interface to support the user lockout feature (the ability to lock out users who fail to enter the correct password several times in a row).
using System.Collections.ObjectModel;
using System.ComponentModel;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
// ...
public class Employee : Person, ISecurityUser,
IAuthenticationStandardUser, IAuthenticationActiveDirectoryUser,
IPermissionPolicyUser, ICanInitialize, ISecurityUserWithLoginInfo, ISecurityUserLockout{
// ...
#region ISecurityUserLockout Members
[Browsable(false)]
public virtual int AccessFailedCount { get; set; }
[Browsable(false)]
public virtual DateTime LockoutEnd { get; set; }
#endregion
}
Note
.NET Framework applications do not support user lockout.
Configure the Security System in Order to Utilize Custom User, Role, and Login Info Types
To use the custom EmployeeRole, custom Employee user, and custom EmployeeLoginInfo instead of the default types, modify the SecurityStrategyComplex.RoleType and SecurityStrategy.UserType values, as shown below.
In an ASP.NET Core Blazor application
File: MyApplication.Blazor\Startup.cs
public class Startup {
// ...
public void ConfigureServices(IServiceCollection services) {
// ...
services.AddXaf(Configuration, builder => {
builder.Security
.UseIntegratedMode(options => {
// ...
options.RoleType = typeof(EmployeeRole);
options.UserType = typeof(Employee);
options.UserLoginInfoType = typeof(EmployeeLoginInfo);
})
// ...
})
// ...
}
}
In WinForms Applications
File: MyApplication.Win\Startup.cs
public class ApplicationBuilder : IDesignTimeApplicationFactory {
// ...
public static WinApplication BuildApplication(string connectionString) {
var builder = WinApplication.CreateBuilder();
// ..
builder.Security
.UseIntegratedMode(options => {
options.RoleType = typeof(EmployeeRole);
options.UserType = typeof(Employee);
options.UserLoginInfoType = typeof(EmployeeLoginInfo);
// ...
})
// ..
};
}
In ASP.NET Web Forms Applications
File: MyApplication.Web\WebApplication.cs
partial class MainDemoWindowsFormsApplication {
// ...
private void InitializeComponent() {
// ...
this.securityStrategyComplex1.Authentication = this.authenticationStandard1;
this.securityStrategyComplex1.RoleType = typeof(EmployeeRole);
this.securityStrategyComplex1.UserType = typeof(Employee);
this.securityStrategyComplex1.UserLoginInfoType = typeof(EmployeeLoginInfo);
// ...
}
}
In .NET Framework projects, you can alternatively invoke the Application Designer, and drag the SecurityStrategyComplex component from the Toolbox to the Security pane. Then, place the AuthenticationStandard or AuthenticationActiveDirectory component near the SecurityStrategyComplex. To use the custom EmployeeRole role and custom Employee user instead of the default role and user, modify the SecurityStrategyComplex.RoleType and SecurityStrategy.UserType values in the Properties window. This step should be executed in WinForms and ASP.NET Web Forms applications.
Create an Administrative Account
If you decide to utilize Active Directory authentication, you can skip this section. The minimum requirements for starting with Standard Authentication is an Administrator role and the Administrator user associated with this role. To add these objects, edit the Updater.cs(Updater.vb) file located in the DatabaseUpdate folder of your module project. Override the ModuleUpdater.UpdateDatabaseAfterUpdateSchema method in the following manner.
using DevExpress.ExpressApp.Security.Strategy;
// ...
public override void UpdateDatabaseAfterUpdateSchema() {
base.UpdateDatabaseAfterUpdateSchema();
EmployeeRole adminEmployeeRole = ObjectSpace.FirstOrDefault<EmployeeRole>(role => role.Name == SecurityStrategy.AdministratorRoleName);
if (adminEmployeeRole == null) {
adminEmployeeRole = ObjectSpace.CreateObject<EmployeeRole>();
adminEmployeeRole.Name = SecurityStrategy.AdministratorRoleName;
adminEmployeeRole.IsAdministrative = true;
}
Employee adminEmployee = ObjectSpace.FirstOrDefault<Employee>(employee => employee.UserName == "Administrator");
if (adminEmployee == null) {
adminEmployee = ObjectSpace.CreateObject<Employee>();
adminEmployee.UserName = "Administrator";
adminEmployee.SetPassword("");
adminEmployee.EmployeeRoles.Add(adminEmployeeRole);
((ISecurityUserWithLoginInfo)adminEmployee).CreateUserLoginInfo(SecurityDefaults.PasswordAuthentication, ObjectSpace.GetKeyValueAsString(adminEmployee));
}
ObjectSpace.CommitChanges();
}
Enable User Lockout (.NET 6+)
In the application builder code, set the LockoutOptions.Enabled property to true
.
File: MySolution.Blazor.Server\Startup.cs, MySolution.Win\Startup.cs
//...
using DevExpress.ExpressApp.Security;
namespace YourApplicationName.Blazor.Server;
public class Startup {
// ...
public void ConfigureServices(IServiceCollection services) {
services.AddXaf(Configuration, builder => {
//...
builder.Security
.UseIntegratedMode(options => {
options.Lockout.Enabled = true;
})
});
}
}
Run the Application
You can now run the application to see the result. You will see that the Employee objects are utilized as a custom user type.
Filter Tasks that Belong to the Current Employee
Apply the ListViewFilterAttribute attributes to the EmployeeTask class to define List View filters. To refer to the current Employee identifier, use the CurrentUserId() function.
using DevExpress.ExpressApp.SystemModule;
// ...
[ListViewFilter("All Tasks", "")]
[ListViewFilter("My Tasks", "[Owner.Id] = CurrentUserId()")]
public class EmployeeTask : Task {
// ...
}
The image below shows the result.