Skip to main content
.NET 6.0+

Customize Standard Authentication Behavior and Supply Additional Logon Parameters (WinForms & ASP.NET Web Forms)

  • 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.

DevExpress XAF - Customize Standard Login Window

View Example: XAF - Customize Logon Parameters

This article applies to WinForms and ASP.NET WebForms applications. For more information on how to implement the same scenario in ASP.NET Core Blazor projects, refer to the following topic: Customize Standard Authentication Behavior and Supply Additional Logon Parameters (Blazor).

Common Steps

To supply custom logon parameters, follow the steps below:

  1. Implement a class with new parameters.

    The login form should display this class’s properties. To access them, use the LogonParameters property.

  2. 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

  1. Add a Company class to your project. This class should contain company names and a list of ApplicationUser 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>();
    }
    
  2. 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; }
    
  3. Add the Company class to your application’s DbContext.

    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, and Password.
  • The DataSourcePropertyAttribute specifies contents of the Lookup Property Editor’s drop-down list.
  • To show only ApplicationUser objects related to the selected company, the CustomLogonParameters class filters the drop-down’s collection and refreshes it each time a user changes the Company 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 custom logon parameters object in your code, use any of the following methods:

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

  1. Invoke the Model Editor for the module project.
  2. Expand the Views | CustomLogonParametersExample.Module.BusinessObjects node, and find ApplicationUser and Company Lookup List Views.
  3. To prevent the creation of new objects in these Views, set the IModelView.AllowNew property to false for both Views:

    ModelEditor_AllowNew

Create an Object Space for the Login Window

  1. Create an Object Space to display ApplicationUser and Company properties in the CreateCustomLogonWindowObjectSpace event.
  2. 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 next 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:

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

DevExpress XAF - Generate a Demo Database

Run the Application

You can now run the application to see custom parameters (Company and Application User lookup editors) in the login window.

DevExpress XAF - Custom Login Form in an ASP.NET Web Forms App

See Also