This example demonstrates how to protect your data with the XAF Security System in the ASP.NET Core MVC web application with the DevExtreme UI.
-
Visual Studio 2022 v17.0+ with the following workloads:
- ASP.NET and web development
- .NET Core cross-platform development
-
Download and run the Unified Component Installer or add NuGet feed URL to Visual Studio NuGet feeds.
We recommend that you select all products when you run the DevExpress installer. It will register local NuGet package sources and item / project templates required for these tutorials. You can uninstall unnecessary components later.
NOTE
If you have a pre-release version of our components, for example, provided with the hotfix, you also have a pre-release version of NuGet packages. These packages will not be restored automatically and you need to update them manually as described in the Updating Packages article using the Include prerelease option.
-
Create a new ASP.NET Core web application or use an existing application.
-
Add DevExpress NuGet packages to your project:
<PackageReference Include="DevExpress.ExpressApp.EFCore" Version="22.2.3" /> <PackageReference Include="DevExpress.Persistent.BaseImpl.EFCore" Version="22.2.3" /> <PackageReference Include="DevExtreme.AspNet.Core" Version="22.2.3" />
-
Install Entity Framework Core, as described in the Installing Entity Framework Core article.
-
Configure the MVC pipelines in the Program.cs:
var builder = WebApplication.CreateBuilder(args);
string loginPath = "/Authentication";
Action<MvcNewtonsoftJsonOptions> JsonOptions =
options => {
options.SerializerSettings.ContractResolver = new DefaultContractResolver();
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
};
builder.Services.AddControllersWithViews()
.AddNewtonsoftJson(JsonOptions);
builder.Services.AddHttpContextAccessor();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => {
options.LoginPath = loginPath;
});
builder.Services.AddDbContextFactory<ApplicationDbContext>((serviceProvider, options) => {
string connectionString = builder.Configuration.GetConnectionString("ConnectionString");
options.UseSqlServer(connectionString);
options.UseLazyLoadingProxies();
options.UseChangeTrackingProxies();
}, ServiceLifetime.Scoped);
var app = builder.Build();
if (app.Environment.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
else {
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseAuthentication();
app.UseDefaultFiles();
app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions() {
OnPrepareResponse = context => {
if (context.Context.User.Identity.IsAuthenticated) {
return;
}
else {
string referer = context.Context.Request.Headers["Referer"].ToString();
string authenticationPagePath = loginPath;
string vendorString = "vendor.css";
if (context.Context.Request.Path.HasValue && context.Context.Request.Path.StartsWithSegments(authenticationPagePath)
|| referer != null && (referer.Contains(authenticationPagePath) || referer.Contains(vendorString))) {
return;
}
context.Context.Response.Redirect(loginPath);
}
}
});
app.UseCookiePolicy();
pp.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.UseDemoData<ApplicationDbContext>(app.Configuration.GetConnectionString("ConnectionString"),
(builder, connectionString) =>
builder.UseSqlServer(connectionString).UseChangeTrackingProxies());
app.Run();
-
The
IConfiguration
object is used to access the application configuration appsettings.json file. In appsettings.json, add the connection string."ConnectionStrings": { "ConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=EFCoreTestDB;Integrated Security=True;MultipleActiveResultSets=True" }
NOTE
The Security System requires Multiple Active Result Sets in EF Core-based applications connected to the MS SQL database. We do not recommend that you remove “MultipleActiveResultSets=True;“ from the connection string or set the MultipleActiveResultSets parameter to false.
-
Register HttpContextAccessor in the Program.cs to access HttpContext in controller constructors.
builder.Services.AddHttpContextAccessor();
-
Set the StaticFileOptions.OnPrepareResponse property with the logic which сhecks if the ASP.NET Core Identity is authenticated. And, if not, it redirects a user to the authentication page.
-
Register DbContextFactory in the Program.cs to access DbContext from code.
-
In the Program.cs file, register the
TypesInfo
service required for the correct operation of the Security System.builder.Services.AddSingleton<ITypesInfo, TypesInfo>();
-
Call the
UseDemoData
method at the end of the Program.cs:public static class ApplicationBuilderExtensions { public static WebApplication UseDemoData<TContext>(this WebApplication app, string connectionString, EFCoreDatabaseProviderHandler databaseProviderHandler) where TContext : DbContext { ITypesInfo typesInfo = app.Services.GetRequiredService<ITypesInfo>(); using (var objectSpaceProvider = new EFCoreObjectSpaceProvider(typeof(TContext), typesInfo, connectionString, databaseProviderHandler)) using(var objectSpace = objectSpaceProvider.CreateUpdatingObjectSpace(true)) { new Updater(objectSpace).UpdateDatabase(); } return app; } }
For more details about how to create demo data from code, see the Updater.cs class.
Step 2: Initialize Data Store and XAF's Security System. Authentication and Permission Configuration
-
Register security system and authentication in Program.cs. We register it as a scoped to have access to SecurityStrategyComplex from SecurityProvider. The
AuthenticationMixed
class allows you to register several authentication providers, so you can use both AuthenticationStandard authentication and ASP.NET Core Identity authentication.var builder = WebApplication.CreateBuilder(args); //... builder.Services.AddScoped((serviceProvider) => { AuthenticationMixed authentication = new AuthenticationMixed(); authentication.LogonParametersType = typeof(AuthenticationStandardLogonParameters); authentication.AddAuthenticationStandardProvider(typeof(PermissionPolicyUser)); authentication.AddIdentityAuthenticationProvider(typeof(PermissionPolicyUser)); ITypesInfo typesInfo = serviceProvider.GetRequiredService<ITypesInfo>(); SecurityStrategyComplex security = new SecurityStrategyComplex(typeof(PermissionPolicyUser), typeof(PermissionPolicyRole), authentication, typesInfo); return security; });
-
Add security extension to
DbContextFactory
to allow your application to filter data based on user permissions. TheDbContextFactory
registers in the Program.cs:builder.Services.AddDbContextFactory<ApplicationDbContext>((serviceProvider, options) => { //... ITypesInfo typesInfo = serviceProvider.GetRequiredService<ITypesInfo>(); options.UseSecurity(serviceProvider.GetRequiredService<SecurityStrategyComplex>(), typesInfo); }, ServiceLifetime.Scoped);
-
Create MemberPermission, ObjectPermission and TypePermission classes. These classes are used as containers to transfer permissions to the client side.
public class MemberPermission { public bool Read { get; set; } public bool Write { get; set; } } //... public class ObjectPermission { public IDictionary<string, object> Data { get; set; } public string Key { get; set; } public bool Write { get; set; } public bool Delete { get; set; } public ObjectPermission() { Data = new Dictionary<string, object>(); } } //... public class TypePermission { public IDictionary<string, bool> Data { get; set; } public bool Create { get; set; } public TypePermission() { Data = new Dictionary<string, bool>(); } }
-
The SecurityProvider class provides access to the XAF Security System functionality.
public class SecurityProvider : IDisposable { public SecurityStrategyComplex Security { get; private set; } public IObjectSpaceProvider ObjectSpaceProvider { get; private set; } IHttpContextAccessor contextAccessor; IDbContextFactory<ApplicationDbContext> xafDbContextFactory; public SecurityProvider(SecurityStrategyComplex security, IDbContextFactory<ApplicationDbContext> xafDbContextFactory, IHttpContextAccessor contextAccessor) { this.xafDbContextFactory = xafDbContextFactory; Security = security; this.contextAccessor = contextAccessor; if(contextAccessor.HttpContext.User.Identity.IsAuthenticated) { Initialize(); } } //... }
-
Register
SecurityProvider
, in the Program.cs.builder.Services.AddScoped<SecurityProvider>();
-
The
Initialize
method initializes andObjectSpaceProvider
properties and performs Login to Security System.public void Initialize() { ((AuthenticationMixed)Security.Authentication).SetupAuthenticationProvider(typeof(IdentityAuthenticationProvider).Name, contextAccessor.HttpContext.User.Identity); ObjectSpaceProvider = GetObjectSpaceProvider(Security); Login(Security, ObjectSpaceProvider); } private void Login(SecurityStrategyComplex security, IObjectSpaceProvider objectSpaceProvider) { IObjectSpace objectSpace = ((INonsecuredObjectSpaceProvider)objectSpaceProvider).CreateNonsecuredObjectSpace(); security.Logon(objectSpace); }
-
The
GetObjectSpaceProvider
method initializes the Object Space Provider.private IObjectSpaceProvider GetObjectSpaceProvider(SecurityStrategyComplex security) { var objectSpaceProvider = new SecuredEFCoreObjectSpaceProvider<ApplicationDbContext>(security, xafDbContextFactory, security.TypesInfo)); return objectSpaceProvider; }
-
The
InitConnection
method authenticates a user both in the Security System and in ASP.NET Core HttpContext. A user is identified by the user name and password parameters.public bool InitConnection(string userName, string password) { AuthenticationStandardLogonParameters parameters = new AuthenticationStandardLogonParameters(userName, password); Security.Logoff(); ((AuthenticationMixed)Security.Authentication).SetupAuthenticationProvider(typeof(AuthenticationStandardProvider).Name, parameters); IObjectSpaceProvider objectSpaceProvider = GetObjectSpaceProvider(Security); try { Login(Security, objectSpaceProvider); SignIn(contextAccessor.HttpContext, userName); return true; } catch { return false; } } // Signs into HttpContext and creates a cookie. private void SignIn(HttpContext httpContext, string userName) { List<Claim> claims = new List<Claim>{ new Claim(ClaimsIdentity.DefaultNameClaimType, userName) }; ClaimsIdentity id = new ClaimsIdentity(claims, "ApplicationCookie", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType); ClaimsPrincipal principal = new ClaimsPrincipal(id); httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal); }
Step 3: Server-Side App Authentication and Authorization with MVC Controllers for Login, Logout and CRUD
- All controllers get SecurityProvider as constructor parameters to have access to objectSpace and security.
public class EmployeesController : Microsoft.AspNetCore.Mvc.Controller {
SecurityProvider securityProvider;
IObjectSpace objectSpace;
public EmployeesController(SecurityProvider securityProvider) {
this.securityProvider = securityProvider;
objectSpace = securityProvider.ObjectSpaceProvider.CreateObjectSpace();
}
//...
}
-
EmployeesController has methods to implement CRUD logic with Employee objects.
The
Get
method to get access to Employee objects. The DataSourceLoader class provides methods to load data from object collections.[HttpGet] public object Get(DataSourceLoadOptions loadOptions) { // The EFCore way: // var dbContext = ((EFCoreObjectSpace)objectSpace).DbContext; // // The XAF way: IQueryable<Employee> employees = objectSpace.GetObjectsQuery<Employee>(); return DataSourceLoader.Load(employees, loadOptions); }
The
Delete
method deletes the Employee object by key.[HttpDelete] public ActionResult Delete(Guid key) { Employee existing = objectSpace.GetObjectByKey<Employee>(key); if(existing != null) { objectSpace.Delete(existing); objectSpace.CommitChanges(); return NoContent(); } return NotFound(); }
The
Update
method updates a specific Employee object with specified values.[HttpPut] public ActionResult Update(Guid key, string values) { Employee employee = objectSpace.GetObjectByKey<Employee>(key); if(employee != null) { JsonParser.ParseJObject<Employee>(JObject.Parse(values), employee, objectSpace); return Ok(employee); } return NotFound(); }
The
Add
method creates new Employee object and saves it with specified values.[HttpPost] public ActionResult Add(string values) { Employee employee = objectSpace.CreateObject<Employee>(); JsonParser.ParseJObject<Employee>(JObject.Parse(values), employee, objectSpace); return Ok(employee); }
JsonParser is a helper class to obtain business object properties values from the
JObject
object.Note that secured EFCoreObjectSpace returns default values (for instance, null) for protected object properties - it is secure even without any custom UI.
-
DepartmentsController has methods to get access to Department objects and contains code similar to the one in EmployeesController.
-
AuthenticationController handles the Log in and Log out operations.
The
Authentication
methods return the Authentication view. TheLogout
method is called when a user clicks theLog out
button on the main page.public class AuthenticationController : Controller { SecurityProvider securityProvider; public AuthenticationController(SecurityProvider securityProvider){ this.securityProvider = securityProvider; } [HttpPost] [Route("Login")] [AllowAnonymous] public ActionResult Login(string userName, string password) { ActionResult result; if(securityProvider.InitConnection(userName, password)) { result = Ok(); } else { result = Unauthorized(); } return result; } [HttpGet] [Route("Logout")] public async Task<ActionResult> Logout() { await HttpContext.SignOutAsync(); return Ok(); } [Route("Authentication")] public IActionResult Authentication() { return View(); } protected override void Dispose(bool disposing) { if(disposing) { securityProvider?.Dispose(); } base.Dispose(disposing); } }
-
ActionsController contains the
GetPermissions
method to process permissions.[HttpPost] public ActionResult GetPermissions(List<string> keys, string typeName) { ActionResult result = NoContent(); using(IObjectSpace objectSpace = securityProvider.ObjectSpaceProvider.CreateObjectSpace()) { PermissionHelper permissionHelper = new PermissionHelper(securityProvider.Security); ITypeInfo typeInfo = objectSpace.TypesInfo.PersistentTypes.FirstOrDefault(t => t.Name == typeName); if(typeInfo != null) { IList entityList = objectSpace.GetObjects(typeInfo.Type, new InOperator(typeInfo.KeyMember.Name, keys)); List<ObjectPermission> objectPermissions = new List<ObjectPermission>(); foreach(object entity in entityList) { ObjectPermission objectPermission = permissionHelper.CreateObjectPermission(typeInfo, entity); objectPermissions.Add(objectPermission); } result = Ok(objectPermissions); } } return result; }
-
HomeController sends to client-side the Index view and type permissions.
public IActionResult Index() { using(IObjectSpace objectSpace = securityProvider.ObjectSpaceProvider.CreateObjectSpace()) { ITypeInfo typeInfo = objectSpace.TypesInfo.PersistentTypes.FirstOrDefault(t => t.Name == typeof(Employee).Name); PermissionHelper permissionHelper = new PermissionHelper(securityProvider.Security); TypePermission typePermission = permissionHelper.CreateTypePermission(typeInfo); return View(typePermission); } }
-
PermissionHelper is a helper class which provides methods to create permissions.
Use the SecurityStrategy.CanCreate and SecurityStrategy.CanWrite methods
to check if the user is allowed to perform a specific operation.The
CreateTypePermission
creates theTypePermission
object, which contains create operation permissions for the specified type and write operation permissions for all members of this type obtained from theGetPersistentMembers
method.public TypePermission CreateTypePermission(ITypeInfo typeInfo) { Type type = typeInfo.Type; TypePermission typePermission = new TypePermission(); typePermission.Create = Security.CanCreate(type); IEnumerable<IMemberInfo> members = GetPersistentMembers(typeInfo); foreach(IMemberInfo member in members) { bool writePermission = Security.CanWrite(type, member.Name); typePermission.Data.Add(member.Name, writePermission); } return typePermission; } private static IEnumerable<IMemberInfo> GetPersistentMembers(ITypeInfo typeInfo) { return typeInfo.Members.Where(p => p.IsVisible && p.IsProperty && (p.IsPersistent || p.IsList)); }
The
CreateObjectPermission
method creates theObjectPermission
object, which contains write and delete operation permissions for the specified object and theMemberPermission
objects for all members of this type obtained from theGetPersistentMembers
method.public ObjectPermission CreateObjectPermission(ITypeInfo typeInfo, object entity) { ObjectPermission objectPermission = new ObjectPermission(); objectPermission.Key = typeInfo.KeyMember.GetValue(entity).ToString(); objectPermission.Write = Security.CanWrite(entity); objectPermission.Delete = Security.CanDelete(entity); IEnumerable<IMemberInfo> members = GetPersistentMembers(typeInfo); foreach(IMemberInfo member in members) { MemberPermission memberPermission = CreateMemberPermission(entity, member); objectPermission.Data.Add(member.Name, memberPermission); } return objectPermission; }
The
CreateMemberPermission
method creates theMemberPermission
object, which contains read and write operation permissions for the specified member.public MemberPermission CreateMemberPermission(object entity, IMemberInfo member) { return new MemberPermission { Read = Security.CanRead(entity, member.Name), Write = Security.CanWrite(entity, member.Name) }; }
Authentication.cshtml is a login page that allows you to log into the application and AuthenticationCode.js contains scripts executed on the login page.
Index.cshtml is the main page. It configures the DevExtreme Data Grid and allows logging the user out.
IndexCode.js contains scripts executed on the main page:
-
The
onInitialized
function initializes the TypePermission object in the DataGrid initialized event.function onInitialized(e) { typePermissions = e; }
-
The
onLoaded
function handles the data grid's load event and sends a request to the server to obtain permissions for the current data grid page.function onLoaded(data) { var oids = $.map(data, function (val) { return val.ID; }); var parameters = { keys: oids, typeName: 'Employee' }; var options = { dataType: "json", contentType: "application/x-www-form-urlencoded; charset=UTF-8", type: "POST", async: false, data: parameters }; $.ajax("api/Actions/GetPermissions", options) .done(function (e) { permissions = e; }); }
-
The
onEditorPreparing
function handles the data grid's editorPreparing event and checks Read and Write operation permissions. If the Read operation permission is denied, it displays the "*******" placeholder and disables the editor. If the Write operation permission is denied, the editor is disabled.function onEditorPreparing(e) { if (e.parentType === "dataRow") { var dataField = e.dataField.split('.')[0]; var key = e.row.key; if (e.row.isNewRow) { if (!typePermissions[dataField]) { e.editorOptions.disabled = true; } } else { var objectPermission = getPermission(key); if (!objectPermission.Data[dataField].Read) { e.editorOptions.disabled = true; e.editorOptions.value = "*******"; } if (!objectPermission.Data[dataField].Write) { e.editorOptions.disabled = true; } } } }
-
The
onCellPrepared
function handles the data grid's cellPrepared event and checks Read operation permissions. If the Read operation is denied, it displays the "*******" placeholder in data grid cells.function onCellPrepared(e) { if (e.rowType === "data") { var key = e.key; var objectPermission = getPermission(key); if (!e.column.command && e.column.dataField != undefined) { var dataField = e.column.dataField.split('.')[0]; if (!objectPermission.Data[dataField].Read) { e.cellElement.text("*******"); } } } }
-
The
allowUpdating
function handles the data grid's allowUpdating event and checks Write operation permissions. Write operation permission checks define whether the Edit actions should be displayed or not.function allowUpdating(e) { if (e.row.rowType === "data") { var objectPermission = getPermission(e.row.key); if (objectPermission.Write) { return true; } } return false; }
-
The
allowDeleting
function handles the data grid's allowDeleting event and checks Delete operation permissions. The delete operation permission defines whether the Delete actions should be displayed or not.function allowDeleting(e) { if (e.row.rowType === "data") { var objectPermission = getPermission(e.row.key); if (objectPermission.Delete) { return true; } } return false; }
-
The
getPermission
function returns the permission object for a specific business object. The business object is identified by the key passed in function parameters:function getPermission(key) { var permission = permissions.filter(function (entry) { return entry.Key === key; }); return permission[0]; }