Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple canActivate guards all run when first fails

I have a route with two canActivate guards (AuthGuard and RoleGuard). The first (AuthGuard) checks to see if the user is logged in and, if not, redirects to the login page. The second checks to see if the user has a role defined that is allowed to view the page and, if not, redirects to the un-authorized page.

canActivate: [ AuthGuard, RoleGuard ] ... export class AuthGuard implements CanActivate {     canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {         ...         this.router.navigate(['/login']);         resolve(false); }  export class RoleGuard implements CanActivate {     canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {         ...         this.router.navigate(['/unauthorized']);         resolve(false); } 

The problem is that when I access the route and I am not logged in I hit the AuthGuard, which fails and tells the router to navigate to /login. However, even though the AuthGuard failed, the RoleGuard runs anyway which then navigates to /unauthorized.

In my opinion it is pointless to run the next guard if the first fails. Is there any way to enforce this behavior?

like image 635
revoxover Avatar asked Nov 14 '16 13:11

revoxover


People also ask

Can we have multiple guards in Angular?

We can use multiple route guards and then the route will only be accessible when all route guards return true. That's it! We now have a full implementation of route guards to protect out routes.

What if CanActivate returns false?

CanActivatelink If any guard returns false , navigation is cancelled. If any guard returns a UrlTree , the current navigation is cancelled and a new navigation begins to the UrlTree returned from the guard.

What does the CanActivate () method return when configuring guards in Angular?

CanActivate. Guards are implemented as services that need to be provided so we typically create them as @Injectable classes. Guards return either true if the user can access a route or false if they can't.

Can deactivate Guard is invoked when?

What is CanDeactivate Guard. The Angular CanDeactivate guard is called, whenever we navigate away from the route before the current component gets deactivated. The best use case for CanDectivate guard is the data entry component.


2 Answers

This is due to the fact you are returning a Promise<boolean> instead of just a boolean. If you were to just return a boolean it wouldn't check the RoleGuard. I would guess this is either a bug in angular2 or an expected result of async requests.

You can however resolve this with your example by only using RoleGuard for urls where a certain Role is required, because I guess you need to be logged in to have a role. In that case you can change your RoleGuard to this:

@Injectable() export class RoleGuard implements CanActivate {   constructor(private _authGuard: AuthGuard) {}    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {     return this._authGuard.canActivate(route, state).then((auth: boolean) => {       if(!auth) {         return false;       }       //... your role guard check code goes here     });   } } 

Update
In the latest Angular version (currently v8.x) - Even if both Guard will return false - they will still be both executed. (behavior was aligned between different return values)

like image 81
Poul Kruijt Avatar answered Sep 21 '22 00:09

Poul Kruijt


As mentioned by @PierreDuc data property in Route Class along with a Master Guard can be used to solve this problem.

Problem

First of all, angular doesn't support the feature to call the guards in tandem. So if first guard is asynchronous and is trying to make ajax calls, all the remaining guards will get fired even before completion of the ajax request in guard 1.

I faced the similar problem and this is how I solved it -


Solution

The idea is to create a master guard and let the master guard handle the execution of other guards.

The routing configuration in this case, will contain master guard as the only guard.

To let master guard know about the guards to be triggered for specific routes, add a data property in Route.

The data property is a key value pair that allows us to attach data with the routes.

The data can then be accessed in the guards using ActivatedRouteSnapshot parameter of canActivate method in the guard.

The solution looks complicated but it will assure proper working of guards once it is integrated in the application.

Following example explains this approach -


Example

1. Constants Object to map all application guards -

export const GUARDS = {     GUARD1: "GUARD1",     GUARD2: "GUARD2",     GUARD3: "GUARD3",     GUARD4: "GUARD4", } 

2. Application Guard -

import { Injectable } from "@angular/core"; import { Guard4DependencyService } from "./guard4dependency";  @Injectable() export class Guard4 implements CanActivate {     //A  guard with dependency     constructor(private _Guard4DependencyService:  Guard4DependencyService) {}      canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {         return new Promise((resolve: Function, reject: Function) => {             //logic of guard 4 here             if (this._Guard4DependencyService.valid()) {                 resolve(true);             } else {                 reject(false);             }         });     } } 

3. Routing Configuration -

import { Route } from "@angular/router"; import { View1Component } from "./view1"; import { View2Component } from "./view2"; import { MasterGuard, GUARDS } from "./master-guard"; export const routes: Route[] = [     {         path: "view1",         component: View1Component,         //attach master guard here         canActivate: [MasterGuard],         //this is the data object which will be used by          //masteer guard to execute guard1 and guard 2         data: {             guards: [                 GUARDS.GUARD1,                 GUARDS.GUARD2             ]         }     },     {         path: "view2",         component: View2Component,         //attach master guard here         canActivate: [MasterGuard],         //this is the data object which will be used by          //masteer guard to execute guard1, guard 2, guard 3 & guard 4         data: {             guards: [                 GUARDS.GUARD1,                 GUARDS.GUARD2,                 GUARDS.GUARD3,                 GUARDS.GUARD4             ]         }     } ]; 

4. Master Guard -

import { Injectable } from "@angular/core"; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from "@angular/router";  //import all the guards in the application import { Guard1 } from "./guard1"; import { Guard2 } from "./guard2"; import { Guard3 } from "./guard3"; import { Guard4 } from "./guard4";  import { Guard4DependencyService } from "./guard4dependency";  @Injectable() export class MasterGuard implements CanActivate {      //you may need to include dependencies of individual guards if specified in guard constructor     constructor(private _Guard4DependencyService:  Guard4DependencyService) {}      private route: ActivatedRouteSnapshot;     private state: RouterStateSnapshot;      //This method gets triggered when the route is hit     public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {          this.route = route;         this.state = state;          if (!route.data) {             Promise.resolve(true);             return;         }          //this.route.data.guards is an array of strings set in routing configuration          if (!this.route.data.guards || !this.route.data.guards.length) {             Promise.resolve(true);             return;         }         return this.executeGuards();     }      //Execute the guards sent in the route data      private executeGuards(guardIndex: number = 0): Promise<boolean> {         return this.activateGuard(this.route.data.guards[guardIndex])             .then(() => {                 if (guardIndex < this.route.data.guards.length - 1) {                     return this.executeGuards(guardIndex + 1);                 } else {                     return Promise.resolve(true);                 }             })             .catch(() => {                 return Promise.reject(false);             });     }      //Create an instance of the guard and fire canActivate method returning a promise     private activateGuard(guardKey: string): Promise<boolean> {          let guard: Guard1 | Guard2 | Guard3 | Guard4;          switch (guardKey) {             case GUARDS.GUARD1:                 guard = new Guard1();                 break;             case GUARDS.GUARD2:                 guard = new Guard2();                 break;             case GUARDS.GUARD3:                 guard = new Guard3();                 break;             case GUARDS.GUARD4:                 guard = new Guard4(this._Guard4DependencyService);                 break;             default:                 break;         }         return guard.canActivate(this.route, this.state);     } } 

Challenges

One of the challenges in this approach is refactoring of existing routing model. However, it can be done in parts as the changes are non-breaking.

I hope this helps.

like image 45
planet_hunter Avatar answered Sep 19 '22 00:09

planet_hunter