Angular Guards priority trap

Angular has a great tool for restricting navigation between pages out of the box. But, like any big project, it has its pitfalls. Today I will tell you about one of them.

Refresh your knowledge before reading this article Angular Guards (hereinafter guards). The article will be about CanActivate, but this applies to the rest of the guards as well. Now let’s move on to the history of priority discovery.

First try

I recently developed a new feature for admins. It was necessary to restrict access to the page – let the user in if he has a certain feature flag and he is an administrator, otherwise redirect to home.

It seems like a trivial task. A solution immediately appeared in my brain with the help of a guard CanActivate. Suppose there are already two services for these two conditions. Next, we call the necessary methods, combine them into one Observable and further if at least one returned falsethen redirect to the main page if both truethen let the user access the page.

@Injectable()
export class AdminGuard implements CanActivate {
  constructor(
    private readonly featureToggleService: FeatureToggleService,
    private readonly userService: UserService,
    private readonly router: Router
  ) {}

  canActivate(): Observable<boolean | UrlTree> {
    const isAdminPageEnabled$ =
      this.featureToggleService.isAdminPageEnabled$.pipe(startWith(null));

    const isAdmin$ = this.userService.isAdmin$.pipe(startWith(null));

    return combineLatest([isAdminPageEnabled$, isAdmin$]).pipe(
      first(
        conditions =>
          conditions.some(condition => condition === false) ||
          conditions.every(Boolean)
      ),
      map(
        ([isAdminPageEnabled, isAdmin]) =>
          (isAdminPageEnabled && isAdmin) || this.router.createUrlTree(['/'])
      )
    );
  }
}

The code works, but if you look closely, you can write better. The implementation has too much responsibility for one small guard, and the code is not readable. Let’s fix this.

Refactoring

Since there is too much responsibility, you can divide the guards into two, given that the property CanActivate at the route accepts array of guards.

// admin-page-feature.guard.ts
@Injectable()
export class AdminPageFeatureGuard implements CanActivate {
  constructor(
    private readonly featureToggleService: FeatureToggleService,
    private readonly router: Router
  ) {}

  canActivate(): Observable<boolean | UrlTree> {
    return this.featureToggleService.isAdminPageEnabled$.pipe(
      map(
        isAdminPageEnabled =>
          isAdminPageEnabled || this.router.createUrlTree(['/'])
      )
    );
  }
}

// admin.guard.ts
@Injectable()
export class AdminGuard implements CanActivate {
  constructor(
    private readonly userService: UserService,
    private readonly router: Router
  ) {}

  canActivate(): Observable<boolean | UrlTree> {
    return this.userService.isAdmin$.pipe(
      map(isAdmin => isAdmin || this.router.createUrlTree(['/']))
    );
  }
}

Now there are two guards that can be reused, and the code has become readable. It would seem that everything is fine, but at this moment I was puzzled by one question. Guards in an array CanActivate run in series or in parallel?

const routes: Routes = [
  {
    path: 'admin',
    component: AdminPageComponent,
    canActivate: [AdminGuard, AdminPageFeatureGuard],
  },
];

Things are bad for me if the guards are executed sequentially, since there is no point in forcing the user to wait for each guard separately.

I did some research and found out that they run in parallel, but with some nuances.

Guard Priorities

In the distant seventh version of Angular, priority for guards was introduced. Passed to array for CanActivate the guards will run in parallel, but the router will wait for the higher priority to complete before moving on. Priority is determined by the order in the array СanActivate.

Let’s look at an example. There are two guards HighPriorityGuard and LowPriorityGuard. Both return Observable. We are navigating the route where these guards are applied.

canActivate: [HighPriorityGuard, LowPriorityGuard]

Facts about performing guards:

  • Run in parallel.

  • If a LowPriorityGuard returns UrlTree|false first, then the router will still wait for the execution HighPriorityGuard before starting navigation.

  • If a HighPriorityGuard returns first UrlTree it will immediately navigate to UrlTree from HighPriorityGuard.

  • If a LowPriorityGuard returns UrlTree first and then HighPriorityGuard returns UrlTreethen navigation will take place on UrlTree from HighPriorityGuardsince the higher priority wins.

  • If a HighPriorityGuard returns true first, the router will wait LowPriorityGuardbecause it cannot navigate until it is sure that all guards return true.

If you have questions about guard priorities, please go to this linkI prepared a small demo, it will be easier to figure it out there.

Why priorities are needed

The Anuglar team has added priority for guards to solve the problem of multiple redirects. Let’s say the application has two guards – authentication and admin. The authentication guard will send you to the login page if the user is not logged in. And the admin guard will send to the “You are not an admin” page if the user does not have the required role. Prior to version 7 of Angular, navigation was done from the guard, which was the first to redirect. And it turned out that a user who is not logged in can get to the “You are not an admin” page, because the admin guard was completed faster.

But now we have UrlTree and the priority of the guards. If the authentication guard is more important, then we put it first in the array, thereby giving it a higher priority, then the router will wait for it to complete and redirect to UrlTree out of him.

Brief summary

Give guards only one responsibility, then they are easier to read and reuse. But remember the priority trap. If you have the first guard executed in a second, and the second in an hour, and there is no difference in the redirect, then you should think about the order in the array CanActivate/CanActivateChild/CanLoad/CanDeactivateso that sometimes the user doesn’t have to wait one hour.

I wrote my own solution to bypass the priority of guards, because I was categorically not satisfied with making the user wait, but more on that in the next article.

Similar Posts

Leave a Reply