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

RolePermissions
AdminCreate, read, update, delete all resources
ManagerRead and update resources for their team
EmployeeRead-only access to assigned resources

RBAC Principles

  1. Users are assigned one or more roles.

  2. Roles determine permissions.

  3. 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

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

  1. Always validate roles on the server. Frontend checks are for UX only.

  2. Use JWT claims for role management in SPAs.

  3. Centralize role constants to avoid typos.

  4. Use multiple roles in [Authorize] attributes for flexible access.

  5. Secure sensitive APIs with [Authorize(Roles = "Admin")] instead of only [Authorize].

  6. Handle token expiration gracefully in Angular.

  7. Seed default roles in the database for initial deployment.

8. Optional Enhancements

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:

…you can create a robust, production-ready RBAC system.

Key takeaways

  1. Always validate roles on the server.

  2. Use JWT to propagate roles to Angular.

  3. Combine guards and conditional rendering for better UX.

  4. Seed default roles and users for easier deployment.

  5. Use policy-based authorization for complex business rules.

This approach works well for enterprise dashboards, admin panels, and multi-role applications.