Angular  

Implementing Role-Based Access Control (RBAC) in Angular Applications

Introduction

Multiple user types, including administrators, managers, and staff, each with distinct permissions, are frequently found in enterprise Angular applications. Role-Based Access Control (RBAC) is the process of controlling access to routes and user interface components according to user roles.

This article describes how to use directives, a shared authentication service, and Angular's routing system to implement RBAC. It also details a problem we ran into in real time and the method we employed to resolve it.

Use Case

An internal admin panel was developed for a logistics organization. The system included the following access requirements:

  • User Management: Accessible only by Admins
  • Settings Page: Accessible by Admins and Managers
  • Reports: Accessible by all users
  • Audit Logs: Accessible only by Admins

User authentication was based on JWT. Upon login, the server returned a token containing the roles assigned to the user.

Architecture Overview

RBAC was implemented using the following components:

  • An AuthService to store and expose the current user's roles.
  • A Route Guard to restrict access to routes based on roles.
  • A Structural Directive to conditionally render UI elements based on role checks.
  • A fallback Unauthorized Component for blocked access.

Step 1. Extracting Roles from Token

After successful login, the token is decoded to extract user roles.

const decodedToken = this.jwtHelper.decodeToken(token);

this.userRoles = decodedToken.roles;  // Example: ['Admin', 'Manager']

These roles are stored in a shared AuthService:

@Injectable({ providedIn: 'root' })
export class AuthService {

  private roles: string[] = [];

  private rolesLoaded = new BehaviorSubject<boolean>(false);
  rolesLoaded$ = this.rolesLoaded.asObservable();

  setRoles(roles: string[]) {
    this.roles = roles;
    this.rolesLoaded.next(true);
  }

  hasRole(role: string): boolean {
    return this.roles.includes(role);
  }

  hasAnyRole(roles: string[]): boolean {
    return roles.some(role => this.roles.includes(role));
  }
}

Step 2. Protecting Routes Using Route Guards

A RoleGuard was implemented using CanActivate to check route access.

@Injectable({ providedIn: 'root' })
export class AuthService {

  private roles: string[] = [];
  private rolesLoaded = new BehaviorSubject<boolean>(false);
  rolesLoaded$ = this.rolesLoaded.asObservable();

  setRoles(roles: string[]) {
    this.roles = roles;
    this.rolesLoaded.next(true);
  }

  hasRole(role: string): boolean {
    return this.roles.includes(role);
  }

  hasAnyRole(roles: string[]): boolean {
    return roles.some(role => this.roles.includes(role));
  }
}

Route definitions included the expected roles:

{
  path: 'user-management',
  component: UserManagementComponent,
  canActivate: [RoleGuard],
  data: { roles: ['Admin'] }
}

Step 3. Controlling UI Elements Using Custom Directive

To hide or show UI elements based on roles, a custom structural directive was created.

@Directive({
  selector: '[appHasRole]'
})
export class HasRoleDirective {

  constructor(
    private auth: AuthService,
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}

  @Input() set appHasRole(role: string | string[]) {
    this.auth.rolesLoaded$.pipe(
      filter(loaded => loaded),
      take(1)
    ).subscribe(() => {
      const hasAccess = Array.isArray(role)
        ? this.auth.hasAnyRole(role)
        : this.auth.hasRole(role);

      hasAccess
        ? this.viewContainer.createEmbeddedView(this.templateRef)
        : this.viewContainer.clear();
    });
  }

}

This directive was used in HTML templates:

<li *appHasRole="'Admin'">
  <a routerLink="/user-management">User Management</a>
</li>

<button *appHasRole="['Admin', 'Manager']">
  Edit Settings
</button>

Issue Faced: Role Data Not Available Immediately

After login, some components were rendering before the roles had finished loading. This caused the menu to appear incomplete or incorrect temporarily.

Root Cause

Components and directives were executing before the asynchronous role loading from the token was complete.

Fix: Using BehaviorSubject to Track Role Availability

To ensure that the application waits until roles are loaded, the AuthService exposed a rolesLoaded$ observable.

Components, directives, and guards subscribed to this observable as like below.

this.auth.rolesLoaded$.pipe(
  filter(loaded => loaded),
  take(1)
).subscribe(() => {
  // Proceed only when roles are ready
});

This resolved the timing issue and ensured correct rendering and access control across the app.

Conclusion

Implementing RBAC in Angular improves security and helps manage feature visibility across different user types. Using route guards, structural directives, and a centralized AuthService allows consistent enforcement of access control at both navigation and UI levels.