ASP.NET Core Identity Integration ASP.NET Core Identity provides UI for login, registration, password management, and more. For a Web API, you'll typically use its underlying services for user/role management and JWT for authentication. 1. Setup Identity in Program.cs Install Packages: dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer Configure Services: builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddIdentity<ApplicationUser, ApplicationRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) }; }); builder.Services.AddAuthorization(options => { // Define policies for your custom permissions options.AddPolicy("ResourcePermission", policy => policy.Requirements.Add(new ResourcePermissionRequirement())); }); builder.Services.AddSingleton<IAuthorizationHandler, ResourcePermissionHandler>(); Add Middleware: app.UseAuthentication(); app.UseAuthorization(); 2. Custom Identity Classes Map your Users and Roles tables to Identity's IdentityUser and IdentityRole . ApplicationUser.cs : public class ApplicationUser : IdentityUser<int> // Use int for PK { public string UserName { get; set; } // Already in IdentityUser public string Email { get; set; } // Already in IdentityUser public string PhoneNumber { get; set; } // Already in IdentityUser // PasswordHash & PasswordSalt managed by Identity public bool IsActive { get; set; } = true; public bool IsLockedOut { get; set; } = false; public DateTime? LastLoginAt { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; // RowVersion handled by EF Core concurrency token } ApplicationRole.cs : public class ApplicationRole : IdentityRole<int> // Use int for PK { public string Name { get; set; } // Already in IdentityRole public string DisplayName { get; set; } public int Level { get; set; } public string Description { get; set; } public bool IsSystemRole { get; set; } = false; public bool IsActive { get; set; } = true; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; // RowVersion handled by EF Core concurrency token } ApplicationDbContext.cs : public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, int> { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Resource> Resources { get; set; } public DbSet<Permission> Permissions { get; set; } public DbSet<RolePermission> RolePermissions { get; set; } public DbSet<UserPermission> UserPermissions { get; set; } public DbSet<Scope> Scopes { get; set; } public DbSet<UserRole> UserRoles { get; set; } // Your custom UserRoles table protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // Map Identity tables to your existing table names builder.Entity<ApplicationUser>().ToTable("Users"); builder.Entity<ApplicationRole>().ToTable("Roles"); builder.Entity<IdentityUserClaim<int>>().ToTable("UserClaims"); builder.Entity<IdentityUserLogin<int>>().ToTable("UserLogins"); builder.Entity<IdentityUserToken<int>>().ToTable("UserTokens"); builder.Entity<IdentityRoleClaim<int>>().ToTable("RoleClaims"); // IdentityUserRole is a join table for IdentityUser and IdentityRole. // Your UserRoles table needs to be handled separately. // Consider if you want to use IdentityUserRole for direct user-role mapping // or keep your custom UserRoles for granular scope control. // If you keep custom UserRoles, you'll manage role assignment manually. builder.Entity<IdentityUserRole<int>>().ToTable("IdentityUserRoles"); // If you use Identity's built-in UserRoles // Configure your custom entities (Resources, Permissions, etc.) builder.Entity<Resource>().HasKey(r => r.ResourceId); builder.Entity<Permission>().HasKey(p => p.PermissionId); builder.Entity<RolePermission>().HasKey(rp => rp.RolePermissionId); builder.Entity<UserPermission>().HasKey(up => up.UserPermissionId); builder.Entity<Scope>().HasKey(s => s.ScopeId); builder.Entity<UserRole>().HasKey(ur => ur.UserRoleId); // Define relationships for your custom tables based on your SQL scripts builder.Entity<Permission>() .HasOne<Resource>() .WithMany() .HasForeignKey(p => p.ResourceId) .IsRequired(false); // Make it optional as per script builder.Entity<UserRole>() .HasOne(ur => ur.User) .WithMany() .HasForeignKey(ur => ur.UserId); builder.Entity<UserRole>() .HasOne(ur => ur.Role) .WithMany() .HasForeignKey(ur => ur.RoleId); builder.Entity<UserRole>() .HasOne(ur => ur.Scope) .WithMany() .HasForeignKey(ur => ur.ScopeId); builder.Entity<RolePermission>() .HasOne(rp => rp.Role) .WithMany() .HasForeignKey(rp => rp.RoleId); builder.Entity<RolePermission>() .HasOne(rp => rp.Permission) .WithMany() .HasForeignKey(rp => rp.PermissionId); builder.Entity<UserPermission>() .HasOne(up => up.User) .WithMany() .HasForeignKey(up => up.UserId); builder.Entity<UserPermission>() .HasOne(up => up.Permission) .WithMany() .HasForeignKey(up => up.PermissionId); // Add concurrency tokens for RowVersion builder.Entity<ApplicationUser>().Property(u => u.RowVersion).IsRowVersion(); builder.Entity<ApplicationRole>().Property(r => r.RowVersion).IsRowVersion(); builder.Entity<Resource>().Property(r => r.RowVersion).IsRowVersion(); builder.Entity<Permission>().Property(p => p.RowVersion).IsRowVersion(); builder.Entity<Scope>().Property(s => s.RowVersion).IsRowVersion(); // UserRoles, RolePermissions, UserPermissions don't have RowVersion in your script } } 3. Authentication (Login & JWT Generation) Users log in to get a JWT. This token will contain claims about their identity and roles. Login Endpoint: [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly UserManager<ApplicationUser> _userManager; private readonly SignInManager<ApplicationUser> _signInManager; private readonly IConfiguration _configuration; public AuthController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager, IConfiguration configuration) { _userManager = userManager; _signInManager = signInManager; _configuration = configuration; } [HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginModel model) { var user = await _userManager.FindByNameAsync(model.Username); if (user == null || !user.IsActive) return Unauthorized("Invalid credentials or inactive user."); var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false); if (!result.Succeeded) return Unauthorized("Invalid credentials."); // Generate JWT var authClaims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.UserName), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; // Add user roles from your custom UserRoles table var userRoles = await _dbContext.UserRoles .Where(ur => ur.UserId == user.Id) .Select(ur => ur.Role.Name) .Distinct() .ToListAsync(); foreach (var userRole in userRoles) { authClaims.Add(new Claim(ClaimTypes.Role, userRole)); } var token = GetToken(authClaims); user.LastLoginAt = DateTime.UtcNow; await _userManager.UpdateAsync(user); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token), expiration = token.ValidTo }); } private JwtSecurityToken GetToken(List<Claim> authClaims) { var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])); var token = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], expires: DateTime.Now.AddHours(3), claims: authClaims, signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256) ); return token; } } 4. Authorization (RBAC with Custom Policies) The core of your RBAC system. You'll need a custom AuthorizationRequirement and AuthorizationHandler . Custom Classes for RBAC ResourcePermissionRequirement.cs : public class ResourcePermissionRequirement : IAuthorizationRequirement { public string Resource { get; } public string Action { get; } public string FieldName { get; } // Optional for field-level permissions public ResourcePermissionRequirement(string resource, string action, string fieldName = null) { Resource = resource; Action = action; FieldName = fieldName; } } ResourcePermissionHandler.cs : public class ResourcePermissionHandler : AuthorizationHandler<ResourcePermissionRequirement> { private readonly ApplicationDbContext _dbContext; private readonly IHttpContextAccessor _httpContextAccessor; public ResourcePermissionHandler(ApplicationDbContext dbContext, IHttpContextAccessor httpContextAccessor) { _dbContext = dbContext; _httpContextAccessor = httpContextAccessor; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ResourcePermissionRequirement requirement) { if (context.User == null || !context.User.Identity.IsAuthenticated) { context.Fail(); return; } var userIdString = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (!int.TryParse(userIdString, out int userId)) { context.Fail(); return; } // Determine the scope for the request (e.g., from header, route, or query param) // For simplicity, let's assume a default global scope or infer from user's primary scope. // In a real app, this would be more complex and likely passed as a parameter. var requestScopeId = GetScopeIdFromRequest(_httpContextAccessor.HttpContext); // Implement this method var hasPermission = await CheckUserPermissions(userId, requestScopeId, requirement.Resource, requirement.Action, requirement.FieldName); if (hasPermission) { context.Succeed(requirement); } else { context.Fail(); } } private async Task<bool> CheckUserPermissions(int userId, int? scopeId, string resource, string action, string fieldName) { // 1. Get all effective roles for the user within the given scope var userRoleIds = await _dbContext.UserRoles .Where(ur => ur.UserId == userId && (scopeId == null || ur.ScopeId == scopeId.Value)) .Select(ur => ur.RoleId) .ToListAsync(); // 2. Find the specific permission var permission = await _dbContext.Permissions .FirstOrDefaultAsync(p => p.Resource == resource && p.Action == action && p.FieldName == fieldName); if (permission == null) return false; // 3. Check RolePermissions (IsAllowed = TRUE) var roleHasPermission = await _dbContext.RolePermissions .AnyAsync(rp => userRoleIds.Contains(rp.RoleId) && rp.PermissionId == permission.PermissionId && rp.IsAllowed); // 4. Check UserPermissions (Overrides role permissions) var userOverride = await _dbContext.UserPermissions .Where(up => up.UserId == userId && up.PermissionId == permission.PermissionId) .OrderByDescending(up => up.CreatedAt) // Get the latest override .FirstOrDefaultAsync(); if (userOverride != null) { return userOverride.IsAllowed; // User override takes precedence } return roleHasPermission; } // Example: How to get scope ID from request (customize as needed) private int? GetScopeIdFromRequest(HttpContext httpContext) { // This is a placeholder. You might get it from: // - A route parameter: httpContext.GetRouteValue("scopeId") // - A query string: httpContext.Request.Query["scopeId"] // - A custom header: httpContext.Request.Headers["X-Scope-Id"] // - A claim in the JWT (if the token contains a default scope) // For now, let's return a default or null for global. return null; // Represents global scope if no specific scope is provided } } Using the Custom Authorization Policy On Controller/Action: [Authorize(Policy = "ResourcePermission")] [HttpGet("products")] public async Task<IActionResult> GetProducts() { // Access check for "Product:View" is handled by the policy return Ok(await _dbContext.Resources.Where(r => r.Name == "Product").ToListAsync()); } // For specific permissions, you might need a custom attribute or pass parameters [ResourceAuthorize("Product", "Edit")] // A custom attribute simplifies this [HttpPut("products/{id}")] public async Task<IActionResult> UpdateProduct(int id, [FromBody] ProductUpdateDto model) { // ... update logic return Ok(); } [ResourceAuthorize("Product", "Edit", "Price")] // Field-level [HttpPut("products/update-price")] public async Task<IActionResult> UpdateProductPrice([FromBody] ProductPriceUpdateDto model) { // ... update price logic return Ok(); } Custom ResourceAuthorizeAttribute : public class ResourceAuthorizeAttribute : AuthorizeAttribute { public ResourceAuthorizeAttribute(string resource, string action, string fieldName = null) { Policy = $"ResourcePermission:{resource}:{action}:{fieldName}"; // You'll need to register policies dynamically in Startup.cs } } // In Program.cs/Startup.cs: // builder.Services.AddAuthorization(options => // { // // For dynamic policies based on attribute parameters // options.AddPolicy("ResourcePermission", policy => // policy.Requirements.Add(new ResourcePermissionRequirement())); // // Example of registering a specific policy if not using dynamic policy names // // options.AddPolicy("Product:View", policy => // // policy.Requirements.Add(new ResourcePermissionRequirement("Product", "View"))); // }); // For dynamic policy creation, you might need a custom IAuthorizationPolicyProvider // or generate policies for all known resources/actions at startup. 5. Data Models for Custom Tables EF Core models for your existing tables. Resource.cs : public class Resource { public int ResourceId { get; set; } public string Name { get; set; } public string DisplayName { get; set; } public string Url { get; set; } public string HttpMethod { get; set; } public int? ParentId { get; set; } public string Module { get; set; } public string Description { get; set; } public bool IsActive { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } public byte[] RowVersion { get; set; } } Permission.cs : public class Permission { public int PermissionId { get; set; } public string Resource { get; set; } public string FieldName { get; set; } public string Action { get; set; } public string Description { get; set; } public bool IsActive { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } public byte[] RowVersion { get; set; } public int? ResourceId { get; set; } // Added based on your ALTER TABLE } RolePermission.cs : public class RolePermission { public int RolePermissionId { get; set; } public int RoleId { get; set; } public int PermissionId { get; set; } public bool IsAllowed { get; set; } public DateTime CreatedAt { get; set; } public virtual ApplicationRole Role { get; set; } public virtual Permission Permission { get; set; } } UserPermission.cs : public class UserPermission { public int UserPermissionId { get; set; } public int UserId { get; set; } public int PermissionId { get; set; } public bool IsAllowed { get; set; } public DateTime CreatedAt { get; set; } public virtual ApplicationUser User { get; set; } public virtual Permission Permission { get; set; } } Scope.cs : public class Scope { public int ScopeId { get; set; } public byte ScopeType { get; set; } public int? CompanyId { get; set; } public int? StoreId { get; set; } public int? WarehouseId { get; set; } public int? DepartmentId { get; set; } public string Name { get; set; } public bool IsActive { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } public byte[] RowVersion { get; set; } } UserRole.cs : public class UserRole { public int UserRoleId { get; set; } public int UserId { get; set; } public int RoleId { get; set; } public int ScopeId { get; set; } public int? AssignedBy { get; set; } public DateTime AssignedAt { get; set; } public bool IsActive { get; set; } public virtual ApplicationUser User { get; set; } public virtual ApplicationRole Role { get; set; } public virtual Scope Scope { get; set; } public virtual ApplicationUser AssignedByUser { get; set; } // Self-referencing } 6. Best Practices & Considerations Scope Management: The GetScopeIdFromRequest method is crucial. You need a robust way to determine the active scope (e.g., global, company, store) for each request. This often involves: Route Parameters: /api/companies/{companyId}/products Query Strings: /api/products?scopeType=Store&scopeId=123 HTTP Headers: X-Scope-Id: 123, X-Scope-Type: Store User Claims: If a user is primarily assigned to one scope, it could be in their JWT. Performance: Caching permissions (e.g., using IMemoryCache or Redis) can significantly improve performance, especially for frequently accessed permissions. The CheckUserPermissions method can be a bottleneck. Dynamic Policy Registration: For a large number of resources/actions, dynamically creating authorization policies based on your Permissions table at application startup is more scalable than hardcoding them. // In Program.cs/Startup.cs builder.Services.AddSingleton<IAuthorizationPolicyProvider, CustomAuthorizationPolicyProvider>(); // CustomAuthorizationPolicyProvider would parse the policy name // (e.g., "ResourcePermission:Product:View") and create the requirement. Claims Transformation: You can use IClaimsTransformation to add claims (like assigned scopes or specific permissions) to the user's principal after authentication, reducing database lookups on every authorization check. Seed Data: Ensure your seed data for Identity (users, roles) aligns with your custom RBAC seed data. You can use UserManager and RoleManager to create initial users and roles. Password Hashing: Identity handles password hashing and salting automatically. Ensure you don't manually set PasswordHash for new users; use _userManager.CreateAsync(user, password) . Migration Strategy: If you have an existing database, use EF Core's "Scaffold-DbContext" or manually create models and then configure them to map to your existing tables. Avoid generating migrations that try to recreate your tables if they already exist.