Customize Standard Authentication Behavior and Supply Additional Logon Parameters (.NET Framework Applications)
- 11 minutes to read
When an XAF application uses the AuthenticationStandard authentication, the default login form displays User Name and Password editors. This topic explains how to customize this form and show Company and Application User lookup editors instead of User Name.
This article applies to .NET Framework applications (WinForms and ASP.NET WebForms) applications. For more information on how to implement the same scenario in .NET projects, refer to the following topic: Customize Standard Authentication Behavior and Supply Additional Logon Parameters (.NET Applications).
#Common Steps
To supply custom logon parameters, follow the steps below:
Implement a class with new parameters.
The login form should display this class’s properties. To access them, use the LogonParameters property.
Inherit the AuthenticationBase class and implement a custom authentication strategy class.
This class specifies how your application authenticates users.
As an alternative, use the approach from the ICustomObjectSerialize topic.
#Detailed Instructions
#Define Custom Parameters
Add a
Company
class to your project. This class should contain company names and a list ofApplicationUser
objects as a part of a one-to-many relationship.using DevExpress.Persistent.Base; using DevExpress.Persistent.BaseImpl.EF; using EFCoreCustomLogonAll.Module.BusinessObjects; using System.Collections.ObjectModel; namespace EFCoreCustomLogonAll.Module.BusinessObjects; [DefaultClassOptions] public class Company : BaseObject { public virtual string Name { get; set; } public virtual IList<ApplicationUser> ApplicationUsers { get; set; } = new ObservableCollection<ApplicationUser>(); }
Add the second part of this relationship to the
ApplicationUser
class generated by the Solution Wizard.public class ApplicationUser : PermissionPolicyUser, ISecurityUserWithLoginInfo { // ... public virtual Company Company { get; set; }
Add the
Company
class to your application’sDbContext
.C#public class EFCoreCustomLogonAllEFCoreDbContext : DbContext { // ... public DbSet<Company> Companies { get; set; }
#Add Custom Parameters to the Login Window
The default login window/form displays an AuthenticationStandardLogonParameters
Detail View. The corresponding object includes UserName
and Password
string properties. To change this behavior and add parameters created in the previous step to the login window, add a CustomLogonParameters
class with custom logon parameters to your project:
using DevExpress.ExpressApp.DC;
using DevExpress.ExpressApp;
using System.ComponentModel;
using System.Runtime.Serialization;
using DevExpress.Persistent.Base;
using EFCoreCustomLogonAll.Module.BusinessObjects;
namespace EFCoreCustomLogonAll.Module.BusinessObjects;
[DomainComponent, Serializable]
[System.ComponentModel.DisplayName("Log In")]
public class CustomLogonParameters : INotifyPropertyChanged {
private Company company;
private ApplicationUser _applicationUser;
private string password;
[ImmediatePostData]
public Company Company {
get { return company; }
set {
if(value == company) return;
company = value;
if(ApplicationUser?.Company != company) {
ApplicationUser = null;
}
OnPropertyChanged(nameof(Company));
}
}
[DataSourceProperty("Company.ApplicationUsers"), ImmediatePostData]
public ApplicationUser ApplicationUser {
get { return _applicationUser; }
set {
if(value == _applicationUser) return;
_applicationUser = value;
Company = _applicationUser?.Company;
UserName = _applicationUser?.UserName;
OnPropertyChanged(nameof(ApplicationUser));
}
}
[Browsable(false)]
public String UserName { get; set; }
[PasswordPropertyText(true)]
public string Password {
get { return password; }
set {
if(password == value) return;
password = value;
}
}
private void OnPropertyChanged(string propertyName) {
if(PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void RefreshPersistentObjects(IObjectSpace objectSpace) {
ApplicationUser = (UserName == null) ? null : objectSpace.FirstOrDefault<ApplicationUser>(e => e.UserName == UserName);
}
}
Important notes regarding the code snippet above:
- In the login window/form, users can only edit visible properties. In this example, these are
ApplicationUser
,Company
, andPassword
. - The DataSourcePropertyAttribute specifies contents of the Lookup Property Editor’s drop-down list.
- To show only
ApplicationUser
objects related to the selected company, theCustomLogonParameters
class filters the drop-down’s collection and refreshes it each time a user changes theCompany
property.
- Add this class for both EF Core and XPO-based projects.
- In the Middle Tier Security scenario, you must serialize logon parameters. However, reference properties cannot be serialized. To solve this problem, the example contains the hidden
UserName
property of the string type.
Tip
To access the custom logon parameters object in your code, do one of the following:
- Cast the value of the static Security
System. property to yourLogon Parameters Custom
type.Logon Parameters - Handle the Xaf
Application. event and access the LogonLogged On Event event argument value.Args. Logon Parameters
#Allow Access to New Parameter Types in the Login Window
Add Company
and ApplicationUser
types to the SecurityStrategy.AnonymousAllowedTypes collection.
#WinForms
In applications created with XAF v22.1+:
File: MySolution.Win\Startup.cs
// ... public class ApplicationBuilder : IDesignTimeApplicationFactory { public static WinApplication BuildApplication(string connectionString) { // ... builder.Security .UseIntegratedMode(options => { // ... options.Events.OnSecurityStrategyCreated = securityStrategyBase => { // ... var securityStrategy = (SecurityStrategy)securityStrategyBase; securityStrategy.AnonymousAllowedTypes.Add(typeof(Company)); securityStrategy.AnonymousAllowedTypes.Add(typeof(Employee)); }; }) // ... } }
In applications that do not use application builders:
File: MySolution.Win\WinApplication.cs(.vb) (the WinApplication descendant’s constructor).
using DevExpress.ExpressApp; using DevExpress.ExpressApp.Security; using DevExpress.ExpressApp.Win; // ... public partial class CustomLogonParametersExampleWindowsFormsApplication : WinApplication { // ... public CustomLogonParametersExampleWindowsFormsApplication() { // ... ((SecurityStrategy)Security).AnonymousAllowedTypes.Add(typeof(Company)); ((SecurityStrategy)Security).AnonymousAllowedTypes.Add(typeof(Employee)); } // ... }
#ASP.NET Web Forms
File: MySolution.Web\WebApplication.cs(.vb) (the WebApplication descendant’s constructor).
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.ExpressApp.Web;
// ...
public partial class CustomLogonParametersExampleAspNetApplication : WebApplication {
// ...
public CustomLogonParametersExampleAspNetApplication() {
// ...
((SecurityStrategy)Security).AnonymousAllowedTypes.Add(typeof(Company));
((SecurityStrategy)Security).AnonymousAllowedTypes.Add(typeof(Employee));
}
// ...
}
#Prevent the Creation of New Parameter Values in the Login Window
- Invoke the Model Editor for the module project.
- Expand the Views | CustomLogonParametersExample.Module.BusinessObjects node, and find ApplicationUser and Company Lookup List Views.
To prevent the creation of new objects in these Views, set the IModelView.AllowNew property to
false
for both Views:
#Create an Object Space for the Login Window
- Create an Object Space to display
ApplicationUser
andCompany
properties in the CreateCustomLogonWindowObjectSpace event. Assign an EventArgs.ObjectSpace parameter to the newly created Object Space.
For this purpose, use the CreateObjectSpace method and add this Object Space to the AdditionalObjectSpaces list.
Since the Logon Parameters Object might be reused for subsequent logons, persistent objects and collections linked to this object must be reloaded in the new Object Space. You can do this as follows:
#WinForms Apps
In the CustomLogonParametersExample.Win/WinApplication.cs file, subscribe to the CreateCustomLogonWindowObjectSpace
event in the application constructor:
using DevExpress.ExpressApp;
// ...
using EFCoreCustomLogonAll.Module;
using EFCoreCustomLogonAll.Module.BusinessObjects;
// ...
public class EFCoreCustomLogonAllWindowsFormsApplication : WinApplication {
public EFCoreCustomLogonAllWindowsFormsApplication() {
// ...
this.CreateCustomLogonWindowObjectSpace += application_CreateCustomLogonWindowObjectSpace;
}
private void application_CreateCustomLogonWindowObjectSpace(object sender, CreateCustomLogonWindowObjectSpaceEventArgs e) {
e.ObjectSpace = CreateObjectSpace(typeof(CustomLogonParameters));
NonPersistentObjectSpace nonPersistentObjectSpace = e.ObjectSpace as NonPersistentObjectSpace;
if(nonPersistentObjectSpace != null) {
if(!nonPersistentObjectSpace.IsKnownType(typeof(Company), true)) {
IObjectSpace additionalObjectSpace = CreateObjectSpace(typeof(Company));
nonPersistentObjectSpace.AdditionalObjectSpaces.Add(additionalObjectSpace);
nonPersistentObjectSpace.Disposed += (s2, e2) => {
additionalObjectSpace.Dispose();
};
}
}
((CustomLogonParameters)e.LogonParameters).RefreshPersistentObjects(e.ObjectSpace);
}
// ...
}
#ASP.NET Web Forms Apps
In the Global.asax.cs (Global.asax.vb) file, subscribe to the CreateCustomLogonWindowObjectSpace
event before your Global.Session_Start
method calls XafApplication.Setup.
using DevExpress.ExpressApp;
// ...
using DevExpress.ExpressApp.Web;
// ...
using CustomLogonXPOWeb.Module.BusinessObjects;
// ...
public class Global : System.Web.HttpApplication {
// ...
protected void Session_Start(Object sender, EventArgs e) {
// ...
WebApplication.Instance.CreateCustomLogonWindowObjectSpace +=
application_CreateCustomLogonWindowObjectSpace;
WebApplication.Instance.Setup();
// ...
}
private static void application_CreateCustomLogonWindowObjectSpace(object sender, CreateCustomLogonWindowObjectSpaceEventArgs e) {
e.ObjectSpace = ((XafApplication)sender).CreateObjectSpace(typeof(CustomLogonParameters));
NonPersistentObjectSpace nonPersistentObjectSpace = e.ObjectSpace as NonPersistentObjectSpace;
if (nonPersistentObjectSpace != null) {
if (!nonPersistentObjectSpace.IsKnownType(typeof(Company), true)) {
IObjectSpace additionalObjectSpace = ((XafApplication)sender).CreateObjectSpace(typeof(Company));
nonPersistentObjectSpace.AdditionalObjectSpaces.Add(additionalObjectSpace);
nonPersistentObjectSpace.Disposed += (s2, e2) => {
additionalObjectSpace.Dispose();
};
}
}
((CustomLogonParameters)e.LogonParameters).RefreshPersistentObjects(e.ObjectSpace);
}
// ...
}
#Implement Custom Authentication
Specify how your application authenticates users. For this purpose, add a CustomAuthentication
class to your project. Note that this class should inherit from the AuthenticationBase class.
using CustomLogonXPOWeb.Module.BusinessObjects;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
using System;
using System.Collections.Generic;
namespace CustomLogonXPOWeb.Module.Security {
public class CustomAuthentication : AuthenticationBase, IAuthenticationStandard {
private CustomLogonParameters customLogonParameters;
public CustomAuthentication() {
customLogonParameters = new CustomLogonParameters();
}
public override void Logoff() {
base.Logoff();
customLogonParameters = new CustomLogonParameters();
}
public override void ClearSecuredLogonParameters() {
customLogonParameters.Password = "";
base.ClearSecuredLogonParameters();
}
public override object Authenticate(IObjectSpace objectSpace) {
ApplicationUser applicationUser = objectSpace.FirstOrDefault<ApplicationUser>(e => e.UserName == customLogonParameters.UserName);
if(applicationUser == null)
throw new ArgumentNullException("ApplicationUser");
if(!((IAuthenticationStandardUser)applicationUser).ComparePassword(customLogonParameters.Password))
throw new AuthenticationException(
applicationUser.UserName, "Password mismatch.");
return applicationUser;
}
public override void SetLogonParameters(object logonParameters) {
this.customLogonParameters = (CustomLogonParameters)logonParameters;
}
public override IList<Type> GetBusinessClasses() {
return new Type[] { typeof(CustomLogonParameters) };
}
public override bool AskLogonParametersViaUI {
get { return true; }
}
public override object LogonParameters {
get { return customLogonParameters; }
}
public override bool IsLogoffEnabled {
get { return true; }
}
}
}
For information on methods and properties that are overridden in this code snippet, refer to the AuthenticationBase class description.
Note
In the client-server security configuration, you should also:
- Override the Authentication
Base. method to initialize the AuthenticationSet Logon Parameters Base. property.Logon Parameters - Register the type of custom logon parameters using the Wcf
Data static method before the data server initialization.Server Helper. Add Known Type
#Pass Custom Classes to the Security System
#WinForms Apps Created with XAF v22.1+
In the Startup.cs file, remove the UsePasswordAuthentication
method and replace auto-generated authentication with your custom authentication.
// ...
public class ApplicationBuilder : IDesignTimeApplicationFactory {
public static WinApplication BuildApplication(string connectionString) {
// ...
builder.Security
.UseIntegratedMode(options => {
// ...
options.Events.OnSecurityStrategyCreated = securityStrategyBase => {
var securityStrategy = (SecurityStrategy)securityStrategyBase;
securityStrategy.Authentication = new CustomAuthentication();
securityStrategy.AnonymousAllowedTypes.Add(typeof(Company));
securityStrategy.AnonymousAllowedTypes.Add(typeof(ApplicationUser));
};
})/*.UsePasswordAuthentication()*/;
// ...
}
}
#WinForms Apps (that Do Not Use Application Builders) and ASP.NET Web Forms Apps
In the WinApplication.Designer.cs (WinApplication.Designer.vb) and WebApplication.cs (WebApplication.cs) files, set the SecurityStrategyComplex.Authentication
property to the new CustomAuthentication
instance.
partial class CustomLogonParametersExampleWindowsFormsApplication {
private void InitializeComponent() {
// ...
this.securityStrategyComplex1.Authentication = new CustomAuthentication();
this.securityStrategyComplex1.RoleType = typeof(DevExpress.Persistent.BaseImpl.PermissionPolicy.PermissionPolicyRole);
this.securityStrategyComplex1.UserType = typeof(ApplicationUser);
}
// ...
}
#Add Demo Data
Override the ModuleUpdater.UpdateDatabaseAfterUpdateSchema method to create companies, application users, and security roles.
using CustomLogonXPOWin.Module.BusinessObjects;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.ExpressApp.SystemModule;
using DevExpress.ExpressApp.Updating;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl;
using DevExpress.Persistent.BaseImpl.PermissionPolicy;
namespace CustomLogonXPOWin.Module.DatabaseUpdate;
// For more typical usage scenarios, be sure to check out https://docs.devexpress.com/eXpressAppFramework/DevExpress.ExpressApp.Updating.ModuleUpdater
public class Updater : ModuleUpdater {
public Updater(IObjectSpace objectSpace, Version currentDBVersion) :
base(objectSpace, currentDBVersion) {
}
public override void UpdateDatabaseAfterUpdateSchema() {
base.UpdateDatabaseAfterUpdateSchema();
//string name = "MyName";
//DomainObject1 theObject = ObjectSpace.FirstOrDefault<DomainObject1>(u => u.Name == name);
//if(theObject == null) {
// theObject = ObjectSpace.CreateObject<DomainObject1>();
// theObject.Name = name;
//}
#if !RELEASE
ApplicationUser sampleUser = ObjectSpace.FirstOrDefault<ApplicationUser>(u => u.UserName == "User");
if(sampleUser == null) {
sampleUser = ObjectSpace.CreateObject<ApplicationUser>();
sampleUser.UserName = "User";
// Set a password if the standard authentication type is used.
sampleUser.SetPassword("");
// The UserLoginInfo object requires a user object Id (Oid).
// Commit the user object to the database before you create a UserLoginInfo object. This will correctly initialize the user key property.
ObjectSpace.CommitChanges(); //This line persists created object(s).
((ISecurityUserWithLoginInfo)sampleUser).CreateUserLoginInfo(SecurityDefaults.PasswordAuthentication, ObjectSpace.GetKeyValueAsString(sampleUser));
}
PermissionPolicyRole defaultRole = CreateDefaultRole();
sampleUser.Roles.Add(defaultRole);
ApplicationUser userAdmin = ObjectSpace.FirstOrDefault<ApplicationUser>(u => u.UserName == "Admin");
if(userAdmin == null) {
userAdmin = ObjectSpace.CreateObject<ApplicationUser>();
userAdmin.UserName = "Admin";
// Set a password if the standard authentication type is used.
userAdmin.SetPassword("");
// The UserLoginInfo object requires a user object Id (Oid).
// Commit the user object to the database before you create a UserLoginInfo object. This will correctly initialize the user key property.
ObjectSpace.CommitChanges(); //This line persists created object(s).
((ISecurityUserWithLoginInfo)userAdmin).CreateUserLoginInfo(SecurityDefaults.PasswordAuthentication, ObjectSpace.GetKeyValueAsString(userAdmin));
}
// If a role with the Administrators name doesn't exist in the database, create this role.
PermissionPolicyRole adminRole = ObjectSpace.FirstOrDefault<PermissionPolicyRole>(r => r.Name == "Administrators");
if(adminRole == null) {
adminRole = ObjectSpace.CreateObject<PermissionPolicyRole>();
adminRole.Name = "Administrators";
}
adminRole.IsAdministrative = true;
userAdmin.Roles.Add(adminRole);
if(ObjectSpace.FindObject<Company>(null) == null) {
Company company1 = ObjectSpace.CreateObject<Company>();
company1.Name = "Company 1";
company1.ApplicationUsers.Add(userAdmin);
ApplicationUser user1 = ObjectSpace.CreateObject<ApplicationUser>();
user1.UserName = "Sam";
user1.SetPassword("");
user1.Roles.Add(defaultRole);
ApplicationUser user2 = ObjectSpace.CreateObject<ApplicationUser>();
user2.UserName = "John";
user2.SetPassword("");
user2.Roles.Add(defaultRole);
Company company2 = ObjectSpace.CreateObject<Company>();
company2.Name = "Company 2";
company2.ApplicationUsers.Add(user1);
company2.ApplicationUsers.Add(user2);
}
ObjectSpace.CommitChanges(); //This line persists created object(s).
#endif
}
public override void UpdateDatabaseBeforeUpdateSchema() {
base.UpdateDatabaseBeforeUpdateSchema();
//if(CurrentDBVersion < new Version("1.1.0.0") && CurrentDBVersion > new Version("0.0.0.0")) {
// RenameColumn("DomainObject1Table", "OldColumnName", "NewColumnName");
//}
}
private PermissionPolicyRole CreateDefaultRole() {
PermissionPolicyRole defaultRole = ObjectSpace.FirstOrDefault<PermissionPolicyRole>(role => role.Name == "Default");
if(defaultRole == null) {
defaultRole = ObjectSpace.CreateObject<PermissionPolicyRole>();
defaultRole.Name = "Default";
defaultRole.AddObjectPermissionFromLambda<ApplicationUser>(SecurityOperations.Read, cm => cm.Oid == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
defaultRole.AddNavigationPermission(@"Application/NavigationItems/Items/Default/Items/MyDetails", SecurityPermissionState.Allow);
defaultRole.AddMemberPermissionFromLambda<ApplicationUser>(SecurityOperations.Write, "ChangePasswordOnFirstLogon", cm => cm.Oid == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
defaultRole.AddMemberPermissionFromLambda<ApplicationUser>(SecurityOperations.Write, "StoredPassword", cm => cm.Oid == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<PermissionPolicyRole>(SecurityOperations.Read, SecurityPermissionState.Deny);
defaultRole.AddTypePermissionsRecursively<ModelDifference>(SecurityOperations.ReadWriteAccess, SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<ModelDifferenceAspect>(SecurityOperations.ReadWriteAccess, SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<ModelDifference>(SecurityOperations.Create, SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<ModelDifferenceAspect>(SecurityOperations.Create, SecurityPermissionState.Allow);
}
return defaultRole;
}
}
#Generate a Demo Database
Generate a database and initial data to be shown in the Login Window before a user logs in for the first time. For this purpose, run your application one time with the following debug parameters:
--updateDatabase --forceUpdate --silent
#Run the Application
You can now run the application to see custom parameters (Company and Application User lookup editors) in the login window.