XAF Blazor UI: How to extend the logon form: register a new user, restore a password
Note:
Instead of custom implementation, we recommend that you delegate these routine tasks to OAuth2 providers. Microsoft, Google, Azure, and GitHub services enable user and document management that's familiar to anyone who works with business apps. Your XAF application can easily integrate these OAuth2 providers into the logon form. You only need to add boilerplate code.
Refer to the following help topic for additional information: Active Directory and OAuth2 Authentication Providers in ASP.NET Core Blazor Applications.
This example contains a reusable Security.Extensions module that implements the capability to register a new user from the login form and the "Forgot Password" feature.
The module includes the following notable building blocks:
- Non-persistent data models for parameter screens (LogonActionParameters.cs).
- A View Controller (ManageUsersOnLogonController.cs) for the login Detail View. The controller declares custom Actions and their behavior. See the
CreateCustomLogonWindowControllers
event in Module.cs to find controller registration code and other service logic. - Services for restoring passwords (RestorePasswordService.cs) and registering new users (UserRegistrationService.cs).
- A custom login view for restoring a user's password (see the
Application_CreateCustomLogonAction
event handler in Module.cs).
Implementation Details
Perform the following steps to integrate this module in your project:
- Download the Security.Extensions module project and add it to your XAF solution. Reference the module project in your Blazor project and rebuild the solution.
See the following topic for details: How to: Add Projects to a Solution. - Add the
SecurityExtensionsModule
to your application:
File to review: DXApplication1.Blazor.Server/Startup.cs
In the previous code sample,C#public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddXaf(Configuration, builder => { builder.Modules .AddSecurityExtensions(options => options.CreateSecuritySystemUser = DXApplication1.Module.DatabaseUpdate.Updater.CreateUser) // ... // ... }); // ... } // ... }
Updater.CreateUser
is your custom method that matches the following definition:C#public delegate IAuthenticationStandardUser CreateSecuritySystemUser(IObjectSpace objectSpace, string userName, string email, string password, bool isAdministrator);
- Add the
Email
property to theApplicationUser
class:
File to review: ApplicationUser.csC#public class ApplicationUser : /*...*/ { // ... public virtual string Email { get; set; } }
Files to Review
- Updater.cs
- Startup.cs
- LogonActionCustomizationController.cs
- ManageUsersOnLogonController.cs
- RestorePasswordService.cs
- UserRegistrationService.cs
- ApplicationBuilderExtensions.cs
- LogonActionParameters.cs
- Module.cs
Documentation
- XafApplication.CreateCustomLogonWindowControllers
- Authentication System Architecture (Blazor)
- Active Directory and OAuth2 Authentication Providers in ASP.NET Core Blazor Applications
- Customize Standard Authentication Behavior and Supply Additional Logon Parameters (.NET Framework Applications)
More Examples
Does this example address your development requirements/objectives?
(you will be redirected to DevExpress.com to submit your response)
Note:
Instead of custom implementation, we recommend that you delegate these routine tasks to OAuth2 providers. Microsoft, Google, Azure, and GitHub services enable user and document management that's familiar to anyone who works with business apps. Your XAF application can easily integrate these OAuth2 providers into the logon form. You only need to add boilerplate code.
Refer to the following help topic for additional information: Active Directory and OAuth2 Authentication Providers in ASP.NET Core Blazor Applications.
This example contains a reusable Security.Extensions module that implements the capability to register a new user from the login form and the "Forgot Password" feature.
The module includes the following notable building blocks:
- Non-persistent data models for parameter screens (LogonActionParameters.cs).
- A View Controller (ManageUsersOnLogonController.cs) for the login Detail View. The controller declares custom Actions and their behavior. See the
CreateCustomLogonWindowControllers
event in Module.cs to find controller registration code and other service logic. - Services for restoring passwords (RestorePasswordService.cs) and registering new users (UserRegistrationService.cs).
- A custom login view for restoring a user's password (see the
Application_CreateCustomLogonAction
event handler in Module.cs).
Implementation Details
Perform the following steps to integrate this module in your project:
- Download the Security.Extensions module project and add it to your XAF solution. Reference the module project in your Blazor project and rebuild the solution.
See the following topic for details: How to: Add Projects to a Solution. - Add the
SecurityExtensionsModule
to your application:
File to review: DXApplication1.Blazor.Server/Startup.cs
In the previous code sample,C#public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddXaf(Configuration, builder => { builder.Modules .AddSecurityExtensions(options => options.CreateSecuritySystemUser = DXApplication1.Module.DatabaseUpdate.Updater.CreateUser) // ... // ... }); // ... } // ... }
Updater.CreateUser
is your custom method that matches the following definition:C#public delegate IAuthenticationStandardUser CreateSecuritySystemUser(IObjectSpace objectSpace, string userName, string email, string password, bool isAdministrator);
- Add the
Email
property to theApplicationUser
class:
File to review: ApplicationUser.csC#public class ApplicationUser : /*...*/ { // ... public virtual string Email { get; set; } }
Files to Review
- Updater.cs
- Startup.cs
- LogonActionCustomizationController.cs
- ManageUsersOnLogonController.cs
- RestorePasswordService.cs
- UserRegistrationService.cs
- ApplicationBuilderExtensions.cs
- LogonActionParameters.cs
- Module.cs
Documentation
- XafApplication.CreateCustomLogonWindowControllers
- Authentication System Architecture (Blazor)
- Active Directory and OAuth2 Authentication Providers in ASP.NET Core Blazor Applications
- Customize Standard Authentication Behavior and Supply Additional Logon Parameters (.NET Framework Applications)
More Examples
Does this example address your development requirements/objectives?
(you will be redirected to DevExpress.com to submit your response)
Example Code
C#using DevExpress.ExpressApp.DC;
using DevExpress.ExpressApp.Model;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.Validation;
using System.ComponentModel;
namespace Security.Extensions;
[DomainComponent]
public abstract class LogonActionParametersBase {
public const string ValidationContext = "CustomLogonActionsContext";
public const string EmailPattern = @"^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$";
}
[DomainComponent]
[ModelDefault("Caption", "Register User")]
[ImageName("BO_User")]
public class RegisterUserParameters : LogonActionParametersBase {
[RuleRequiredField(null, ValidationContext)]
public string UserName { get; set; }
[RuleRequiredField(null, ValidationContext)]
[RuleRegularExpression(null, ValidationContext, EmailPattern)]
public string Email { get; set; }
[ModelDefault("IsPassword", "True")]
[RuleRequiredField(null, ValidationContext)]
public string Password { get; set; }
}
[DomainComponent]
[ModelDefault("Caption", "Restore Password")]
[ImageName("Action_ResetPassword")]
public class RestorePasswordParameters : LogonActionParametersBase {
[RuleRequiredField(null, ValidationContext)]
[RuleRegularExpression(null, ValidationContext, EmailPattern)]
public string Email { get; set; }
}
[DomainComponent]
[ModelDefault("Caption", "Set New Password")]
[ImageName("Action_ResetPassword")]
public class SetNewPasswordParameters : LogonActionParametersBase {
[ModelDefault("IsPassword", "True")]
[RuleRequiredField(null, ValidationContext)]
public string Password { get; set; }
[ModelDefault("IsPassword", "True")]
[RuleRequiredField(null, ValidationContext)]
[RuleValueComparison(null, ValidationContext, ValueComparisonType.Equals, nameof(Password),
ParametersMode.Expression, CustomMessageTemplate = "Passwords are different.")]
public string ConfirmPassword { get; set; }
[Browsable(false)]
public string Token { get; set; }
}
C#using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Actions;
using DevExpress.ExpressApp.Templates;
using DevExpress.Persistent.Base;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Security.Extensions.Services;
using System;
namespace Security.Extensions.Controllers;
// A controller that adds the "Register"/"Restore password" buttons to the login page.
public class ManageUsersOnLogonController : ViewController<DetailView> {
private const string LogonActionParametersActiveKey = "Active for LogonActionParameters only";
public SimpleAction RegisterUserAction { get; }
public SimpleAction RestorePasswordAction { get; }
public SimpleAction CancelAction { get; }
public SimpleAction AcceptLogonParametersAction { get; }
public ManageUsersOnLogonController() {
RegisterUserAction = new SimpleAction(this, "RegisterUser", PredefinedCategory.PopupActions) {
Caption = "Register User",
ToolTip = "Register",
ImageName = "BO_User",
PaintStyle = ActionItemPaintStyle.Image,
};
RegisterUserAction.Execute += (s, e) => ShowLogonActionView(typeof(RegisterUserParameters));
RestorePasswordAction = new SimpleAction(this, "RestorePassword", PredefinedCategory.PopupActions) {
Caption = "Restore Password",
ToolTip = "Restore Password",
ImageName = "Action_ResetPassword",
PaintStyle = ActionItemPaintStyle.Image,
};
RestorePasswordAction.Execute += (s, e) => ShowLogonActionView(typeof(RestorePasswordParameters));
AcceptLogonParametersAction = new SimpleAction(this, "AcceptLogonParameters", PredefinedCategory.PopupActions) {
Caption = "OK"
};
AcceptLogonParametersAction.Execute += (s, e) => AcceptParameters(e.CurrentObject as LogonActionParametersBase);
CancelAction = new SimpleAction(this, "CancelLogonParameters", PredefinedCategory.PopupActions) {
Caption = "Cancel"
};
CancelAction.Execute += (s, e) => ReloadPage();
}
// Ensures that this controller is active only when a user is not logged on.
protected override void OnFrameAssigned() {
base.OnFrameAssigned();
Active[ControllerActiveKey] = !Application.Security.IsAuthenticated;
}
// Manages the activity of Actions within the logon window depending on the current context.
protected override void OnViewControlsCreated() {
base.OnViewControlsCreated();
// Manage the state of own Actions as well as dialog Actions of the LogonController class within the same logon Frame.
bool isRegisterUserOrRestorePasswordView = View?.ObjectTypeInfo?.Implements<LogonActionParametersBase>() ?? false;
LogonController logonController = Frame.GetController<LogonController>();
if (logonController != null) {
logonController.AcceptAction.Active[LogonActionParametersActiveKey] = !isRegisterUserOrRestorePasswordView;
logonController.CancelAction.Active[LogonActionParametersActiveKey] = !isRegisterUserOrRestorePasswordView;
}
AcceptLogonParametersAction.Active[LogonActionParametersActiveKey] = isRegisterUserOrRestorePasswordView;
CancelAction.Active[LogonActionParametersActiveKey] = isRegisterUserOrRestorePasswordView;
RegisterUserAction.Active[LogonActionParametersActiveKey] = !isRegisterUserOrRestorePasswordView;
RestorePasswordAction.Active[LogonActionParametersActiveKey] = !isRegisterUserOrRestorePasswordView;
}
// Configures a View used to display our parameters objects.
private void ShowLogonActionView(Type logonActionParametersType) {
ArgumentNullException.ThrowIfNull(logonActionParametersType);
var objectSpace = Application.CreateObjectSpace(logonActionParametersType);
var logonActionParameters = objectSpace.CreateObject(logonActionParametersType);
var detailView = Application.CreateDetailView(objectSpace, logonActionParameters);
Frame.SetView(detailView);
}
private void AcceptParameters(LogonActionParametersBase logonActionParameters) {
ArgumentNullException.ThrowIfNull(logonActionParameters);
if (logonActionParameters is RegisterUserParameters registerUserParameters) {
RegisterUser(registerUserParameters);
}
else if (logonActionParameters is RestorePasswordParameters restorePasswordParameters) {
EmailRestorePasswordDetails(restorePasswordParameters);
}
else if (logonActionParameters is SetNewPasswordParameters setNewPasswordParameters) {
SetNewPassword(setNewPasswordParameters);
}
}
private void RegisterUser(RegisterUserParameters parameters) {
var userRegistrationService = Application.ServiceProvider.GetRequiredService<UserRegistrationService>();
userRegistrationService.RegisterNewUserAndLogin(parameters.UserName, parameters.Email, parameters.Password, isAdministrator: false);
}
private void EmailRestorePasswordDetails(RestorePasswordParameters parameters) {
var restorePasswordService = Application.ServiceProvider.GetRequiredService<RestorePasswordService>();
string restorePasswordUrl = restorePasswordService.GenerateRestorePasswordUrl(parameters.Email);
// Send an email with the login details here.
// Refer to https://learn.microsoft.com/en-us/dotnet/api/system.net.mail.mailmessage for more details.
#if DEBUG
// Display a notification with the reset password URL, close automatically after 5 minutes
Application.ShowViewStrategy.ShowMessage(
$"""
(Debug mode) Follow the link below to open the password reset form:
{restorePasswordUrl}
""",
InformationType.Info, displayInterval: 300_000);
#endif
}
private void SetNewPassword(SetNewPasswordParameters setNewPasswordParameters) {
try {
var restorePasswordService = Application.ServiceProvider.GetRequiredService<RestorePasswordService>();
restorePasswordService.SetNewPassword(setNewPasswordParameters.Token, setNewPasswordParameters.Password);
ReloadPage();
}
catch {
throw new UserFriendlyException("The password reset link is invalid or has expired.");
}
}
private void ReloadPage() {
var navigationManager = Application.ServiceProvider.GetRequiredService<NavigationManager>();
navigationManager.NavigateTo("LoginPage", forceLoad: true);
}
}
C#using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Actions;
using DevExpress.Persistent.Base.Security;
using Microsoft.Extensions.DependencyInjection;
using Security.Extensions.Controllers;
using Security.Extensions.Services;
using System.Collections.Generic;
namespace Security.Extensions;
// A delegate that, when invoked, creates and returns a new user with the specified user name, password and email address.
public delegate IAuthenticationStandardUser CreateSecuritySystemUser(IObjectSpace objectSpace, string userName, string email, string password, bool isAdministrator);
public sealed class SecurityExtensionsModule : ModuleBase {
public SecurityExtensionsModule() {
RequiredModuleTypes.Add(typeof(DevExpress.ExpressApp.Security.SecurityModule));
RequiredModuleTypes.Add(typeof(DevExpress.ExpressApp.Validation.ValidationModule));
}
public override void Setup(XafApplication application) {
base.Setup(application);
application.CreateCustomLogonWindowControllers += application_CreateCustomLogonWindowControllers;
application.CreateCustomLogonAction += Application_CreateCustomLogonAction;
}
private void application_CreateCustomLogonWindowControllers(object sender, CreateCustomLogonWindowControllersEventArgs e) {
var application = (XafApplication)sender;
e.Controllers.Add(application.CreateController<ManageUsersOnLogonController>());
e.Controllers.Add(application.CreateController<LogonActionCustomizationController>());
}
private void Application_CreateCustomLogonAction(object sender, CreateCustomLogonActionEventArgs e) {
var restorePasswordService = Application.ServiceProvider.GetRequiredService<RestorePasswordService>();
var restorePasswordToken = restorePasswordService.GetRestorePasswordTokenFromUrl();
if (!string.IsNullOrEmpty(restorePasswordToken)) {
var logonWindowControllers = e.CreateLogonWindowControllers();
e.LogonAction = CreateRestorePasswordLogonAction(restorePasswordToken, logonWindowControllers);
}
}
private PopupWindowShowAction CreateRestorePasswordLogonAction(string restorePasswordToken, List<Controller> logonWindowControllers) {
var restorePasswordLogonAction = new PopupWindowShowAction();
restorePasswordLogonAction.Application = Application;
restorePasswordLogonAction.CustomizePopupWindowParams += (s, e) => {
var objectSpace = Application.CreateObjectSpace<SetNewPasswordParameters>();
var newPasswordParameters = objectSpace.CreateObject<SetNewPasswordParameters>();
newPasswordParameters.Token = restorePasswordToken;
e.View = Application.CreateDetailView(objectSpace, newPasswordParameters);
e.DialogController = Application.CreateController<LogonController>();
e.DialogController.Controllers.AddRange(logonWindowControllers);
};
return restorePasswordLogonAction;
}
}
C#using DevExpress.Data.Filtering;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Options;
using System;
using System.Linq;
using System.Text.Json;
using System.Web;
namespace Security.Extensions.Services;
file class RestorePasswordToken {
public string UserId { get; set; }
}
internal class RestorePasswordService {
private readonly Type securityUserType;
private readonly INonSecuredObjectSpaceFactory nonSecuredObjectSpaceFactory;
private readonly NavigationManager navigationManager;
private readonly ITimeLimitedDataProtector dataProtector;
public const string LoginProviderName = "RestorePasswordToken";
public RestorePasswordService(
IOptions<SecurityOptions> securityOptions,
INonSecuredObjectSpaceFactory nonSecuredObjectSpaceFactory,
NavigationManager navigationManager,
IDataProtectionProvider dataProtectionProvider) {
securityUserType = securityOptions.Value.UserType;
this.nonSecuredObjectSpaceFactory = nonSecuredObjectSpaceFactory;
this.navigationManager = navigationManager;
dataProtector = dataProtectionProvider.CreateProtector("RestorePassword").ToTimeLimitedDataProtector();
}
public string GenerateRestorePasswordUrl(string userEmail) {
if (string.IsNullOrWhiteSpace(userEmail)) {
throw new UserFriendlyException("Email address is not specified.");
}
using var nonSecuredObjectSpace = nonSecuredObjectSpaceFactory.CreateNonSecuredObjectSpace(securityUserType);
var user = nonSecuredObjectSpace.FindObject(securityUserType, CriteriaOperator.Parse("Email = ?", userEmail)) as ISecurityUserWithLoginInfo;
if (user == null) {
throw new UserFriendlyException("Cannot find a user with the specified email address.");
}
var token = new RestorePasswordToken() {
UserId = nonSecuredObjectSpace.GetKeyValueAsString(user)
};
var serializedToken = JsonSerializer.Serialize(token);
var serializedProtectedToken = dataProtector.Protect(serializedToken, lifetime: TimeSpan.FromHours(1));
// store the token in the database, replacing a previously issued token (if any)
if (user.UserLogins.FirstOrDefault(info => info.LoginProviderName == LoginProviderName) is { } restorePasswordInfo) {
nonSecuredObjectSpace.Delete(restorePasswordInfo);
}
user.CreateUserLoginInfo(LoginProviderName, serializedProtectedToken);
nonSecuredObjectSpace.CommitChanges();
return $"{navigationManager.BaseUri}LoginPage?restorePasswordToken={serializedProtectedToken}";
}
public void SetNewPassword(string restorePasswordToken, string newPassword) {
var serializedUnprotectedToken = dataProtector.Unprotect(restorePasswordToken);
var token = JsonSerializer.Deserialize<RestorePasswordToken>(serializedUnprotectedToken);
using var nonSecuredObjectSpace = nonSecuredObjectSpaceFactory.CreateNonSecuredObjectSpace(securityUserType);
var userKey = nonSecuredObjectSpace.GetObjectKey(securityUserType, token.UserId);
var user = (ISecurityUserWithLoginInfo)nonSecuredObjectSpace.GetObjectByKey(securityUserType, userKey);
var restorePasswordInfo = user.UserLogins.FirstOrDefault(info => info.LoginProviderName == LoginProviderName);
// the processed token must match the token stored in the database
if (restorePasswordInfo?.ProviderUserKey != restorePasswordToken) {
throw new InvalidOperationException();
}
nonSecuredObjectSpace.Delete(restorePasswordInfo);
((IAuthenticationStandardUser)user).SetPassword(newPassword);
nonSecuredObjectSpace.CommitChanges();
}
public string GetRestorePasswordTokenFromUrl() {
var query = HttpUtility.ParseQueryString(new Uri(navigationManager.Uri).Query);
return query["restorePasswordToken"];
}
}
C#using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Blazor.Services;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.Base.Security;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
using System;
namespace Security.Extensions.Services;
internal class UserRegistrationService {
private readonly UserManager userManager;
private readonly Type securityUserType;
private readonly SecurityExtensionsOptions moduleOptions;
private readonly INonSecuredObjectSpaceFactory nonSecuredObjectSpaceFactory;
private readonly NavigationManager navigationManager;
public UserRegistrationService(
UserManager userManager,
SignInManager signInManager,
IOptions<SecurityOptions> securityOptions,
IOptions<SecurityExtensionsOptions> moduleOptions,
INonSecuredObjectSpaceFactory nonSecuredObjectSpaceFactory,
NavigationManager navigationManager) {
this.userManager = userManager;
this.securityUserType = securityOptions.Value.UserType;
this.moduleOptions = moduleOptions.Value;
this.nonSecuredObjectSpaceFactory = nonSecuredObjectSpaceFactory;
this.navigationManager = navigationManager;
}
public void RegisterNewUserAndLogin(string userName, string email, string password, bool isAdministrator) {
var newUser = RegisterNewUser(userName, email, password, isAdministrator);
var authToken = userManager.GetAuthenticationToken((ISecurityUserWithLoginInfo)newUser, expirationSeconds: 15);
var loginUrl = $"{SignInMiddlewareDefaults.SignInEndpointName}?token={authToken}";
navigationManager.NavigateTo(loginUrl, true);
}
public IAuthenticationStandardUser RegisterNewUser(string userName, string email, string password, bool isAdministrator) {
using var nonSecuredObjectSpace = nonSecuredObjectSpaceFactory.CreateNonSecuredObjectSpace(securityUserType);
var newUser = moduleOptions.CreateSecuritySystemUser(nonSecuredObjectSpace, userName, email, password, isAdministrator);
return newUser;
}
}
C#using DevExpress.ExpressApp.Security;
using DevExpress.ExpressApp.ApplicationBuilder;
using DevExpress.ExpressApp.Blazor.ApplicationBuilder;
using DevExpress.ExpressApp.Blazor.Services;
using DevExpress.Persistent.Base;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.EntityFrameworkCore;
using DXApplication1.Blazor.Server.Services;
using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
namespace DXApplication1.Blazor.Server;
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services) {
services.AddSingleton(typeof(Microsoft.AspNetCore.SignalR.HubConnectionHandler<>), typeof(ProxyHubConnectionHandler<>));
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHttpContextAccessor();
services.AddScoped<CircuitHandler, CircuitHandlerProxy>();
services.AddXaf(Configuration, builder => {
builder.UseApplication<DXApplication1BlazorApplication>();
builder.Modules
.AddConditionalAppearance()
.AddValidation(options => {
options.AllowValidationDetailsAccess = false;
})
.AddSecurityExtensions(options => options.CreateSecuritySystemUser = DXApplication1.Module.DatabaseUpdate.Updater.CreateUser)
.Add<DXApplication1.Module.DXApplication1Module>()
.Add<DXApplication1BlazorModule>();
builder.ObjectSpaceProviders
.AddSecuredEFCore(options => options.PreFetchReferenceProperties())
.WithDbContext<DXApplication1.Module.BusinessObjects.DXApplication1EFCoreDbContext>((serviceProvider, options) => {
// Uncomment this code to use an in-memory database. This database is recreated each time the server starts. With the in-memory database, you don't need to make a migration when the data model is changed.
// Do not use this code in production environment to avoid data loss.
// We recommend that you refer to the following help topic before you use an in-memory database: https://docs.microsoft.com/en-us/ef/core/testing/in-memory
//options.UseInMemoryDatabase("InMemory");
string connectionString = null;
if(Configuration.GetConnectionString("ConnectionString") != null) {
connectionString = Configuration.GetConnectionString("ConnectionString");
}
#if EASYTEST
if(Configuration.GetConnectionString("EasyTestConnectionString") != null) {
connectionString = Configuration.GetConnectionString("EasyTestConnectionString");
}
#endif
ArgumentNullException.ThrowIfNull(connectionString);
options.UseSqlServer(connectionString);
options.UseChangeTrackingProxies();
options.UseObjectSpaceLinkProxies();
options.UseLazyLoadingProxies();
})
.AddNonPersistent();
builder.Security
.UseIntegratedMode(options => {
options.Lockout.Enabled = true;
options.RoleType = typeof(PermissionPolicyRole);
// ApplicationUser descends from PermissionPolicyUser and supports the OAuth authentication. For more information, refer to the following topic: https://docs.devexpress.com/eXpressAppFramework/402197
// If your application uses PermissionPolicyUser or a custom user type, set the UserType property as follows:
options.UserType = typeof(DXApplication1.Module.BusinessObjects.ApplicationUser);
// ApplicationUserLoginInfo is only necessary for applications that use the ApplicationUser user type.
// If you use PermissionPolicyUser or a custom user type, comment out the following line:
options.UserLoginInfoType = typeof(DXApplication1.Module.BusinessObjects.ApplicationUserLoginInfo);
options.Events.OnSecurityStrategyCreated += securityStrategy => {
// Use the 'PermissionsReloadMode.NoCache' option to load the most recent permissions from the database once
// for every DbContext instance when secured data is accessed through this instance for the first time.
// Use the 'PermissionsReloadMode.CacheOnFirstAccess' option to reduce the number of database queries.
// In this case, permission requests are loaded and cached when secured data is accessed for the first time
// and used until the current user logs out.
// See the following article for more details: https://docs.devexpress.com/eXpressAppFramework/DevExpress.ExpressApp.Security.SecurityStrategy.PermissionsReloadMode.
((SecurityStrategy)securityStrategy).PermissionsReloadMode = PermissionsReloadMode.NoCache;
};
})
.AddPasswordAuthentication(options => {
options.IsSupportChangePassword = true;
});
});
var authentication = services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
authentication.AddCookie(options => {
options.LoginPath = "/LoginPage";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if(env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
else {
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. To change this for production scenarios, see: https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRequestLocalization();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseXaf();
app.UseEndpoints(endpoints => {
endpoints.MapXafEndpoints();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
endpoints.MapControllers();
});
}
}
C#using System.Collections.ObjectModel;
using System.ComponentModel;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
namespace DXApplication1.Module.BusinessObjects;
[DefaultProperty(nameof(UserName))]
public class ApplicationUser : PermissionPolicyUser, ISecurityUserWithLoginInfo, ISecurityUserLockout {
public virtual string Email { get; set; }
[Browsable(false)]
public virtual int AccessFailedCount { get; set; }
[Browsable(false)]
public virtual DateTime LockoutEnd { get; set; }
[Browsable(false)]
[DevExpress.ExpressApp.DC.Aggregated]
public virtual IList<ApplicationUserLoginInfo> UserLogins { get; set; } = new ObservableCollection<ApplicationUserLoginInfo>();
IEnumerable<ISecurityUserLoginInfo> IOAuthSecurityUser.UserLogins => UserLogins.OfType<ISecurityUserLoginInfo>();
ISecurityUserLoginInfo ISecurityUserWithLoginInfo.CreateUserLoginInfo(string loginProviderName, string providerUserKey) {
ApplicationUserLoginInfo result = ((IObjectSpaceLink)this).ObjectSpace.CreateObject<ApplicationUserLoginInfo>();
result.LoginProviderName = loginProviderName;
result.ProviderUserKey = providerUserKey;
result.User = this;
return result;
}
}
C#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 DXApplication1.Module.BusinessObjects;
using Microsoft.Extensions.DependencyInjection;
using DevExpress.Persistent.Base.Security;
using DevExpress.Persistent.Validation;
using System.Security.AccessControl;
namespace DXApplication1.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 static IAuthenticationStandardUser CreateUser(IObjectSpace objectSpace, string userName, string email, string password, bool isAdministrator) {
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(email)) {
throw new UserFriendlyException("User Name and Email address are not specified!");
}
var userManager = objectSpace.ServiceProvider.GetRequiredService<UserManager>();
if (userManager.FindUserByName<ApplicationUser>(objectSpace, userName) != null) {
throw new UserFriendlyException("A user already exists with this name.");
}
if (objectSpace.FirstOrDefault<ApplicationUser>(user => user.Email == email) != null) {
throw new UserFriendlyException("A user already exists with this email.");
}
var role = isAdministrator ? GetAdminRole(objectSpace) : GetDefaultRole(objectSpace);
var result = userManager.CreateUser<ApplicationUser>(objectSpace, userName, password, (user) => {
user.Email = email;
user.Roles.Add(role);
});
if (!result.Succeeded) {
throw new UserFriendlyException("Error creating a new user.");
}
return result.User;
}
public override void UpdateDatabaseAfterUpdateSchema() {
base.UpdateDatabaseAfterUpdateSchema();
// The code below creates users and roles for testing purposes only.
// In production code, you can create users and assign roles to them automatically, as described in the following help topic:
// https://docs.devexpress.com/eXpressAppFramework/119064/data-security-and-safety/security-system/authentication
#if !RELEASE
// If a role doesn't exist in the database, create this role
var defaultRole = GetDefaultRole(ObjectSpace);
var adminRole = GetAdminRole(ObjectSpace);
ObjectSpace.CommitChanges(); //This line persists created object(s).
string EmptyPassword = "";
var userManager = ObjectSpace.ServiceProvider.GetRequiredService<UserManager>();
if (userManager.FindUserByName<ApplicationUser>(ObjectSpace, "User") == null) {
CreateUser(ObjectSpace, "User", "user@example.com", EmptyPassword, isAdministrator: false);
}
if (userManager.FindUserByName<ApplicationUser>(ObjectSpace, "Admin") == null) {
CreateUser(ObjectSpace, "Admin", "admin@example.com", EmptyPassword, isAdministrator: true);
}
ObjectSpace.CommitChanges(); //This line persists created object(s).
#endif
}
public override void UpdateDatabaseBeforeUpdateSchema() {
base.UpdateDatabaseBeforeUpdateSchema();
}
private static PermissionPolicyRole GetAdminRole(IObjectSpace objectSpace) {
PermissionPolicyRole adminRole = objectSpace.FirstOrDefault<PermissionPolicyRole>(r => r.Name == "Administrators");
if(adminRole == null) {
adminRole = objectSpace.CreateObject<PermissionPolicyRole>();
adminRole.Name = "Administrators";
adminRole.IsAdministrative = true;
}
return adminRole;
}
private static PermissionPolicyRole GetDefaultRole(IObjectSpace objectSpace) {
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.ID == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
defaultRole.AddNavigationPermission(@"Application/NavigationItems/Items/Default/Items/MyDetails", SecurityPermissionState.Allow);
defaultRole.AddMemberPermissionFromLambda<ApplicationUser>(SecurityOperations.Write, "ChangePasswordOnFirstLogon", cm => cm.ID == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
defaultRole.AddMemberPermissionFromLambda<ApplicationUser>(SecurityOperations.Write, "StoredPassword", cm => cm.ID == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<PermissionPolicyRole>(SecurityOperations.Read, SecurityPermissionState.Deny);
defaultRole.AddObjectPermission<ModelDifference>(SecurityOperations.ReadWriteAccess, "UserId = ToStr(CurrentUserId())", SecurityPermissionState.Allow);
defaultRole.AddObjectPermission<ModelDifferenceAspect>(SecurityOperations.ReadWriteAccess, "Owner.UserId = ToStr(CurrentUserId())", SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<ModelDifference>(SecurityOperations.Create, SecurityPermissionState.Allow);
defaultRole.AddTypePermissionsRecursively<ModelDifferenceAspect>(SecurityOperations.Create, SecurityPermissionState.Allow);
}
return defaultRole;
}
}
C#using DevExpress.Blazor;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Blazor.Templates.Toolbar.ActionControls;
using DevExpress.ExpressApp.SystemModule;
namespace Security.Extensions.Controllers;
// A controller that highlights the accept button ("OK") and allows submitting the form by pressing the Enter key.
public class LogonActionCustomizationController : WindowController {
private ActionControlsSiteController actionControlsSiteController;
private void ActionControlsSiteController_CustomizeActionControl(object sender, ActionControlEventArgs e) {
if (e.ActionControl.ActionId is "AcceptLogonParameters" && e.ActionControl is DxToolbarItemActionControlBase toolbarItemAction) {
toolbarItemAction.ToolbarItemModel.RenderStyle = ButtonRenderStyle.Primary;
}
}
protected override void OnFrameAssigned() {
base.OnFrameAssigned();
Active[ControllerActiveKey] = !Application.Security.IsAuthenticated;
}
protected override void OnActivated() {
base.OnActivated();
actionControlsSiteController = Frame.GetController<ActionControlsSiteController>();
if (actionControlsSiteController is not null) {
actionControlsSiteController.CustomizeActionControl += ActionControlsSiteController_CustomizeActionControl;
}
}
protected override void OnDeactivated() {
base.OnDeactivated();
if (actionControlsSiteController is not null) {
actionControlsSiteController.CustomizeActionControl -= ActionControlsSiteController_CustomizeActionControl;
actionControlsSiteController = null;
}
}
}
C#using DevExpress.ExpressApp.ApplicationBuilder;
using Microsoft.Extensions.DependencyInjection;
using Security.Extensions;
using Security.Extensions.Services;
using System;
namespace Security.Extensions {
public class SecurityExtensionsOptions {
public CreateSecuritySystemUser CreateSecuritySystemUser { get; set; }
}
}
namespace DevExpress.ExpressApp.Blazor.ApplicationBuilder {
public static class ApplicationBuilderExtensions {
// Adds the SecurityExtensionsModule to the application and configures the required services.
public static IModuleBuilder<IBlazorApplicationBuilder> AddSecurityExtensions(this IModuleBuilder<IBlazorApplicationBuilder> builder,
Action<SecurityExtensionsOptions> configureOptions) {
SecurityExtensionsOptions options = new();
configureOptions.Invoke(options);
ArgumentNullException.ThrowIfNull(options.CreateSecuritySystemUser);
builder.Add<SecurityExtensionsModule>();
builder.Context.Services.Configure<SecurityExtensionsOptions>(o => o.CreateSecuritySystemUser = options.CreateSecuritySystemUser);
builder.Context.Services.AddScoped<RestorePasswordService>();
builder.Context.Services.AddScoped<UserRegistrationService>();
return builder;
}
}
}