expressappframework-404264-data-security-and-safety-security-system-authentication-customize-standard-authentication-behavior-and-supply-additional-logon-parameters-customize-authentication-behavior-net.md
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.
View Example: XAF - Customize Logon Parameters
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.
Add the second part of this relationship to the ApplicationUser class generated by the Template Kit.
Add the Company class to your application’s DbContext.
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.
To access lists of companies and application users in login form (before authentication), implement CompanyDTO and ApplicationUserDTO data transfer objects.
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.
Create the ILogonDataProvider service that contains abstract data retrieval logic.
Implement this interface in platform-specific classes: MiddleTierClientLogonDataProvider or BlazorLogonDataProvider.
Register platform-specific providers as services:
A Blazor lookup editor displays the Edit button. Create a controller and call the View.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();
});
}
}
}
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);
});
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).
// ...
}
// ...
}
Complete the following steps to configure an application with Middle-Tier security to work with custom login parameters.
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 the following classes in Middle Tier server application:
The AuthenticationController class specifies the type for deserializing incoming JSON.
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 to see custom parameters (Company and Application User lookup editors) in the login window.
Note
See Also
XAF Blazor UI: How to extend the logon form - register a new user, restore a password