Pass parameter into route guard
Asked Answered
N

9

169

I'm working on an app that has a lot of roles that I need to use guards to block nav to parts of the app based on those roles. I realize I can create individual guard classes for each role, but would rather have one class that I could somehow pass a parameter into. In other words I would like to be able to do something similar to this:

{ 
  path: 'super-user-stuff', 
  component: SuperUserStuffComponent,
  canActivate: [RoleGuard.forRole('superUser')]
}

But since all you pass is the type name of your guard, can't think of a way to do that. Should I just bit the bullet and write the individual guard classes per role and shatter my illusion of elegance in having a single parameterized type instead?

Nubble answered 10/3, 2017 at 13:22 Comment(0)
H
336

Instead of using forRole(), you can do this:

{ 
   path: 'super-user-stuff', 
   component: SuperUserStuffComponent,
   canActivate: [RoleGuard],
   data: {roles: ['SuperAdmin', ...]}
}

and use this in your RoleGuard

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot)
    : Observable<boolean> | Promise<boolean> | boolean  {

    let roles = route.data.roles as Array<string>;
    ...
}
Hyonhyoscine answered 10/3, 2017 at 15:2 Comment(11)
Great option as well, thanks. I like Aluan's factory method approach just a trace bit better though, but thanks for expanding my brain on the possibilities!Nubble
Is this secure? can someone just post this data?Screen
I think the secureness of this data is irrelevant. You have to use authentication and authorization on server side. I thinkg the point of the guard is not to fully protect your application. If someone "hacks" it and navigates to the admin page, he/she will not get the secure data from the server only just see you admin components which is ok in my opinion. I think this is much more better solution the the accepted one. The accepted solution breaks the dependency injection.Sacttler
This is good solution and it works great in my generic AuthGuard.Iodine
Good solution, but what if I want to use the RoleGuard twice (or any)? It is more likely to append with Resolve, { user1: UserResolver(1), user2: UserResolver(2) }. Of course, I want to be able to use any attribute name, and any number of guards or resolver. Looks like there is design lack with Resolve & Guard.Vibratile
ok, How do you test this? const canActivate = <Promise<boolean>>authGuard.canActivate(); //error expect parameter canActivate.then(isConnected => expect(isConnected).toBeFalsy());Maturation
This solution works great. My issue is that it relies on a layer of indirection. There's no way someone looking at this code would realize that roles object and the route guard are linked without knowing how the code works ahead of time. It sucks that Angular doesn't support a way to do this in a more declarative way. (To be clear this is me bemoaning Angular not this perfectly reasonable solution.)Ehling
@Ehling thank you, yes but I used this solution much times ago and i moved to another solution. My new solution is one RoleGaurd and one file with "access.ts" name with Map<URL, AccessRoles> constant in it, then I using it in RoleGaurd. If you want control your accesses in your app, i think this new way is much better especially when you have more than one app in one project.Hyonhyoscine
I get can't inject ActivatedRouteSnapshot with this... Anyone else have that problem?Hance
How can I pass dynamic value or variable here instead ['SuperAdmin', ...]. I have a object in guard and I want to pass itBobsledding
How to pass the param if there are multiple guards in canActivate Array?Sandell
S
25

As of 2022 you can use CanActivateFn (https://angular.io/api/router/CanActivateFn). This function returns a CanActivateFn instance:

// Returns a function which can act as a guard for a route
function requireAnyRole(...roles: Role[]): CanActivateFn {
  return (ars: ActivatedRouteSnapshot, rss: RouterStateSnapshot) => {
    // do some checks here and return true/false/observable
    // can even inject stuff with inject(ClassOrToken)
  }
}

then you can use it when defining routes

{
  path: 'some/path',
  component: WhateverComponent,
  canActivate: [requireAnyRole(Role1, Role2, Role3)]
}
Seminal answered 17/10, 2022 at 23:4 Comment(5)
Just tried that solution. It works nicely. Definitely the best solution if you use Angular 14 or higher.Lawford
Thank you for this answer. In 2023, for me thats the best way to do it. Works like charm. Little extra: I needed to export the function to use it in the canActivateGeanticlinal
It works well, but I couldn't figure out how to write specs for it. When using TestBed.runInInjectionContext(isLoggedIn(false)) I get Error: NG0204: Can't resolve all parameters for isLoggedIn: (?). - any ideas?Xanthene
@FlorianGössele When you follow the above pattern, think of it as "nested" functions. Without seeing your full code, I would assume you want to do: isLoggedIn(false)({} as ActivatedRouteSnapshot, {} as RouterStateSnapshot)Encomiast
@Encomiast I got it to run back then: #77098223Xanthene
K
15

Here's my take on this and a possible solution for the missing provider issue.

In my case, we have a guard that takes a permission or list of permissions as parameter, but it's the same thing has having a role.

We have a class for dealing with auth guards with or without permission:

@Injectable()
export class AuthGuardService implements CanActivate {

    checkUserLoggedIn() { ... }

This deals with checking user active session, etc.

It also contains a method used to obtain a custom permission guard, which is actually depending on the AuthGuardService itself

static forPermissions(permissions: string | string[]) {
    @Injectable()
    class AuthGuardServiceWithPermissions {
      constructor(private authGuardService: AuthGuardService) { } // uses the parent class instance actually, but could in theory take any other deps

      canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
        // checks typical activation (auth) + custom permissions
        return this.authGuardService.canActivate(route, state) && this.checkPermissions();
      }

      checkPermissions() {
        const user = ... // get the current user
        // checks the given permissions with the current user 
        return user.hasPermissions(permissions);
      }
    }

    AuthGuardService.guards.push(AuthGuardServiceWithPermissions);
    return AuthGuardServiceWithPermissions;
  }

This allows us to use the method to register some custom guards based on permissions parameter in our routing module:

....
{ path: 'something', 
  component: SomeComponent, 
  canActivate: [ AuthGuardService.forPermissions('permission1', 'permission2') ] },

The interesting part of forPermission is AuthGuardService.guards.push - this basically makes sure that any time forPermissions is called to obtain a custom guard class it will also store it in this array. This is also static on the main class:

public static guards = [ ]; 

Then we can use this array to register all guards - this is ok as long as we make sure that by the time the app module registers these providers, the routes had been defined and all the guard classes had been created (e.g. check import order and keep these providers as low as possible in the list - having a routing module helps):

providers: [
    // ...
    AuthGuardService,
    ...AuthGuardService.guards,
]

Hope this helps.

Kalvin answered 31/5, 2017 at 13:17 Comment(3)
This solution gives me a static error: ERROR in Error encountered resolving symbol values statically.Preoccupy
This solution worked for me for development, but when I build the application for production in throws error ERROR in Error during template compile of 'RoutingModule' Function calls are not supported in decorators but 'PermGuardService' was called.Oliviero
Does this work with lazy loaded modules that have their own routing modules?Vermination
M
6

Another option combination of approach with data and factory function:

export function canActivateForRoles(roles: Role[]) {
  return {data: {roles}, canActivate: [RoleGuard]}
}

export class RoleGuard implements CanActivate {
  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot)
      : Observable<boolean> | Promise<boolean> | boolean  {
  
      const roles = route.data.roles as Role[];
    ...
  }
}

...

{ path: 'admin', component: AdminComponent, ...canActivateWithRoles([Role.Admin]) },

Mazurka answered 22/1, 2021 at 20:2 Comment(1)
Limitation of using Spread Syntax (...) here means any data property defined in your routes will be overwritten by ...canActivateWithRoles([...]). For example, this will not work {/* ... */, data: { title: 'MyTitle' }, ...canActivateWithRoles([Role.Admin]) }.Carlsbad
D
6

You can write your role guard like this:

export class RoleGuard {
  static forRoles(...roles: string[]) {

    @Injectable({
      providedIn: 'root'
    })
    class RoleCheck implements CanActivate {
      constructor(private authService: AuthService) { }
        canActivate(): Observable<boolean> | Promise<boolean> | boolean {
          const userRole = this.authService.getRole();

          return roles.includes(userRole);
        }
      }

      return RoleCheck;
    }

}

And use it like this with multiple roles as well if you wish:

{ 
  path: 'super-user-stuff', 
  component: SuperUserStuffComponent,
  canActivate: [RoleGuard.forRoles('superUser', 'admin', 'superadmin')]
}
Danikadanila answered 19/5, 2022 at 0:49 Comment(0)
D
2

@AluanHaddad's solution is giving "no provider" error. Here is a fix for that (it feels dirty, but I lack the skills to make a better one).

Conceptually, I register, as a provider, each dynamically generated class created by roleGuard.

So for every role checked:

canActivate: [roleGuard('foo')]

you should have:

providers: [roleGuard('foo')]

However, @AluanHaddad's solution as-is will generate new class for each call to roleGuard, even if roles parameter is the same. Using lodash.memoize it looks like this:

export var roleGuard = _.memoize(function forRole(...roles: string[]): Type<CanActivate> {
    return class AuthGuard implements CanActivate {
        canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
            Observable<boolean>
            | Promise<boolean>
            | boolean {
            console.log(`checking access for ${roles.join(', ')}.`);
            return true;
        }
    }
});

Note, each combination of roles generates a new class, so you need to register as a provider every combination of roles. I.e. if you have:

canActivate: [roleGuard('foo')] and canActivate: [roleGuard('foo', 'bar')] you will have to register both: providers[roleGuard('foo'), roleGuard('foo', 'bar')]

A better solution would be to register providers automatically in a global providers collection inside roleGuard, but as I said, I lack the skills to implement that.

Donaldson answered 23/4, 2017 at 14:33 Comment(1)
I really like this functional approach but mixin closures with DI(classes) look like overhead.Blowup
I
2

Another solution could be to return an InjectionToken and use a factory method:

export class AccessGuard {
  static canActivateWithRoles(roles: string[]) {
    return new InjectionToken<CanActivate>('AccessGuardWithRoles', {
      providedIn: 'root',
      factory: () => {
        const authorizationService = inject(AuthorizationService);

        return {
          canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): <boolean | UrlTree > | Promise<boolean | UrlTree> | boolean | UrlTree {
              return authorizationService.hasRole(roles);
          }
        };
      },
    });
  }
}

And use it like this:

canActivate: [AccessGuard.canActivateWithRoles(['ADMIN'])]
Ivette answered 4/2, 2022 at 13:4 Comment(0)
C
1

Going to add a good Angular 16 method

      {
        path: 'compliance',
        component: ComplianceMainComponent,
        canActivate: [permissionGuard(ManagerPermission.COMPLIANCE)],
      },

Warp the Guard with another function that takes the param and returns Guard

export const permissionGuard = (permission: ManagerPermission) =>

  (route: ActivatedRouteSnapshot, state: RouterStateSnapshot):

    Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree => {

    const store = inject(Store);

    return store.select(fromStore.getManagerPermissions).pipe(
      take(1),

      map((permissions) => permissions.includes(permission)),
    );
  };
Cloots answered 4/12, 2023 at 15:47 Comment(0)
O
0

There is a way to do it with useFactory and providers:

const routes: Routes = [
 { 
   path: 'super-user-stuff', 
   component: SuperUserStuffComponent,
   // Name can be whatever you want
   canActivate: ['CanActiveSuperUserStuffGuard']
 }
]

And in providers you will need to add following:

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
  providers: [
    {
      provide: 'CanActiveSuperUserStuffGuard',
      useFactory: () => new RoleGuard('superUser')
    }
  ]
})
export class YourRoutingModule {
}

To make this work you will also need to change scope of your Guard removing providedIn: 'root' (Just leave @Injectable()) and pass parameteur into constructor as following (in your guard file):

  constructor(@Inject('roleName') private readonly roleName: string) {
  } 

!!! BE AWARE !!! Using this approach will create a new instance of guard for every such declaration

Othilia answered 20/6, 2022 at 15:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.