Skip to main content
All docs
V23.2
.NET 6.0+

Customize Standard Authentication Behavior and Supply Additional Logon Parameters (Blazor)

  • 7 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 ASP.NET Core Blazor applications. For more information on how to implement the same scenario in WinForms and ASP.NET WebForms projects, refer to the following topic: Customize Standard Authentication Behavior and Supply Additional Logon Parameters (WinForms & ASP.NET Web Forms).

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 Blazor project:

using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Core;
using DevExpress.ExpressApp.DC;
using DevExpress.Persistent.Base;
using EFCoreCustomLogonAll.Module.BusinessObjects;
using Newtonsoft.Json;
using System.ComponentModel;

namespace EFCustomLogon.Module.BusinessObjects;

[DomainComponent]
[DisplayName("Log In")]
public class CustomLogonParameters : INotifyPropertyChanged, IDisposable, IServiceProviderConsumer {
    private Company company;
    private ApplicationUser applicationUser;
    private string password;
    IServiceProvider? serviceProvider;
    readonly List<IDisposable> objToDispose = new List<IDisposable>();
    IReadOnlyList<Company>? _companies = null;

    [JsonIgnore]
    [ImmediatePostData]
    [DataSourceProperty("Companies", DataSourcePropertyIsNullMode.SelectAll)]
    public Company Company {
        get { return company; }
        set {
            if(value == company) return;
            company = value;
            if(ApplicationUser?.Company != company) {
                ApplicationUser = null;
            }
            OnPropertyChanged(nameof(Company));
        }
    }

    [Browsable(false)]
    [JsonIgnore]
    public IReadOnlyList<Company>? Companies {
        get {
            if(_companies == null) {
                _companies = LoadData();
            }
            return _companies;
        }
    }
    private IReadOnlyList<Company> LoadData() {
        List<Company> companies = new List<Company>();
        INonSecuredObjectSpaceFactory nonSecuredObjectSpaceFactory = serviceProvider!.GetRequiredService<INonSecuredObjectSpaceFactory>();
        var os = nonSecuredObjectSpaceFactory.CreateNonSecuredObjectSpace<Company>();
        objToDispose.Add(os);
        companies.AddRange(os.GetObjects<Company>());
        return companies.AsReadOnly();
    }
    void IDisposable.Dispose() {
        foreach(IDisposable disposable in objToDispose) {
            disposable.Dispose();
        }
        serviceProvider = null;
    }
    [JsonIgnore]
    [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;

    void IServiceProviderConsumer.SetServiceProvider(IServiceProvider serviceProvider) {
        this.serviceProvider = serviceProvider;
    }
}

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.

Implement Custom Authentication

Specify how your application authenticates users. For this purpose, add a CustomAuthentication class to your Blazor project. Note that this class should inherit from the AuthenticationBase class.

using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
using EFCoreCustomLogonAll.Module.BusinessObjects;

namespace EFCustomLogon.Module.BusinessObjects;

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) {
        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.

Implement a Custom Authentication Provider

Add a CustomAuthenticationStandardProvider class to your Blazor project. Ensure that this class inherits from the AuthenticationStandardProviderV2 class. Override the CreateAuthentication method of the newly created class to return a CustomAuthentication instance.

using DevExpress.ExpressApp.Security;
using Microsoft.Extensions.Options;

namespace EFCustomLogon.Module.BusinessObjects;

public class CustomAuthenticationStandardProvider : AuthenticationStandardProviderV2 {
    public CustomAuthenticationStandardProvider(IOptions<AuthenticationStandardProviderOptions> options,
    IOptions<SecurityOptions> securityOptions) :
        base(options, securityOptions) { }
    protected override AuthenticationBase CreateAuthentication(Type userType, Type logonParametersType) {
        return new CustomAuthentication();
    }
}

Pass Custom Classes to the Security System

In the Startup.cs file:

  • Comment out the AddPasswordAuthentication method.
  • Pass a custom CustomAuthenticationStandardProvider to Security.
  • Set the provider’s LogonParametersType option to CustomLogonParameters.
public class Startup {
// ...
    public void ConfigureServices(IServiceCollection services) {
    // ...
        services.AddXaf(Configuration, builder => {
        // ...
            builder.Security
                .UseIntegratedMode(options => {
                // ...
                })
                  .AddAuthenticationProvider<AuthenticationStandardProviderOptions, CustomAuthenticationStandardProvider>(options => {
                      options.IsSupportChangePassword = true;
                      options.LogonParametersType = typeof(CustomLogonParameters);
                  });
            //.AddPasswordAuthentication(options => {
            //    options.IsSupportChangePassword = true;
            //});
        });
        // ...
    }
}

Add Demo Data

Override the ModuleUpdater.UpdateDatabaseAfterUpdateSchema method to create companies, application users, and security roles.

using DevExpress.ExpressApp.Security;
using DevExpress.ExpressApp.SystemModule;
// ...
public class Updater : ModuleUpdater {
    public Updater(IObjectSpace objectSpace, Version currentDBVersion) :
        base(objectSpace, currentDBVersion) {
    }
    public override void UpdateDatabaseAfterUpdateSchema() {
        base.UpdateDatabaseAfterUpdateSchema();
        //string name = "MyName";
        //EntityObject1 theObject = ObjectSpace.FirstOrDefault<EntityObject1>(u => u.Name == name);
        //if(theObject == null) {
        //    theObject = ObjectSpace.CreateObject<EntityObject1>();
        //    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
// ...
    }
}

Generate a Demo Database

  1. Standard XAF applications generate data after a user logs in. To generate a database and demo data before login, add an ApplicationBuilderExtensions extension class to your Blazor project:

    using DevExpress.ExpressApp.Core;
    
    namespace EFCoreCustomLogonAll.Blazor.Server.Security;
    
    static class ApplicationBuilderExtensions {
    // ...
        //}
        public static IApplicationBuilder UseDemoData(this IApplicationBuilder app) {
            using var scope = app.ApplicationServices.CreateScope();
    
            var updatingObjectSpaceFactory = scope.ServiceProvider.GetRequiredService<IUpdatingObjectSpaceFactory>();
            using var objectSpace = updatingObjectSpaceFactory.CreateUpdatingObjectSpace(typeof(Module.BusinessObjects.ApplicationUser), true);
            new Module.DatabaseUpdate.Updater(objectSpace, new Version()).UpdateDatabaseAfterUpdateSchema();
    
            return app;
        }
    
  2. Utilize the newly created class in the Startup.cs file:

    public class Startup {
    // ...
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
        // ...
            app.UseDemoData();
        }
    }
    

Run the Application

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

custom-logon-parameters-blazor-result