Angular 4 Multiple Guards - Execution Sequence
Asked Answered
A

4

18

I have 2 guards, AuthGuard and AccessGuard in the application. AuthGuard protects all the pages as the name suggests and stores the session object in the GlobalService and AccessGuard depends on the some access data in session object stored by AuthGuard in GlobalService.

Problem arises when AuthGuard returns an Observable and then simultaneously AccessGuard executes to check for session object which has not yet arrived and the code breaks. Is there any other way I can restrict the execution of AccessGuard until the session object arrives or any other work around to break this race condition?

#Note I have not merged the AccessGuard logic to AuthGuard as only some of the routes need to be checked for access while all other needs authentication. For example, Accounts page and DB page are accessible to all but User Managements and Dashboard need external access parameters that come from session object

export const routes: Routes = [
  {
    path: 'login',
    loadChildren: 'app/login/login.module#LoginModule',
  },
  {
    path: 'logout',
    loadChildren: 'app/logout/logout.module#LogoutModule',
  },
  {
    path: 'forget',
    loadChildren: 'app/forget/forget.module#ForgetModule',
  },{
    path: 'reset',
    loadChildren: 'app/reset/reset.module#ResetModule',
  },

    path: 'pages',
    component: Pages,
    children: [
      { path: '', redirectTo: 'db', pathMatch: 'full' },
      { path: 'db', loadChildren: 'app/pages/db/db.module#DbModule' },
      { path: 'bi', loadChildren: 'app/pages/dashboard/dashboard.module#DashboardModule', canActivate:[AccessableGuard] },
      { path: 'account', loadChildren: 'app/pages/account/account.module#AccountModule' },
      { path: 'um', loadChildren: 'app/pages/um/um.module#UserManagementModule', canActivate:[AccessableGuard] },
    ],
    canActivate: [AuthGuard]
  }
];

export const routing: ModuleWithProviders = RouterModule.forChild(routes);

#EDIT: Adding the Guard Codes

AuthGuard:

canActivate(route:ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean{
  return new Observable<boolean>( observer => {
    this._dataService.callRestful('POST', params.SERVER.AUTH_URL + urls.AUTH.GET_SESSION).subscribe(
        (accessData) => {
          if (accessData['successful']) {
            observer.next(true);
            observer.complete();
            console.log("done");
          }
          else {
            observer.next(false);
            observer.complete();
          }
        });
  });
}

AccessableGuard:

canActivate(route:ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean{        
if(this._dataService.getModulePermission(route.routeConfig.path.toUpperCase()) < 2){
        return false;
      }
      return true;
    }

#NOTE: _dataService is GlobalService that stores the Access Permissions from AuthGuard.

Anisometropia answered 10/5, 2017 at 13:2 Comment(0)
W
3

Take a look at this Angular guide (link). "If you were using a real world API, there might be some delay before the data to display is returned from the server. You don't want to display a blank component while waiting for the data.

It's preferable to pre-fetch data from the server so it's ready the moment the route is activated. This also allows you to handle errors before routing to the component...

In summary, you want to delay rendering the routed component until all necessary data have been fetched.

You need a resolver."

Westsouthwest answered 23/5, 2017 at 13:2 Comment(3)
Hey, Thanks. But CanActivateChild did a charm for me.Anisometropia
@AkulNarang How did you use CanActivate to solve this? Seems this is a pretty common issue with Guards there is not a great way to solve. Don't really want to put a resolver on every route that needs this type of guard.Apart
Which version of Angular are you using ?Anisometropia
R
24

I chose a different path --- Nesting my guards and making them dependencies of each other.

I have a RequireAuthenticationGuard and a RequirePermissionGuard. For most routes they need to both run but there is a specific order I require.

The RequireAuthenticationGuard depends on my authN services to check if the current session is authenticated.

The RequirePermissionGuard depends on my authZ services to check if the current session is authorized for a route.

I add the RequireAuthenticationGuard as a constructor dependency of RequirePermissionGuard and only begin checking permissions if authentication has been determined.

require-authentication.guard.ts

constructor(
    private userSessionSerivce: UserSessionService) {}

canActivate(
    _route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
): Observable<boolean> {
    return this.validateAuthentication(state.url);
}

require-permission.guard.ts

constructor(
    private permissionService: PermissionService,
    /**
    * We use the RequireAuthenticationGuard internally
    * since Angular does not provide ordered deterministic guard execution in route definitions
    *
    * We only check permissions once authentication state has been determined
    */
    private requireAuthenticationGuard: RequireAuthenticatedGuard,
) {}

canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
): Observable<boolean> {
    const requiredPermissions: Permission[] = next.data.permissions || [];

    return this.requireAuthenticationGuard
        .canActivate(next, state)
        .pipe(
            mapTo(this.validateAuthorization(state.url, requiredPermissions)),
        );
}
Rotator answered 7/9, 2018 at 22:30 Comment(4)
gotta say this is probably the most elegant way of doing this.Domella
I tried this but permissionGuard service is being called before authGuardService completes.Dotson
Seems like if you chain them like this that you should not put the RequireAuthenticatedGuard in the CanActivate array. If you do, it would be called twice. Once by the router and once by the permission guard.Phenothiazine
@Phenothiazine It shouldn't matter too much how many times those methods get called. What matters is, are they expensive calls? If so, the result could be cached. Also, it would be ideal if the source of the AuthN/AuthZ state was a hot observable. That way it could be read without triggering any expensive operation.Rotator
M
5

Using a Master Guard to fire application guards can do the trick.

EDIT : Adding the code snippet for better understanding.

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.

Mennonite answered 7/12, 2017 at 15:17 Comment(2)
this is great, but what if we need both guards to pass true? I implemented this solution, and the route can be activated as long as one of the guards returns true, which sort of negates the idea of having multiple guards.Darrin
I think you are missing something, this approach works like AND condition so if any of the guards fail, the route won't get hit. If you see executeGuards method, it is getting called recursively only if previous guard passes true or promise<true>Mennonite
W
3

Take a look at this Angular guide (link). "If you were using a real world API, there might be some delay before the data to display is returned from the server. You don't want to display a blank component while waiting for the data.

It's preferable to pre-fetch data from the server so it's ready the moment the route is activated. This also allows you to handle errors before routing to the component...

In summary, you want to delay rendering the routed component until all necessary data have been fetched.

You need a resolver."

Westsouthwest answered 23/5, 2017 at 13:2 Comment(3)
Hey, Thanks. But CanActivateChild did a charm for me.Anisometropia
@AkulNarang How did you use CanActivate to solve this? Seems this is a pretty common issue with Guards there is not a great way to solve. Don't really want to put a resolver on every route that needs this type of guard.Apart
Which version of Angular are you using ?Anisometropia
V
2

Just create a master guard which one injects the sub guards, here is an example:

app.guard.ts

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { GuardA } from '...';
import { GuardB } from '...';

@Injectable({
    providedIn: 'root',
})
export class AppGuard implements CanActivate {

    constructor(
        // inject your sub guards
        private guardA: GuardA,
        private guardB: GuardB,
    ) {
    }

    public async canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        for (const guard of this.getOrderedGuards()) {
            if (await guard.canActivate(next, state) === false) {
                return false;
            }
        }
        return true;
    }

 // -> Return here the sub guards in the right order
    private getOrderedGuards(): CanActivate[] {
        return [
            this.guardA,
            this.guardB,
        ];
    }
}

Then in your app-routing.module.ts

const routes: Routes = [
    {
        path: 'page',
        loadChildren: './pages.module#PageModule',
        canActivate: [AppGuard],
    }
];

Of course you have to manage your modules so that the guards are provided (understand injectable) into your AppGuard.

Valeric answered 13/11, 2019 at 14:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.