Introduction
In any enterprise application, security is critical. One of the most common security models is role-based access control (RBAC). RBAC ensures that users can only access resources and perform actions according to their roles, such as Admin, Manager, or Employee.
When building applications with ASP.NET Core as the backend and Angular as the frontend, implementing RBAC involves:
This guide explains step-by-step how to implement RBAC in a production-ready manner.
1. Understanding Role-Based Access Control
Roles define what a user can or cannot do. RBAC is simpler to maintain than user-based permissions.
Example Roles in a System
| Role | Permissions |
|---|
| Admin | Create, read, update, delete all resources |
| Manager | Read and update resources for their team |
| Employee | Read-only access to assigned resources |
RBAC Principles
Users are assigned one or more roles.
Roles determine permissions.
Roles are validated on both server and client.
2. ASP.NET Core Authentication Setup
2.1 Install Identity
For ASP.NET Core, we can use Identity for user and role management.
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
2.2 Configure Identity
In Program.cs:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.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"]))
};
});
3. Creating Roles and Admin User
You can seed roles at startup:
public static async Task SeedRolesAndAdmin(IServiceProvider serviceProvider)
{
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
string[] roles = { "Admin", "Manager", "Employee" };
foreach (var role in roles)
{
if (!await roleManager.RoleExistsAsync(role))
{
await roleManager.CreateAsync(new IdentityRole(role));
}
}
// Create default admin
var adminEmail = "[email protected]";
var adminUser = await userManager.FindByEmailAsync(adminEmail);
if (adminUser == null)
{
adminUser = new ApplicationUser { UserName = adminEmail, Email = adminEmail };
await userManager.CreateAsync(adminUser, "Admin@123");
await userManager.AddToRoleAsync(adminUser, "Admin");
}
}
Call this method in Program.cs after building the app:
using (var scope = app.Services.CreateScope())
{
await SeedRolesAndAdmin(scope.ServiceProvider);
}
4. Protecting APIs With Roles
ASP.NET Core provides the [Authorize] attribute to secure controllers and actions.
[Authorize(Roles = "Admin")]
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetAllUsers()
{
// Only Admins can access
return Ok(new { Message = "This is admin-only data." });
}
}
[Authorize(Roles = "Manager,Admin")]
[HttpPost("update")]
public IActionResult UpdateUser([FromBody] UserDto user)
{
// Admin and Manager can update users
return Ok();
}
Key Points
[Authorize(Roles = "Admin")] → only users in Admin role can access.
[Authorize(Roles = "Manager,Admin")] → multiple roles allowed.
Roles are validated on the server, preventing unauthorized access even if someone modifies the Angular UI.
5. Implementing Role-Based Authorization in JWT
For SPAs like Angular, we use JWT (JSON Web Tokens) to carry roles.
5.1 Adding Roles to JWT
private async Task<string> GenerateJwtToken(ApplicationUser user)
{
var roles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddHours(2),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
Now the JWT carries role information that Angular can use for conditional UI rendering.
6. Angular Role-Based Access
6.1 Auth Service
@Injectable({ providedIn: 'root' })
export class AuthService {
private tokenKey = 'auth_token';
constructor(private router: Router) {}
getToken(): string | null {
return localStorage.getItem(this.tokenKey);
}
getRoles(): string[] {
const token = this.getToken();
if (!token) return [];
const payload = JSON.parse(atob(token.split('.')[1]));
return payload['http://schemas.microsoft.com/ws/2008/06/identity/claims/role']
? [payload['http://schemas.microsoft.com/ws/2008/06/identity/claims/role']]
: [];
}
isInRole(role: string): boolean {
return this.getRoles().includes(role);
}
}
6.2 Role-Based Route Guards
@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot): boolean {
const roles = route.data['roles'] as string[];
if (!roles || roles.length === 0) return true;
if (roles.some(role => this.auth.isInRole(role))) return true;
this.router.navigate(['/unauthorized']);
return false;
}
}
6.3 Protecting Angular Routes
const routes: Routes = [
{ path: 'admin', component: AdminComponent, canActivate: [RoleGuard], data: { roles: ['Admin'] } },
{ path: 'manager', component: ManagerComponent, canActivate: [RoleGuard], data: { roles: ['Manager', 'Admin'] } },
{ path: '**', redirectTo: 'login' }
];
6.4 Conditional UI Rendering
<button *ngIf="auth.isInRole('Admin')">Delete User</button>
This ensures that UI elements are only visible to users with the appropriate role. Remember, server-side validation is still critical, as frontend-only checks can be bypassed.
7. Best Practices
Always validate roles on the server. Frontend checks are for UX only.
Use JWT claims for role management in SPAs.
Centralize role constants to avoid typos.
Use multiple roles in [Authorize] attributes for flexible access.
Secure sensitive APIs with [Authorize(Roles = "Admin")] instead of only [Authorize].
Handle token expiration gracefully in Angular.
Seed default roles in the database for initial deployment.
8. Optional Enhancements
Hierarchical roles: Admin > Manager > Employee
Dynamic roles: Store role-permissions in DB for runtime updates
Claims-based access: Use claims for fine-grained control, e.g., CanEditUser
Policy-based authorization: ASP.NET Core policies allow complex conditions
Example
services.AddAuthorization(options =>
{
options.AddPolicy("EditPolicy", policy =>
policy.RequireRole("Admin", "Manager")
.RequireClaim("CanEditUser", "true"));
});
Conclusion
Role-Based Security is essential for protecting your application. By combining:
ASP.NET Core Identity for user and role management
JWT tokens carrying roles
[Authorize] attributes for API protection
Angular route guards and conditional UI rendering
…you can create a robust, production-ready RBAC system.
Key takeaways
Always validate roles on the server.
Use JWT to propagate roles to Angular.
Combine guards and conditional rendering for better UX.
Seed default roles and users for easier deployment.
Use policy-based authorization for complex business rules.
This approach works well for enterprise dashboards, admin panels, and multi-role applications.