Protecting Angular Routes: Exploring the Power of canLoad and canActivate for Secure and Efficient Navigation

roberteftene

roberteftene

Posted on January 21, 2024

Protecting Angular Routes: Exploring the Power of canLoad and canActivate for Secure and Efficient Navigation

Navigating a web application involves more than just creating links and rendering content for the user. Most of the times, as developers, we need to enforce certain conditions or permissions before allowing users to access specific parts of our application. One way of accomplishing this is by using route guarding tools, and Angular provides us with powerful instruments for this purpose: canLoad and canActivate.

We will delve into the world of route guarding in Angular, focusing on the distinct roles played by canLoad and canActivate. These interfaces enable us to enhance the security of the applications and optimise the content loading. Therefore, this article will help you understand their exact applications, implementations and the scenarios where each shines.

Introducing to canLoad

The main purpose of canLoad is to prevent lazy-loaded modules from loading in the browser if certain conditions are not met. As an example, let’s say we have an app for teachers and students, and we want to load the teachers module in a lazy manner, but only if the user have the role of a teacher. Here comes into play canLoad , which will load the module only if the user is a teacher. In this way, besides securing the content, we are also optimising the application by not loading unnecessary modules.

For demonstration purposes, let’s continue with the teacher/student platform. We will define the routes of our application like this:

const routes: Routes = [
  {
    path:'',
    children: [
      {

        path:'student',
        loadChildren: () => import('./modules/student/student.module').then((m) => m.StudentModule),
       //assign route guard 
    canLoad: [UserCheckGuardService],

       //we are passing the role required for accessing this module
        data: [{
          roles: 'student'
        }],
      },
      {
        path: 'teacher',
        //assign route guard
        canLoad: [UserCheckGuardService],
        data: [{
          roles: 'teacher'
        }],
        loadChildren: () => import('./modules/teacher/teacher.module').then((m) => m.TeacherModule)
      },
      {
        path: 'social-events',
        loadChildren: () => import('./modules/public/public.module').then((m) => m.PublicModule)
      },
      {
        path: 'login',
        loadComponent: () => import('./shared/login/login.component').then((c) => c.LoginComponent)
      }
    ]
  }
];
Enter fullscreen mode Exit fullscreen mode

Note that we are using canLoad property for providing the guard for the student and teacher modules. We also defining the role required for accessing the route, in the data property. What we are passing to the canLoad property is an implementation of the CanLoad interface, UserCheckGuardService.

@Injectable({
  providedIn: 'root'
})
export class UserCheckGuardService implements CanLoad {
  private readonly router = inject(Router);
  private readonly authService = inject(AuthService);

  constructor() {
  }

  canLoad(route: Route, segments: UrlSegment[]): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    if (this.authService.getCurrentUser()) {
      if (route.data[0].roles === this.authService.getCurrentUser()?.role) {
        return true;
      }
    }
    return this.router.navigate(['login'])
  }
}
Enter fullscreen mode Exit fullscreen mode

In this class, I implemented the logic for the canLoad method, which will be called before initial loading of the module. The method checks if the current user has the required role for accessing the route. If yes, the module is loaded; otherwise, the user will be redirected to the login page. In this way we are only loading the module when we are suppose to do it.

Note: Once a module is loaded, the canLoad guard will not be triggered again. canLoad is specifically designed to control the initial loading of a module. However, it's crucial to be aware of a potential risk. If a user logs out and a different user logs in, attempting to access an already loaded module, the canLoad guard won't be invoked again. Consequently, this can lead to a security vulnerability, allowing the user to bypass the guard. To address this scenario we will use canActivate.

Another thing to keep in mind is that, at the moment of writing this article, canLoad is not working for standalone components. canActivate can be used instead.

Introducing to canActivate

The canActivate interface plays a pivotal role in route guards, operating before a route is activated. This implies that every time a user attempts to access a route, the associated guards are triggered.

For our scenario, I’ve created StudentRouteGuard service which is implementing canActivate interface:

@Injectable({
  providedIn: 'root'
})
export class StudentRouteGuardService implements CanActivate {
  private readonly router = inject(Router);
  private readonly authService = inject(AuthService);

  constructor() {
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    if (this.authService.getCurrentUser()) {
      if (this.authService.getCurrentUser()?.role === 'student') {
        return true;
      }
    }
    return this.router.navigate(['login'])
  }
}
Enter fullscreen mode Exit fullscreen mode

Having a similar code implementation with UserCheckGuardService, the StudentRouteGuard will check, each time the route is activated, if the current user has the role of a student.

Next, we are adding this guard to the desired route path.

const routes: Routes = [
  {
    path: '',
    component: StudentOverviewComponent,
    // added canActivate implementation for securing StudentOverview view
    canActivate: [StudentRouteGuardService]
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class StudentRoutingModule { }
Enter fullscreen mode Exit fullscreen mode

With this setup, even if the module was previously loaded (and canLoad is no longer triggered), canActivate will activate on every attempt to access the route, redirecting users with incorrect roles to the login page.

As a conclusion, it’s important to note that the combination of canLoad and canActivate is the key of achieving a robust security strategy. While canLoad it’s optimising initial module loading, canActivate ensure continuous route-level security.

Key Notes

  • canLoad is useful when we want to prevent unnecessary loading of lazy-loaded modules, therefore, provides a performance boost
  • canLoad is triggered only at the initial loading of the module. Once the module is loaded, the guard will not be called anymore.
  • canLoad works only for lazy-loaded modules
  • canActivate runs before activating a route and provides robust security at both module and route levels

Thanks for reading! Feel free to leave any questions or feedback!

Happy coding! And may your Angular routes always be secure and efficient!

💖 💪 🙅 🚩
roberteftene
roberteftene

Posted on January 21, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related