Execute Multiple Asynchronous Route Guards in Order
Asked Answered
S

5

11

I know angular route guards execute in the specified order when the canActivate function returns a simple boolean, however, what if the guards return type Observable<boolean> or Promise<boolean>?

Example in route:

{
    path: 'confirm',
    canActivate: [AuthGuard, SessionExpiredAuthGuard, CheckoutAuthGuard],
    component: CheckoutReviewOrderComponent
},

SessionExpiredAuthGuard and CheckoutAuthGuard both return type Observable<boolean>. I don't want the CheckoutAuthGuard to be executed before the SessionExpiredAuthGuard is finished retrieving it's data from the asynchronous http request.

Is there any way to force these asynchronous guards to execute in order?

Spates answered 19/6, 2017 at 22:48 Comment(3)
Have you actually seen them acting out of order? I think they're resolved with concatAll, which would call them in series not in parallel.Chagall
Yes. Sometimes the CheckoutAuthGuard finishes before the previous one.Spates
@jonrsharpe, I am however using the finally method inside my SessionExpiredAuthGuard... would that get executed last on the whole stack of observables?Spates
S
16

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.

Sinclair answered 7/12, 2017 at 14:48 Comment(4)
I am getting error on "return guard.canActivate(this.route, this.state);" Error is: Type 'boolean | Observable<boolean>' is not assignable to type 'boolean'. Type 'Observable<boolean>' is not assignable to type 'boolean'.Testimony
Can you post the code through - stackblitz.com ?Sinclair
Its resolved! Just changed activateGuard as async and modified this line return guard.canActivate(this.route) as Promise<boolean>Testimony
hi @Sinclair instead of declaring data with attributes with auth guards, is there any alternative to add multiple auth guard references in masterguard and refer only masterguard in routing configuration?Threepiece
C
2

In addition to the answer planet_hunter, I dare to share a little improvement master-guard

Connie answered 28/2, 2019 at 16:59 Comment(0)
B
2

With Angular 15's functional guards and the recently upgraded inject function, it's now possible to write an elegant function that executes async guards in order.

For example, let's assume that all guards return an Observable<boolean | UrlTree>:

interface AsyncGuard extends CanActivate {
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree>;
}

You could then write a universal ordering function like this:

function orderedAsyncGuards(
  guards: Array<new () => AsyncGuard>
): CanActivateFn {
  return (route, state) => {
    // Instantiate all guards.
    const guardInstances = guards.map(inject) as AsyncGuard[];
    // Convert an array into an observable.
    return from(guardInstances).pipe(
      // For each guard, fire canActivate and wait for it to complete.
      concatMap((guard) => guard.canActivate(route, state)),
      // Don't execute the next guard if the current guard's result is not true.
      takeWhile((value) => value === true, /* inclusive */ true),
      // Return the last guard's result.
      last()
    );
  };
}

Then you can use it in the route configuration like this:

const ROUTE = {
  ...
  canActivate: [orderedAsyncGuards([FirstGuard, SecondGuard])]

Here's a working StackBlitz example.

Bowker answered 19/12, 2022 at 11:24 Comment(0)
S
0

Here is my solution inspired by @planet_hunter which is fully compatible with Angular 8's CanActivate signature:

Multiple canActivate guards all run when first fails

Sambo answered 27/11, 2019 at 20:52 Comment(0)
U
0

One perfect explanation I was able to get,

Angular guards wouldn't run synchronously by any chance, except for any workaround.

But even though guards run parallely, the routers would execute based on priority of the guards.

which is well explained here by https://stackoverflow.com/users/1697459/wilt

https://mcmap.net/q/267705/-multiple-canactivate-guards-all-run-when-first-fails/63955377#63955377]

Ukase answered 4/1 at 6:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.