You Probably Don’t Know How to Do Role-Based Access Control (RBAC) in Angular

Leader posted 3 min read

In this article, we'll dive into the right way to implement Role-Based Access Control (RBAC) in Angular. Many developers make the mistake of only handling permissions on the frontend, using simple *ngIf directives to show or hide content. This approach is not only insecure but also scales poorly. A robust RBAC system in Angular requires a multi-layered approach involving route guards, a centralized service, and proper HTTP interceptors.


The Big Mistake: Frontend-Only RBAC

A common, yet insecure, method for RBAC in Angular is to rely solely on conditional rendering in the component's template. For example, a developer might check the user's role directly in the HTML:

<button *ngIf="user.role === 'admin'">
  Delete Item
</button>

While this hides the button from unauthorized users, it does not prevent a savvy user from manually triggering the underlying action. The endpoint is still exposed, and the user could make a direct API call to perform the forbidden action. This is a crucial security vulnerability. The frontend should only ever be a reflection of the user's permissions, not the enforcer of them.


The Correct Approach: A Multi-Layered Defense

A secure and scalable RBAC implementation in Angular involves three key layers:

1. Centralized Role Service

The first step is to create a single AuthService (or RbacService) that handles all user authentication and role management. This service should:

  • Store the user's roles and permissions, ideally retrieved from a JWT (JSON Web Token) that the backend provides upon login. This token is secure and tamper-proof.
  • Provide a set of helper methods, like hasRole('admin') or hasPermission('delete:items'), that can be used throughout the application to check permissions.
  • Manage the user's login state and token.

By centralizing this logic, you ensure consistency and prevent duplicate code.

2. Route Protection with Guards

Angular's route guards are the first line of defense against unauthorized navigation. You should use a CanActivate guard to check a user's permissions before they can even access a specific route or component.

Here's a simple example of a guard:

// src/app/guards/role.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const roleGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  const expectedRoles = route.data['roles'] as string[];

  if (authService.hasAnyRole(expectedRoles)) {
    return true; // Allow navigation
  }

  // Redirect to a 'denied' or 'login' page
  return router.createUrlTree(['/access-denied']);
};

You then apply this guard directly to your routes in app-routing.module.ts:

// src/app/app-routing.module.ts
const routes: Routes = [
  {
    path: 'admin-dashboard',
    component: AdminDashboardComponent,
    canActivate: [roleGuard],
    data: { roles: ['admin', 'manager'] }
  }
];

This ensures that only users with the admin or manager roles can access the admin dashboard, preventing unauthorized route changes.


3. API Protection with Interceptors

While guards protect the frontend, HTTP interceptors are essential for protecting your API calls. An interceptor can automatically add a user's JWT token to the header of every outgoing request. This allows the backend to validate the user's identity and permissions for every single action, regardless of whether the request originated from a protected route or a hidden button.

// src/app/interceptors/auth.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = this.authService.getToken();

    if (token) {
      // Clone the request and add the authorization header
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }

    return next.handle(request);
  }
}

This is the most critical step. Backend validation is the ultimate security measure. The backend should always verify the user's roles and permissions before fulfilling any request that modifies or accesses sensitive data. Your Angular app's RBAC system should be seen as a user experience enhancement, not a security feature.

By implementing RBAC with a centralized service, route guards, and HTTP interceptors, you build a layered and robust system that is both secure and maintainable. Stop relying on simple template hacks and start building for security from the ground up.

If you read this far, tweet to the author to show them you care. Tweet a Thanks

More Posts

How DomSanitizer works to prevent Cross-Site Scripting (XSS) attacks in Angular

Sunny - Aug 23

How to Maintain Fast Load Time in Angular Apps

Sunny - Jun 18

Angular 20.2 Just Killed CSS Leaks: Are You Using IsolatedShadowDom Yet?

Sunny - Aug 31

TypeScript Tricks Every Angular Developer Should Know

Sunny - Aug 21

How to Add Markdown Preview in an Angular Component

Sunny - Jul 30
chevron_left