Skip to main content
All docs
V24.1
.NET 6.0+

Customize Standard Authentication Behavior and Supply Additional Logon Parameters (.NET Applications)

  • 13 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 .NET applications. For more information on how to implement the same scenario in .NET Framework projects, refer to the following topic: Customize Standard Authentication Behavior and Supply Additional Logon Parameters (.NET Framework Applications).

Define a Data Model for 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 (EF Core only).

    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 application’s module project (MySolution.Module). The implementation of this class may require additional adjustments based on the target UI platforms. See the Platform Specifics subsection for more information.

using DevExpress.ExpressApp.Core; 
using DevExpress.ExpressApp.DC; 
using DevExpress.ExpressApp.Security; 
using DevExpress.Persistent.Base; 
using MySolution.Module.BusinessObjects; 
using Microsoft.Extensions.DependencyInjection; 
using System.ComponentModel; 
using System.Text.Json.Serialization; 

[DomainComponent] 
[DisplayName("Log In")] 
public class CustomLogonParameters : IAuthenticationStandardLogonParameters, ISupportClearPassword, 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; 
    } 

    public void ClearPassword() { 
        password = null; 
    }
} 

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.
  • The CustomLogonParameters class must implement the ISupportClearPassword and AuthenticationStandardLogonParameters interfaces.

Platform Specifics

Depending on the target platform, you may need to adjust the CustomLogonParameters class implementation as described below.

ASP.NET Core Blazor

In Blazor, a logon parameters object is serialized to JSON and stored in a cookie. This has the following implications:

  • The CustomLogonParameters object must support the ISupportClearPassword interface, because the password property must never be added to the cookie.

  • All CustomLogonParameters object properties whose data type does not support JSON serialization must be decorated with the JsonIgnoreAttribute.

  • You should not use the JsonIgnoreAttribute on serializable properties that need to be submitted to the Web API Service‘s Authenticate endpoint. Otherwise, the values of these properties will be impossible to submit.

  • To keep the cookie compact, we recommend that you assign null to all properties that are not required by the application after the logon process is complete. You can do this in the ISupportClearPassword.ClearPassword method.

    [DomainComponent] 
    [DisplayName("Log In")] 
    public class CustomLogonParameters : IAuthenticationStandardLogonParameters, ISupportClearPassword, INotifyPropertyChanged, IDisposable, IServiceProviderConsumer { 
        // ...
        public void ClearPassword() { 
            password = null;
            CustomData = null; 
        }
        // ...
    } 
    

WinForms

The WinForms platform requires CustomLogonParameters to implement the ISupportClearPassword and IAuthenticationStandardLogonParameters interfaces. No additional requirements apply.

WinForms with Middle Tier Security

  • The CustomLogonParameters object’s properties must be decorated with appropriate attributes depending on whether or not these properties are serializable and/or must be serialized and passed between the client and server after the logon process is complete:

    • DataMemberAttribute - if a property is serializable and must be passed between the client and server.
    • IgnoreDataMemberAttribute - if a property is not serializable or should not be passed between the client and server.
  • If nullable reference types are enabled for the whole project, all properties in the CustomLogonParameters class must be marked as nullable. Otherwise, in some cases, faulty behavior can occur during a user login attempt (when a user leaves a logon parameter input empty but tries to log in).

Implement Custom Authentication

Next, you need to implement custom logic that makes authentication decisions against the custom set of logon parameters specified by a user. To do this, use one of the following techniques:

Handle the OnAuthenticate Event

Handle the OnAuthenticate event to implement custom logic required to authenticate a user. Note that this logic completely overrides the default logic used for password-based authentication.

File: MySolution.Blazor.Server\Startup.cs, MySolution.Win\Startup.cs (MySolution.MiddleTier\Startup.cs), MySolution.WebApi\Startup.cs

// ...
builder.Security
    .UseIntegratedMode(options => {
    // ...
    })        
    .AddPasswordAuthentication(options => {
        options.IsSupportChangePassword = true;
        options.LogonParametersType = typeof(CustomLogonParameters);
        options.Events.OnAuthenticate = context => {
            CustomLogonParameters logonParameters = (CustomLogonParameters)context.LogonParameters;
            ApplicationUser applicationUser = context.ObjectSpace.FirstOrDefault<ApplicationUser>(e => e.UserName == context.LogonParameters.UserName);

            if (applicationUser == null)
                throw new AuthenticationException(
                    context.LogonParameters.UserName,
                    SecurityExceptionLocalizer.GetExceptionMessage(SecurityExceptionId.RetypeTheInformation)
                );

            var userLockoutService = context.ObjectSpace.ServiceProvider.GetRequiredService<IUserLockout>();
            if (userLockoutService.IsLockedOut(applicationUser)) {
                ISecurityUserLockout userLockout = (ISecurityUserLockout)applicationUser;
                throw new AuthenticationException(logonParameters.UserName, SecurityExceptionLocalizer.GetExceptionMessage(SecurityExceptionId.UserLockout, Math.Floor((userLockout.LockoutEnd - DateTime.UtcNow).TotalSeconds)));
            }

            if (applicationUser.Company.ID != logonParameters.Company.ID) {
                throw new AuthenticationException(
                    applicationUser.UserName, "No such user in the specified company.");
            }

            if (!((IAuthenticationStandardUser)applicationUser).ComparePassword(logonParameters.Password)) {
                userLockoutService.AccessFailed(applicationUser, context.ObjectSpace);
                throw new AuthenticationException(
                    applicationUser.UserName, "Password mismatch.");
            }

            if (!((ISecurityUser)applicationUser).IsActive) {
                throw new AuthenticationException(applicationUser.UserName, SecurityExceptionLocalizer.GetExceptionMessage(SecurityExceptionId.UserIsNotActive));
            }

            userLockoutService.ResetLockout(applicationUser, context.ObjectSpace);
            context.User = applicationUser;
        };
    });
// ...

Implement a Custom Authentication Provider (Advanced)

Add a CustomAuthentication class to your solution’s module project (MySolution.Module). Note that this class should inherit from the AuthenticationBase class.

using DevExpress.ExpressApp; 
using DevExpress.ExpressApp.Security; 
using DevExpress.Persistent.Base.Security;
using MySolution.Module.BusinessObjects;
using Microsoft.Extensions.DependencyInjection;

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"); 

        var userLockoutService = objectSpace.ServiceProvider.GetRequiredService<IUserLockout>(); 
        if(userLockoutService.IsLockedOut(applicationUser) ?? false) {
            ISecurityUserLockout userLockout = (ISecurityUserLockout)applicationUser;
            throw new AuthenticationException(customLogonParameters.UserName, SecurityExceptionLocalizer.GetExceptionMessage(SecurityExceptionId.UserLockout, Math.Floor((userLockout.LockoutEnd - DateTime.UtcNow).TotalSeconds)));
        } 

        if(!((IAuthenticationStandardUser)applicationUser).ComparePassword(customLogonParameters.Password)) {
            userLockoutService.AccessFailed(applicationUser, objectSpace);
            throw new AuthenticationException(
                applicationUser.UserName, "Password mismatch.");
        } 

        if(!((ISecurityUser)user).IsActive) {
            throw new AuthenticationException(user.UserName, SecurityExceptionLocalizer.GetExceptionMessage(SecurityExceptionId.UserIsNotActive));
        } 

        if(!ValidateSecurityUser(user)) {
            throw new AuthenticationException(user.UserName); 

        } 

        userLockoutService.ResetLockout(user, objectSpace); 
        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.

Add a CustomAuthenticationStandardProvider class to your module project (MySolution.Module). 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

If You Used the OnAuthenticate Event

In the application’s Startup.cs files, set the provider’s LogonParametersType option to CustomLogonParameters within the AddPasswordAuthentication method call.

File: MySolution.Blazor.Server\Startup.cs, MySolution.Win\Startup.cs, (MySolution.MiddleTier\Startup.cs), MySolution.WebApi\Startup.cs

// ...
builder.Security
    .UseIntegratedMode(options => {
    // ...
    })
    .AddPasswordAuthentication(options => {
        options.IsSupportChangePassword = true;
        options.LogonParametersType = typeof(CustomLogonParameters);
    });
// ...

If You Implemented a Custom Authentication Provider

In the application’s Startup.cs files:

  • Comment out the AddPasswordAuthentication method.
  • Pass a custom CustomAuthenticationStandardProvider to Security.
  • Set the provider’s LogonParametersType option to CustomLogonParameters.

File: MySolution.Blazor.Server\Startup.cs, MySolution.Win\Startup.cs, (MySolution.MiddleTier\Startup.cs), MySolution.WebApi\Startup.cs

// ...
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
        var defaultRole = CreateDefaultRole();
        var adminRole = CreateAdminRole();
        ObjectSpace.CommitChanges(); //This line persists created object(s).  

        UserManager userManager = ObjectSpace.ServiceProvider.GetRequiredService<UserManager>();
        ApplicationUser userAdmin = userManager.FindUserByName<ApplicationUser>(ObjectSpace, "Admin");
        // If a user named 'Admin' doesn't exist in the database, create this user.
        if (userAdmin == null) {
            // Set a password if the standard authentication type is used.
            string EmptyPassword = "";
            userAdmin = userManager.CreateUser<ApplicationUser>(ObjectSpace, "Admin", EmptyPassword, (user) => {
                // Add the Administrators role to the user.
                user.Roles.Add(adminRole);
            }).User;
        }

        if (ObjectSpace.FindObject<Company>(null) == null) {
            Company company1 = ObjectSpace.CreateObject<Company>();
            company1.Name = "Company 1";
            company1.ApplicationUsers.Add(userAdmin);
            ApplicationUser user1 = userManager.CreateUser<ApplicationUser>(ObjectSpace, "Sam", "", (user) => {
                user.Roles.Add(defaultRole);
            }).User;
            ApplicationUser user2 = userManager.CreateUser<ApplicationUser>(ObjectSpace, "John", "", (user) => {
                user.Roles.Add(defaultRole);
            }).User;
            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

Standard XAF applications generate data after a user logs in. To generate a database and demo data before login, use one of the following techniques depending on the target platform:

Blazor, WinForms with Middle-Tier Security

  1. Add an ApplicationBuilderExtensions extension class to your Blazor or Middle-Tier Server project:

    using DevExpress.ExpressApp.Core;
    
    namespace MySolution.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 to generate data:

    File: MySolution.Blazor/Startup.cs, MySolution.MiddleTier/Startup.cs

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

WinForms

Add the following line to the WinForms project’s Program.cs file:

File: MySolution.Win/Program.cs

public static int Main(string[] args) {
    // ...
    try {
        winApplication.Setup();
# if DEBUG
        winApplication.CheckCompatibility();
# endif
        winApplication.Start();
    }
    // ...
}

Platform-Specific Steps

You also need to complete the steps described below to configure some of your solution’s platform-specific application modules to work with custom logon parameters.

Web API Service (Standalone)

A Web API Service application exposes an Authenticate endpoint. For this endpoint, the logon parameters type is explicitly specified and used by the deserialization logic. For authentication to work correctly, you need to replace the default type with your custom type in the AuthenticationController implementation. You can also update the SwaggerRequestBody attribute for the Swagger UI to display a proper hint on the request body format:

File: MySolution.WebApi/API/Security/AuthenticationController.cs

public class AuthenticationController : ControllerBase {
    // ...  
    public IActionResult Authenticate(
        [FromBody]
        [SwaggerRequestBody(@"For example: <br /> { ""userName"": ""Admin"", ""password"": """", ""customParameter:"" ""customParameterValue"" }")]
        // AuthenticationStandardLogonParameters logonParameters
        CustomLogonParameters logonParameters
    ) {
        // ...
    }
}

WinForms with Middle-Tier Security

Register the Custom Logon Parameters Class in the Client Application

The CustomLogonParameters class must be explicitly registered in the client WinForms application. To do this, add the following line to the Main method within the client application’s Program.cs file:

File: MySolution.Win/Program.cs

static class Program {
    // ...
    [STAThread]
    public static int Main(string[] args) {
        // ...
        DevExpress.ExpressApp.Security.ClientServer.WebApi.WebApiDataServerHelper.AddKnownType(typeof(CustomLogonParameters));
        // ...
    }
    // ...
}

If this step is omitted, an exception will occur when you try to run the client application.

Register Types Allowed for Anonymous Access

This step is required if your CustomLogonParameters implementation needs to access some objects to populate the login window’s editors. For example, consider the following method from the CustomLogonParameters implementation shown in this topic:

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();
}

You can see that this code uses a Non-Secure Object Space Factory to read objects of the Company type. However, in an application with Middle Tier Security, data access operations are validated by the security system if initiated on the client side even when a Non-Secure Object Space is used. Because no user is logged in at the stage when this code runs, trying to access data through the Object Space will result in an exception.

For this code to work, you need to explicitly allow anonymous access to objects of the Company type. To do this, add the following line to the OnSecurityStrategyCreated event handler in the server application’s Startup.cs file:

File: MySolution.MiddleTier/Startup.cs

// ...
builder.Security
    .UseIntegratedMode(options => {
        // ...
        options.Events.OnSecurityStrategyCreated += securityStrategy => {
            // ...
            ((SecurityStrategy)securityStrategy).AnonymousAllowedTypes.Add(typeof(Company));
        };
// ...
});

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