windowsforms-405147-data-access-security-connect-to-a-secure-middle-tier-api-server.md
A middle-tier architecture combines all data access and related logic in one place, adding an essential layer of security as it can manage authentication, authorization, and encryption. With this additional layer of protection, desktop UI clients cannot access database connection information or modify database tables directly. This separation simplifies client application development and reduces code duplication, as the same logic can be reused by multiple UI clients, making the ‘system’ easier to maintain and scale.
In a Middle Tier Server architecture, DbContext and other EF Core CRUD APIs are still used in the UI client app code, which interacts with the server remotely. Only the server has direct access to the database. Before passing data to the client’s DbContext, the server enforces security measures such as authentication, authorization, and data validation.
Entity Framework Core (EF Core)
Middle Tier Server (Data Access and Business Logic Layer)
WinForms UI Client
Our sample Windows Forms application demonstrates the following:
View Example: Middle Tier (EF Core)
The RemoteContextUtils class implements utility APIs related to remote DbContext and secured communication within client applications. The CreateSecuredClient method securely connects to an EF Core Middle Tier Security application:
public static WebApiSecuredDataServerClient CreateSecuredClient(string url, string login, string password) {
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(url);
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
var securedClient = new WebSocketSecuredDataServerClient(httpClient, XafTypesInfo.Instance);
securedClient.CustomAuthenticate += (sender, arguments) => {
arguments.Handled = true;
HttpResponseMessage msg = arguments.HttpClient.PostAsJsonAsync("api/Authentication/Authenticate", (AuthenticationStandardLogonParameters)arguments.LogonParameters).GetAwaiter().GetResult();
string token = (string)msg.Content.ReadFromJsonAsync(typeof(string)).GetAwaiter().GetResult();
if(msg.StatusCode == HttpStatusCode.Unauthorized) {
throw new UserFriendlyException(token);
}
msg.EnsureSuccessStatusCode();
arguments.HttpClient.DefaultRequestHeaders.Authorization
= new AuthenticationHeaderValue("bearer", token);
};
securedClient.Authenticate(new AuthenticationStandardLogonParameters(login, password));
((IMiddleTierServerSecurity)securedClient).Logon();
return securedClient;
}
The RemoteContextUtils.GetDBContext() method returns a secured DbContext for CRUD operations:
public static DXApplication1EFCoreDbContext GetDBContext() {
return new DXApplication1EFCoreDbContext(Options);
}
public static DbContextOptions<DXApplication1EFCoreDbContext> CreateDbContextOptions(WebApiSecuredDataServerClient securedClient) {
var builder = new DbContextOptionsBuilder<DXApplication1EFCoreDbContext>();
return builder
.UseLazyLoadingProxies()
.UseChangeTrackingProxies()
.UseMiddleTier(securedClient)
.Options as DbContextOptions<DXApplication1EFCoreDbContext>;
}
The following code example creates Admin and User roles and defines these permissions:
Admin: Read/Write (full access)
User: Read Only (data editing is not allowed)
public class Updater : ModuleUpdater {
private const string AdministratorUserName = "Admin";
private const string AdministratorRoleName = "Administrators";
private const string DefaultUserName = "User";
private const string DefaultUserRoleName = "Users";
//...
private PermissionPolicyRole GetUserRole() {
PermissionPolicyRole userRole = ObjectSpace.FirstOrDefault<PermissionPolicyRole>(u => u.Name == DefaultUserRoleName);
if(userRole == null) {
userRole = ObjectSpace.CreateObject<PermissionPolicyRole>();
userRole.Name = DefaultUserRoleName;
userRole.AddTypePermission<Employee>(SecurityOperations.Read, SecurityPermissionState.Allow);
userRole.AddTypePermission<Employee>(SecurityOperations.Write, SecurityPermissionState.Deny);
}
return userRole;
}
private PermissionPolicyRole GetAdminRole() {
PermissionPolicyRole adminRole = ObjectSpace.FirstOrDefault<PermissionPolicyRole>(u => u.Name == AdministratorRoleName);
if(adminRole == null) {
adminRole = ObjectSpace.CreateObject<PermissionPolicyRole>();
adminRole.Name = AdministratorRoleName;
adminRole.IsAdministrative = true;
}
return adminRole;
}
//...
}
The LogIn form attempts to log the user into the security system and returns DialogResult.OK if a login was successful:
using(AuthForm authForm = new AuthForm()) {
if(authForm.ShowDialog() == DialogResult.OK) {
MiddleTierStartupHelper.WaitMiddleTierServerReady(MiddleTierStartupHelper.EFCoreWebApiMiddleTierInstanceKey, TimeSpan.MaxValue);
// User authorization.
var securedClient = RemoteContextUtils.CreateSecuredClient(System.Configuration.ConfigurationManager.AppSettings["endpointUrl"], authForm.Login, authForm.Password);
RemoteContextUtils.SecuredDataServerClient = securedClient;
DbContextOptions<DXApplication1EFCoreDbContext> options = RemoteContextUtils.CreateDbContextOptions(securedClient);
RemoteContextUtils.Options = options;
Application.Run(new MainForm());
}
else
break;
}
If user authentication was successful, the SetUpBinding method creates a DbContext and binds the grid to data:
public partial class MainForm : RibbonForm {
EntityServerModeSource serverModeSource = new EntityServerModeSource();
DXApplication1EFCoreDbContext dbContext = null;
public MainForm() {
InitializeComponent();
SetUpBinding();
//...
}
void SetUpBinding() {
dbContext?.Dispose();
dbContext = RemoteContextUtils.GetDBContext();
serverModeSource = new EntityServerModeSource() { ElementType = typeof(Employee), KeyExpression = "ID" };
serverModeSource.QueryableSource = dbContext.Employees;
gridControl.DataSource = serverModeSource;
}
}
Highlighted lines enable/disable Ribbon commands (New, Edit, Delete) based on access permissions of the current (logged in) user:
public MainForm() {
InitializeComponent();
SetUpBinding();
bbiNew.Enabled = RemoteContextUtils.IsGranted(typeof(Employee), SecurityOperations.Create);
bbiDelete.Enabled = RemoteContextUtils.IsGranted(typeof(Employee), SecurityOperations.Delete);
bbiEdit.Enabled = RemoteContextUtils.IsGranted(typeof(Employee), SecurityOperations.Write);
}
View Example: Middle Tier (EF Core)
Important
These deployment recommendations do not apply to all possible configurations and should not be considered comprehensive. We offer these instructions as a getting-started reference. Steps may vary depending on your operating system, installed software, and DevExpress versions. You, the developer, are responsible for the application, database, network, and other configurations based on your client, security, environment, and other requirements. We recommend that you review these settings with your database, network, and IT infrastructure administrators and consider their recommendations tailored to your case.
See Also
Middle Tier Security with EF Core
Create an Application with EF Core Middle Tier Security
Connect a .NET Desktop Client to an ASP.NET Core WebAPI Service Powered by EF Core
Connect a .NET Desktop Client to a Secure Backend Web API Service (EF Core with OData)