Search code examples
angularobservablerbacbehaviorsubjectangular-guards

Angular - Cannot get current user's role in guard to check if the he/she has permission to access protected routes


I am trying to implement role based access control on Angular based project where there are multiple types of roles

I want to get user's data(role property) in CanActivate interface to check if the user has permission to access protected routes in guard, but BehaviouralSubject does not updating initial state after receiving latest values from API.

Thus I am getting user's data in authentification service, then passing received value to userRole subject, but updated property (BehaviouralSubject) is not available in guard file.

Authentification Service

    constructor() {
      let localToken: string
      if (localStorage.getItem('token')) {
        localToken = localStorage.getItem('token')
        this.getUserInfo(localToken)
      }
    }
      userRole = new BehaviorSubject(null)
      userRole$ = this.userRole.asObservable()    

      // called in constructor of a auth. service (every time when user called) 
      // and after login as well, so user's data always available. 
      getUserInfo(data) {
          this.requestService
            .post('GET_USER_INFO', data)
            .subscribe((response: ILoggedInUser) => {
               // data from API {email: "[email protected]", userName: "ccc", role: "viewer"}
               this.userRole.next(response.user)  
            })
        }

Auth Guard

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {

      return this.authService.userRole$.pipe(
        map(member => {
          if (member) {
            // check if route is restricted by role
            if (
              route.data.roles &&
              route.data.roles.indexOf(member.role) === -1
            ) {
              // role not authorised so redirect to home page
              this.router.navigate(['/login'])
              return false
            }

          return true
          } else {
            this.router.navigate(['/login'])
            return false
          }
        }),
      )
    }

Here is routing:

     const routes: Routes = [
      {
        path: 'home',
        component: HomeComponent,
      },
      {
        path: 'admin',
        component: AdminComponent,
        canActivate: [AuthGuard],
        data: { roles: ['admin'] },
      },
      {
        path: 'editor',
        component: EditorsComponent,
        canActivate: [AuthGuard],
        data: { roles: ['admin', 'editor'}
      },
      {
        path: 'viewer',
        component: ViewersComponent,
        canActivate: [AuthGuard],
        data: { roles: ['admin', 'viewer'] },
      }
    ]

I tried another approach as well, (for example using Subscriptions) but when I try to subscribe to userRole subject like this:

     return this.authService.userRole.subscribe((member) => {
        .......
        ....... 
     }

then there is a Property 'canActivate' in type 'AuthGuard' is not assignable to the same property in base type 'CanActivate' error, since Subscription type is not assignable to type boolean.

Therefore, could you give any hint OR recommend the better way of handling RBAC in angular with observables.

P.S.

I have seen solutions with localStorage, but I don't want to keep user's data (except token) there.


Solution

  • The issue is that userRole$ observable emmits null initially because it is subscribed to userRole BehaviorSubject which gets null as an argument.

    Hence, userRole$ observable is equal to null getting an update and property canActivate in type AuthGuard cannot return null.

    Therefore you should use filter() operator before passing object to map in order to get rid of null.

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
      return this.authService.userRole$.pipe(
        filter(member => !!member),  // Filter NULL value here before sending object further
            map(member => {
            ....
          return true
    
          } else {
            ....
          }
        )
    }