Skip to main content
All docs
V25.1
  • .NET 8.0+

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

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

    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 Template Kit.

      public class ApplicationUser : PermissionPolicyUser, ISecurityUserWithLoginInfo, ISecurityUserLockout {
      // ...
          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 form uses an AuthenticationStandardLogonParameters Detail View that includes UserName and Password fields. To add custom fields, create a CustomLogonParameters class in your application’s module project (MySolution.Module).

    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.ConditionalAppearance;
    using DevExpress.ExpressApp.DC;
    using DevExpress.ExpressApp.Security;
    using DevExpress.Persistent.Base;
    using Microsoft.Extensions.DependencyInjection;
    using System.ComponentModel;
    using System.Runtime.Serialization;
    using System.Text.Json.Serialization;
    
    namespace EFCoreCustomLogonAll.Module.Authentication;
    
    [DataContract]
    [DomainComponent]
    [DisplayName("Log In")]
    [Appearance("CompanyIsNull", TargetItems = $"{nameof(Password)}, {nameof(ApplicationUser)}", Criteria = "IsNull([Company])", Enabled = false)]
    public class CustomLogonParameters : IAuthenticationStandardLogonParameters, ISupportClearPassword, INotifyPropertyChanged, IObjectSpaceLink {
        private CompanyDTO? company;
        private ApplicationUserDTO? applicationUser;
        private string? password;
        private IReadOnlyList<CompanyDTO>? _companies = null;
        private IObjectSpace? objectSpace;
        private ILogonDataProvider? logonDataProvider;
    
        [Browsable(false)]
        public string? UserName { get; set; }
    
        [JsonIgnore]
        [ImmediatePostData]
        [DataSourceProperty("Companies", DataSourcePropertyIsNullMode.SelectAll)]
        [IgnoreDataMember]
        public CompanyDTO? Company {
            get { return company; }
            set {
                if(value == company) return;
                company = value;
                if(company == null || ApplicationUser?.CompanyID != company.ID) {
                    ApplicationUser = null;
                }
                OnPropertyChanged(nameof(CompanyDTO));
            }
        }
        [Browsable(false)] // hide from UI
        [JsonIgnore]
        [IgnoreDataMember]
        public IReadOnlyList<CompanyDTO>? Companies {
            get {
                if(_companies == null) {
                    if(logonDataProvider != null) {
                        _companies = logonDataProvider.GetCompanies().AsReadOnly();
                    } else {
                        _companies = Array.Empty<CompanyDTO>();
                    }
                }
                return _companies;
            }
        }
    
        [JsonIgnore]
        [DataSourceProperty($"{nameof(Company)}.{nameof(Company.ApplicationUsers)}"), ImmediatePostData]
        [System.Runtime.Serialization.IgnoreDataMember]
        public ApplicationUserDTO? ApplicationUser {
            get { return applicationUser; }
            set {
                if(value == applicationUser)
                    return;
                applicationUser = value;
                UserName = applicationUser?.UserName;
                OnPropertyChanged(nameof(ApplicationUser));
            }
        }
    
        [PasswordPropertyText(true)]
        [DataMember]
        public string? Password {
            get { return password; }
            set {
                if(password == value)
                    return;
                password = value;
                OnPropertyChanged(nameof(Password));
            }
        }
    
        IObjectSpace? IObjectSpaceLink.ObjectSpace {
            get => objectSpace;
            set {
                objectSpace = value;
                if(objectSpace != null) {
                    logonDataProvider = objectSpace.ServiceProvider.GetService<ILogonDataProvider>();
                }
                ApplicationUser = null;
                Company = null;
                _companies = null;
            }
        }
    
        private void OnPropertyChanged(string propertyName) {
            if(PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    
        void ISupportClearPassword.ClearPassword() {
            password = null;
        }
    
        public event PropertyChangedEventHandler? PropertyChanged;
    }
    

    Note the following implementation details:

    • The CustomLogonParameters class must implement the ISupportClearPassword and IAuthenticationStandardLogonParameters interfaces.
    • (Blazor specific) Properties with data types that cannot be serialized to JSON must be marked with JsonIgnoreAttribute.
    • (Blazor specific) To keep the cookie compact, assign null to unnecessary properties after login in the ISupportClearPassword.ClearPassword method.
    • (WinForms specific) Properties should be marked with the correct attributes based on whether they need to be serialized and transferred between the client and server after login:
    • (WinForms specific) If nullable reference types are enabled for the project, all properties in the CustomLogonParameters class should be marked as nullable.

    Implement Data Transfer Objects (DTO)

    1. To access lists of companies and application users in login form (before authentication), implement CompanyDTO and ApplicationUserDTO data transfer objects.

      using DevExpress.ExpressApp.DC;
      using DevExpress.Persistent.Base;
      
      namespace EFCoreCustomLogonAll.Module.Authentication;
      
      [DomainComponent]
      public class CompanyDTO {
          IList<ApplicationUserDTO>? users;
      
          public virtual string? Name { get; set; }
      
          [DevExpress.ExpressApp.Data.Key]
          [VisibleInListView(false), VisibleInDetailView(false), VisibleInLookupListView(false)]
          public virtual Guid ID { get; set; }
      
          public IList<ApplicationUserDTO>? ApplicationUsers {
              get {
                  if(users == null && LogonDataProvider != null) {
                      users = LogonDataProvider.GetCompanyUsers(ID);
                  }
                  return users;
              }
          }
          public ILogonDataProvider? LogonDataProvider { get; set; }
      }
      
    2. Implement the AuthenticationDataController with GetCompanies and GetApplicationUsers methods to supply data for the logon form. This data is consumed by the CustomLogonParameters class, which operates in Middle Tier and standard Blazor applications.

      using DevExpress.ExpressApp;
      using EFCoreCustomLogonAll.Module.Authentication;
      using EFCoreCustomLogonAll.Module.BusinessObjects;
      using Microsoft.AspNetCore.Mvc;
      
      namespace EFCoreCustomLogonAll.MiddleTier.Authentication {
          public class AuthenticationDataController : ControllerBase, IDisposable {
              IObjectSpace nonSecuredObjectSpace;
              private readonly INonSecuredObjectSpaceFactory? nonSecuredObjectSpaceFactory;
              public AuthenticationDataController(INonSecuredObjectSpaceFactory nonSecuredObjectSpaceFactory) {
                  this.nonSecuredObjectSpaceFactory = nonSecuredObjectSpaceFactory;
              }
      
              [HttpGet("/GetCompanies")]
              public ActionResult GetCompanies() {
                  List<CompanyDTO> companies = new List<CompanyDTO>();
                  foreach(var company in GetNonSecuredObjectSpace().GetObjects<Company>()) {
                      companies.Add(new CompanyDTO() { ID = company.ID, Name = company.Name });
                  }
                  return Ok(companies);
              }
              [HttpGet("/GetApplicationUsers({companyKey})")]
              public ActionResult GetApplicationUsers(string companyKey) {
                  var users = new List<ApplicationUserDTO>();
                  Guid guid = new Guid(companyKey);
                  foreach(var user in GetNonSecuredObjectSpace().GetObjectsQuery<ApplicationUser>().Where(user => user.Company != null && user.Company.ID == guid)) {
                      users.Add(new ApplicationUserDTO() { ID = user.ID, UserName = user.UserName });
                  }
      
                  return Ok(users);
              }
              private IObjectSpace GetNonSecuredObjectSpace() {
                  if(nonSecuredObjectSpace == null) {
                      nonSecuredObjectSpace = nonSecuredObjectSpaceFactory.CreateNonSecuredObjectSpace<Company>();
                  }
                  return nonSecuredObjectSpace;
              }
      
              public void Dispose() {
                  if(nonSecuredObjectSpace != null) {
                      nonSecuredObjectSpace.Dispose();
                      nonSecuredObjectSpace = null;
                  }
              }
          }
      }
      
    3. Create the ILogonDataProvider service that contains abstract data retrieval logic.

      namespace EFCoreCustomLogonAll.Module.Authentication;
      
      public interface ILogonDataProvider {
          IList<CompanyDTO> GetCompanies();
          IList<ApplicationUserDTO> GetCompanyUsers(Guid companyID);
      }
      
    4. Implement this interface in platform-specific classes: MiddleTierClientLogonDataProvider or BlazorLogonDataProvider.

      using DevExpress.ExpressApp.Security.ClientServer;
      using System.Collections.ObjectModel;
      using System.Net.Http.Json;
      using System.Text.Json;
      
      namespace EFCoreCustomLogonAll.Module.Authentication;
      
      public class MiddleTierClientLogonDataProvider : ILogonDataProvider {
          readonly WebApiSecuredDataServerClientBase dataServerClient;
          public MiddleTierClientLogonDataProvider(WebApiSecuredDataServerClientBase dataServerClient) {
              this.dataServerClient = dataServerClient;
              var t = dataServerClient.HttpClient;
          }
          public IList<CompanyDTO> GetCompanies() {
              var companies = GetAllAsync<CompanyDTO>(dataServerClient.HttpClient, "GetCompanies").GetAwaiter().GetResult();
              foreach(var company in companies) {
                  company.LogonDataProvider = this;
              }
              return new ReadOnlyCollection<CompanyDTO>(companies);
          }
      
          public IList<ApplicationUserDTO> GetCompanyUsers(Guid companyID) {
              var result = GetAllAsync<ApplicationUserDTO>(dataServerClient.HttpClient, $"GetApplicationUsers({companyID})").GetAwaiter().GetResult();
              return new ReadOnlyCollection<ApplicationUserDTO>(result);
          }
      
          async Task<T[]> GetAllAsync<T>(HttpClient httpClient, string requestUri) {
              var json = await RequestAsync(httpClient, new HttpRequestMessage(HttpMethod.Get, requestUri));
      
              return json.GetProperty("$values").Deserialize<T[]>()
                  ?? throw new NullReferenceException();
          }
          async Task<JsonElement> RequestAsync(HttpClient httpClient, HttpRequestMessage request, bool preventAuthorization = false) {
              request.Headers.Add("Accept", "application/json");
              using var httpResponse = Send(httpClient, request, preventAuthorization);
              if(httpResponse.StatusCode == System.Net.HttpStatusCode.NotFound) {
                  throw new HttpRequestException($"{request.Method} request has no JSON! Code {(int)httpResponse.StatusCode}, '{httpResponse.ReasonPhrase}'");
              }
              if(httpResponse.StatusCode == System.Net.HttpStatusCode.NoContent) {
                  return new JsonElement();
              } else {
                  return await httpResponse.Content.ReadFromJsonAsync<JsonElement>();
              }
          }
          HttpResponseMessage Send(HttpClient httpClient, HttpRequestMessage request, bool preventAuthorization = false) {
              var httpResponse = httpClient.SendAsync(request).GetAwaiter().GetResult();
              if(!httpResponse.IsSuccessStatusCode) {
                  using(var stream = httpResponse.Content.ReadAsStream()) {
                      using(StreamReader reader = new StreamReader(stream)) {
                          throw new HttpRequestException(httpResponse.ReasonPhrase + Environment.NewLine + reader.ReadToEnd(), null, httpResponse.StatusCode);
                      }
                  }
              }
              return httpResponse;
          }
      }
      
    5. Register platform-specific providers as services:

      public void ConfigureServices(IServiceCollection services) {
      // ...
          services.AddScoped<ILogonDataProvider, BlazorLogonDataProvider>();
      
      public void ConfigureServices(IServiceCollection services) {
      // ...
          services.AddScoped<ILogonDataProvider, MiddleTierClientLogonDataProvider>();
      
      public static WinApplication BuildApplication() {
      // ...
          var builder = WinApplication.CreateBuilder();
          // ...
          builder.Services.AddScoped<ILogonDataProvider, MiddleTierClientLogonDataProvider>();
      

    Customize Blazor Lookup Editors

    A Blazor lookup editor displays the Edit button. Create a controller and call the CustomizeViewItemControl method to hide the button from the Company and Application User lookup editors.

    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.Blazor.Editors;
    using EFCoreCustomLogonAll.Module.Authentication;
    
    namespace EFCoreCustomLogonAll.Blazor.Server.Authentication {
        // https://supportcenter.devexpress.com/ticket/details/t1164456/blazor-lookup-list-view-s-allowedit-property-does-not-affect-visibility-of
        public class CustomLogonParameterLookupActionVisibilityController : ObjectViewController<DetailView, CustomLogonParameters> {
            protected override void OnActivated() {
                base.OnActivated();
                View.CustomizeViewItemControl<LookupPropertyEditor>(this, e => {
                    e.HideEditButton();
                });
            }
        }
    }
    

    Login Forms

    Pass Custom Classes to the Security System

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

    Files: MySolution.Blazor.Server\Startup.cs, MySolution.MiddleTier\Startup.cs

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

    Add Demo Data

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

    using DevExpress.ExpressApp;
    using DevExpress.Data.Filtering;
    using DevExpress.Persistent.Base;
    using DevExpress.ExpressApp.Updating;
    using DevExpress.ExpressApp.Security;
    using DevExpress.ExpressApp.SystemModule;
    using DevExpress.ExpressApp.EF;
    using DevExpress.Persistent.BaseImpl.EF;
    using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
    using EFCoreCustomLogonAll.Module.BusinessObjects;
    using Microsoft.Extensions.DependencyInjection;
    
    namespace EFCoreCustomLogonAll.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();
            // ...
            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).
            // ...
        }
        // ...
    }
    

    Middle-Tier-Specific Steps

    Complete the following steps to configure an application with Middle-Tier security to work with custom login parameters.

    Register the CustomLogonParameters Class in Client Applications

    The CustomLogonParameters type must be explicitly registered in the client application as a known login parameter type. Call the AddKnownType(Type) method in the following files: MySolution.Blazor.Server/Program.cs (for Blazor) and MySolution.Win/Program.cs (for WinForms).

    public static int Main(string[] args) {
    // ...
        WebApiDataServerHelper.AddKnownType(typeof(CustomLogonParameters));
        // ...
    }
    

    Implement AuthenticationController and JwtTokenProviderService Classes

    Implement the following classes in Middle Tier server application:

    • The AuthenticationController class specifies the type for deserializing incoming JSON.

      using DevExpress.ExpressApp;
      using DevExpress.ExpressApp.Security;
      using DevExpress.ExpressApp.Security.Authentication.ClientServer;
      using EFCoreCustomLogonAll.Module.Authentication;
      using Microsoft.AspNetCore.Mvc;
      
      namespace EFCoreCustomLogonAll.WebApi.JWT;
      
      [ApiController]
      [Route("api/[controller]")]
      // This is a JWT authentication service sample.
      public class AuthenticationController : ControllerBase {
          readonly IAuthenticationTokenProvider tokenProvider;
          public AuthenticationController(IAuthenticationTokenProvider tokenProvider) {
              this.tokenProvider = tokenProvider;
          }
          [HttpPost("Authenticate")]
          public IActionResult Authenticate(
              [FromBody]
              CustomLogonParameters logonParameters
          ) {
              try {
                  return Ok(tokenProvider.Authenticate(logonParameters));
              } catch(AuthenticationException ex) {
                  return Unauthorized(ex.GetJson());
              }
          }
      }
      
    • The JwtTokenProviderService class allows you to customize logic that makes authentication decisions against the custom set of login parameters specified by a user.

    Add the following code to the OnCustomAuthenticate event handler in Startup.cs files of Blazor and WinForms Middle Tier clients:

    builder.Security
    // ...
        .UseMiddleTierMode(options => {
        // ...
            options.Events.OnCustomAuthenticate = (sender, security, args) => {
                args.Handled = true;
                HttpResponseMessage msg = args.HttpClient.PostAsJsonAsync("api/Authentication/Authenticate", (CustomLogonParameters)args.LogonParameters).GetAwaiter().GetResult();
                string token = (string)msg.Content.ReadFromJsonAsync(typeof(string)).GetAwaiter().GetResult();
                if(msg.StatusCode == HttpStatusCode.Unauthorized) {
                    XafExceptions.Authentication.ThrowAuthenticationFailedFromResponse(token);
                }
                msg.EnsureSuccessStatusCode();
                args.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
            };
    

    Run the Application

    Run the application to see custom parameters (Company and Application User lookup editors) in the login window.

    Note

    • When first running a Blazor application, it does not create a database or populate lookup editors. To create a database, either log in with an empty username or start the Middle Tier server.
    • If using Middle Tier security, start the Middle Tier server before launching the Blazor or Win Forms client.

    custom-logon-parameters-blazor-result

    See Also