164

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?

Brian Noyes
  • 1,880
  • 2
  • 12
  • 10

8 Answers8

330

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>;
    ...
}
Raphaël Balet
  • 6,334
  • 6
  • 41
  • 78
Hasan Beheshti
  • 3,505
  • 1
  • 9
  • 15
  • 1
    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! – Brian Noyes Mar 11 '17 at 18:56
  • Is this secure? can someone just post this data? – Jeff May 12 '17 at 14:10
  • 11
    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. – bucicimaci May 15 '17 at 15:10
  • 1
    This is good solution and it works great in my generic AuthGuard. – SAV Jun 07 '17 at 06:14
  • 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. – Nico Toub Jul 20 '17 at 09:53
  • ok, How do you test this? const canActivate = >authGuard.canActivate(); //error expect parameter canActivate.then(isConnected => expect(isConnected).toBeFalsy()); – gyc Mar 15 '18 at 08:29
  • 10
    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.) – KhalilRavanna Jan 14 '19 at 19:38
  • 1
    @KhalilRavanna 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 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. – Hasan Beheshti Jan 16 '19 at 18:34
  • I get can't inject ActivatedRouteSnapshot with this... Anyone else have that problem? – caden311 Jan 31 '20 at 22:58
  • How can I pass dynamic value or variable here instead `['SuperAdmin', ...]`. I have a object in guard and I want to pass it – Ambuj Khanna Mar 06 '20 at 09:41
  • How to pass the param if there are multiple guards in canActivate Array? – Muhammad Awais Jul 10 '20 at 11:54
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.

Ovidiu Dolha
  • 5,335
  • 1
  • 21
  • 30
  • 1
    This solution gives me a static error: ERROR in Error encountered resolving symbol values statically. – Arninja Jun 07 '17 at 07:58
  • 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.` – kpacn May 13 '19 at 12:05
  • Does this work with lazy loaded modules that have their own routing modules? – crush Jun 06 '19 at 14:29
15

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)]
}
tsiki
  • 1,638
  • 21
  • 34
  • Just tried that solution. It works nicely. Definitely the best solution if you use Angular 14 or higher. – spierala Mar 06 '23 at 16:40
  • 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 canActivate – himtim Apr 21 '23 at 12:48
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]) },

Timofey Yatsenko
  • 758
  • 10
  • 14
  • 5
    Limitation of using [Spread Syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/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]) }`. – scraymer Jan 31 '21 at 14:17
5

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')]
}
D Lai
  • 53
  • 2
  • 4
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.

THX-1138
  • 21,316
  • 26
  • 96
  • 160
  • I really like this functional approach but mixin closures with DI(classes) look like overhead. – BILL May 09 '19 at 09:12
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'])]
maechler
  • 1,257
  • 13
  • 18
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

Vasyl Kobetiak
  • 221
  • 4
  • 4