Role-Based Access Control (RBAC) is a fundamental security practice in Angular applications. It determines what a user can or cannot access based on their assigned role. RBAC improves security, simplifies authorization logic, and ensures that sensitive functionality is only accessible to authorized users.
This post explains how to implement RBAC in Angular using services, guards, components, and templates, complete with code examples and best practices.
1. Introduction to Role-Based Access Control
In RBAC, users are assigned roles such as:
admin
– full access to all features.editor
– access to modify content.viewer
– read-only access.
RBAC allows developers to centralize access logic, making it easier to maintain and update permissions across the application.
Benefits of RBAC:
- Prevent unauthorized access to sensitive routes and components.
- Reduce repetitive conditional checks in components.
- Maintain consistent access rules across modules.
- Simplify application security audits and compliance.
2. Setting Up Roles in Angular
The first step is to define roles in your application. You can store roles in authentication tokens (JWT), user service, or local storage.
Example: Role Enum
// roles.enum.ts
export enum Roles {
Admin = 'admin',
Editor = 'editor',
Viewer = 'viewer'
}
User Service Example:
// auth.service.ts
import { Injectable } from '@angular/core';
import { Roles } from './roles.enum';
@Injectable({ providedIn: 'root' })
export class AuthService {
private userRole: Roles = Roles.Viewer; // Default role
constructor() {}
setUserRole(role: Roles) {
this.userRole = role;
localStorage.setItem('userRole', role);
}
getUserRole(): Roles {
return localStorage.getItem('userRole') as Roles || this.userRole;
}
isRole(role: Roles): boolean {
return this.getUserRole() === role;
}
}
3. Role Check Function
You can create a utility function to check access based on roles.
// role-utils.ts
import { AuthService } from './auth.service';
export function canAccess(authService: AuthService, requiredRole: string): boolean {
const userRole = authService.getUserRole();
return userRole === requiredRole;
}
Usage:
- In guards to protect routes.
- In components to conditionally render UI.
- In templates for role-based hiding or showing content.
4. Role-Based Route Guards
Angular route guards allow you to prevent navigation to unauthorized routes. Use CanActivate
or CanLoad
to enforce RBAC.
Example: Role Guard
// role.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot } from '@angular/router';
import { AuthService } from './auth.service';
import { Roles } from './roles.enum';
@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot): boolean {
const requiredRole = route.data['role'] as Roles;
if (this.authService.isRole(requiredRole)) {
return true;
} else {
this.router.navigate(['/unauthorized']);
return false;
}
}
}
Apply Guard to Routes:
// app-routing.module.ts
const routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [RoleGuard],
data: { role: Roles.Admin }
},
{
path: 'editor',
component: EditorComponent,
canActivate: [RoleGuard],
data: { role: Roles.Editor }
},
{ path: '**', redirectTo: 'home' }
];
5. Role-Based Access in Components
Sometimes you want to conditionally render UI elements based on roles.
// admin.component.ts
import { Component } from '@angular/core';
import { AuthService } from './auth.service';
import { Roles } from './roles.enum';
@Component({
selector: 'app-admin',
template: `
<h1>Admin Dashboard</h1>
<button *ngIf="authService.isRole(Roles.Admin)">Delete User</button>
<button *ngIf="authService.isRole(Roles.Editor)">Edit Content</button>
<button *ngIf="authService.isRole(Roles.Viewer)">View Only</button>
`
})
export class AdminComponent {
Roles = Roles;
constructor(public authService: AuthService) {}
}
Here, *ngIf
directives control visibility based on the user’s role.
6. Role-Based Access in Templates
You can create a directive for cleaner template usage.
Role Directive:
// has-role.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { AuthService } from './auth.service';
import { Roles } from './roles.enum';
@Directive({
selector: '[hasRole]'
})
export class HasRoleDirective {
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private authService: AuthService
) {}
@Input() set hasRole(role: Roles) {
if (this.authService.isRole(role)) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
Usage in Template:
<button *hasRole="Roles.Admin">Delete User</button>
<button *hasRole="Roles.Editor">Edit Post</button>
This keeps templates clean and readable while enforcing RBAC.
7. Role Hierarchies (Optional)
Sometimes roles are hierarchical. For example:
- Admin > Editor > Viewer
You can implement a role hierarchy in your service:
// auth.service.ts
private roleHierarchy: Roles[] = [Roles.Viewer, Roles.Editor, Roles.Admin];
hasRoleOrHigher(requiredRole: Roles): boolean {
const userRole = this.getUserRole();
return this.roleHierarchy.indexOf(userRole) >= this.roleHierarchy.indexOf(requiredRole);
}
Usage:
if (authService.hasRoleOrHigher(Roles.Editor)) {
// Editor or Admin can access
}
8. RBAC with Lazy-Loaded Modules
Combine role guards with lazy loading for secure, scalable applications.
// app-routing.module.ts
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [RoleGuard],
data: { role: Roles.Admin }
}
Here, the AdminModule
won’t even be loaded unless the user is an admin.
9. Handling Unauthorized Access
Redirect unauthorized users to a dedicated page:
// unauthorized.component.ts
@Component({
selector: 'app-unauthorized',
template: <h2>Access Denied</h2><p>You do not have permission to view this page.</p>
})
export class UnauthorizedComponent {}
Add route:
{ path: 'unauthorized', component: UnauthorizedComponent }
10. RBAC Best Practices
- Centralize role logic: Keep role checking in a service.
- Use enums: Avoid string literals for roles.
- Combine with guards: Prevent unauthorized route access.
- Directive for templates: Simplifies role-based UI rendering.
- Lazy loading: Protect entire modules based on role.
- Role hierarchies: Support advanced access control.
- Audit and testing: Ensure roles behave as expected across modules.
11. Full Example
AuthService:
@Injectable({ providedIn: 'root' })
export class AuthService {
private roleHierarchy: Roles[] = [Roles.Viewer, Roles.Editor, Roles.Admin];
setUserRole(role: Roles) {
localStorage.setItem('userRole', role);
}
getUserRole(): Roles {
return localStorage.getItem('userRole') as Roles || Roles.Viewer;
}
isRole(role: Roles): boolean {
return this.getUserRole() === role;
}
hasRoleOrHigher(role: Roles): boolean {
const userRole = this.getUserRole();
return this.roleHierarchy.indexOf(userRole) >= this.roleHierarchy.indexOf(role);
}
}
RoleGuard:
@Injectable({ providedIn: 'root' })
export class RoleGuard implements CanActivate, CanLoad {
constructor(private authService: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot): boolean {
const requiredRole = route.data['role'] as Roles;
if (this.authService.isRole(requiredRole)) return true;
this.router.navigate(['/unauthorized']);
return false;
}
canLoad(route: Route, segments: UrlSegment[]): boolean {
const requiredRole = route.data && route.data['role'] as Roles;
if (!requiredRole) return true;
if (this.authService.isRole(requiredRole)) return true;
this.router.navigate(['/unauthorized']);
return false;
}
}
Route Example:
const routes: Routes = [
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), canLoad: [RoleGuard], data: { role: Roles.Admin } },
{ path: 'editor', component: EditorComponent, canActivate: [RoleGuard], data: { role: Roles.Editor } },
{ path: '**', redirectTo: 'home' }
];
Template Example:
<button *hasRole="Roles.Admin">Delete User</button>
<button *hasRole="Roles.Editor">Edit Post</button>
<button *hasRole="Roles.Viewer">View Content</button>
12. Summary
- RBAC ensures secure and organized access control.
- Combine guards, services, and directives for maximum control.
- Lazy loading with role guards prevents unauthorized module downloads.
- Maintain role enums, hierarchies, and centralized checks for scalability.
- Templates and components can adapt dynamically based on the current user’s role.
Leave a Reply