5

I want to use several roles for accessing views in the application, if I use one role everything works correctly, however when I use several roles, the views do not give access

My model User have this:

export class User {
    role: Role[];                // I change - role: Role[] for few roles
    expiresIn: string;
    aud: string;
    iss: string;
    token?: string;
}

export const enum Role {
    Admin = 'admin',
    User = 'user',   
    Engineer = 'engineer'
}

my backend give my token with with roles:

//....
role: (2) ["admin", "engineer"]
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ
//....

If I use this in login metod

tokenInfo['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'][0]   - first element in array

i have only 1 role, and code work fine, but I can have many users who belong to different roles, and I need the application to give them access if there are at least 1 role

I handle token decoding and getting roles in authorization service

signin(username:string, password:string ) {
    return this.http.post<User>(`${environment.apiUrl}${environment.apiVersion}Profile/Login`, {username, password})    
    .pipe(map(user => {
    if (user && user.token) {
      let tokenInfo = this.getDecodedAccessToken(user.token); // decode token
      this.session = {
        token: user.token,
        role: tokenInfo['http://schemas.microsoft.com/ws/2008/06/identity/claims/role'],     - add this [0]
        expiresIn: tokenInfo.exp,
        aud: tokenInfo.aud,
        iss: tokenInfo.iss,            
      }
      localStorage.setItem('currentUser', JSON.stringify(this.session));
      this.currentUserSubject.next(this.session);         
    }
    return this.session;
    }))
} 

sigin metod for example

Login() {
    this.auth.signin(this.signinForm.value.email, this.signinForm.value.password)
        .pipe(first())
        .subscribe(
            data => {
                console.log("User is logged in");
                this.router.navigate(['/dashboard']);
                this.loading = false;
            });
  }

Not sure if I correctly specify multiple access roles

//......
const adminRoutes: Routes = [
{
    path: 'dashboard',
    loadChildren: () => import('./views/dashboard/dashboard.module').then(m => m.DashboardModule),
    canActivate: [AuthGaurd],

},
{
    path: 'books',
    loadChildren: () => import('./views/books/books.module').then(m => m.BooksModule),
    canActivate: [AuthGaurd],
    data: { roles: [Role.Admin] }  <- work fine if 1 role
},
{
    path: 'person',
    loadChildren: () => import('./views/person/person.module').then(m => m.PersonModule),
    canActivate: [AuthGaurd],    
    data: { roles: [Role.Admin, Role.Engineer] }  <- if have 1 role - admin - open
 },
 {
    path: 'eqip',
    loadChildren: () => import('./views/eqip/eqip.module').then(m => m.PersonModule),
    canActivate: [AuthGaurd],    
    data: { roles: [Role.Engineer] }  <- not open becouse only admin role
 }];

const routes: Routes = [
{
    path: '',
    redirectTo: 'applayout-sidebar-compact/dashboard/v1',
    pathMatch: 'full',
},
...
{
    path: '**',
    redirectTo: 'others/404'
}];

@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }
//......

and guard sevice

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const url: string = state.url;
const currentUser = this.auth.currentUserValue;    
// in auth.service.ts
// constructor(private http: HttpClient) {
//   this.currentUserSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('currentUser')));
//   this.currentUser = this.currentUserSubject.asObservable();

// }
// public get currentUserValue(): User {
//   return this.currentUserSubject.value;
// }
if (this.auth.isUserLoggedIn()) {


  // test code
  const ter = route.data.roles.includes(currentUser.role) <- Error now here
  console.log(ter)  



  // main check role code
  // if (route.data.roles && route.data.roles.indexOf(currentUser.role) === -1) {
  //   this.router.navigate(["/"]);
  //   return false;
  // }

  return true;

}
this.auth.setRedirectUrl(url);
this.router.navigate([this.auth.getLoginUrl()]);
return false;

}

token in localStorage:

aud: "Service"
expiresIn: 1591967261
iss: "USs"
role: ["admin", "engineer"]
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHR....

change app-routing.module.ts

@NgModule({
imports: [RouterModule.forRoot(routes, { 
    useHash: true,
    initialNavigation: 'enabled',
    paramsInheritanceStrategy: 'always',
    relativeLinkResolution: 'corrected',
    scrollPositionRestoration: 'enabled',
})],
exports: [RouterModule]

Error

Uncaught (in promise): TypeError: Cannot read property 'includes' of undefined

TypeError: Cannot read property 'includes' of undefined

3 Answers3

4

It could also be that a typescript enum is not a string. so comparing a enum with a string will never be true.

What you need to use is a const enum because that compiles down to a string.

try changing to

export const enum Role {
    Admin = 'admin',
    User = 'user',   
    Engineer = 'engineer'
}

Though this does have other implications. https://www.typescriptlang.org/docs/handbook/enums.html#const-enums

and instead of doing a indexOf you can use .includes

route.data.roles.includes(currentUser.role)

Edit: It could also be that your data is not inherited down to where you are trying to get it.

You might need to add this to your Router config

RouterModule.forRoot([], {
      initialNavigation: 'enabled',
      paramsInheritanceStrategy: 'always', <-- this makes params and data accessible lower down into the tree
      relativeLinkResolution: 'corrected',
      scrollPositionRestoration: 'enabled',
    }),

Leon Radley
  • 7,596
  • 5
  • 35
  • 54
  • add 'const' and update import but still have a mistake 'Cannot read property 'indexOf' of undefined' if try check roles in guard if (route.data.roles.indexOf(currentUser.role) === -1) { console.log('not have roles) } – Ярослав Прохоров Jun 02 '20 at 08:05
  • I've added an example of using .includes instead, it is easier to understand than indexOf. But your problem with data.roles being undefined, is because you are not passing any roles in the route config? – Leon Radley Jun 02 '20 at 09:20
  • Cannot read property 'includes' of undefined now, in routing i have this - data: { roles: [Role.Engineer] } – Ярослав Прохоров Jun 02 '20 at 09:38
  • make sure route.data contains your info before trying to work with it. But it could also be that your data does not get inherited down. I've updated my answer with more info – Leon Radley Jun 02 '20 at 11:37
2

This really depends on how you are handling your AuthGuard code. There is a comprehensive tutorial on how to set up your Authentication and Authorization in this guide: https://jasonwatmore.com/post/2018/11/22/angular-7-role-based-authorization-tutorial-with-example

Big area where you could be experiencing the issue is on your AuthGuard. You can have this example from the link I shared above:

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

import { AuthenticationService } from '@/_services';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private authenticationService: AuthenticationService
    ) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const currentUser = this.authenticationService.currentUserValue;
        if (currentUser) {
            // check if route is restricted by role
            if (route.data.roles && route.data.roles.indexOf(currentUser.role) === -1) {
                // role not authorised so redirect to home page
                this.router.navigate(['/']);
                return false;
            }

            // authorised so return true
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});
        return false;
    }
}

You also need to make sure you are passing the right roles into your AuthGuard.

If you want deeper restrictions in the future, there's also this guide: How to prevent actions by user role in Angular

Hope this helps!

Algef Almocera
  • 759
  • 1
  • 6
  • 9
1

In your route config, there are some routes which don't need to check roles property in data. Assuming everybody should have access to them.

Change your auth guard to :-

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    const url: string = state.url;
    const currentUser = this.auth.currentUserValue;
    console.log(currentUser);
    if (this.auth.isUserLoggedIn()) {
      if (!route.data.roles || route.data.roles.length === 0) {
        return true;
      }
      if (typeof currentUser.role === 'string' && route.data.roles.includes(currentUser.role)) {
        return true;
      }
      if (Array.isArray(currentUser.role)) {
        for (let i = 0; i < currentUser.role.length; i++) {
          if (route.data.roles.includes(currentUser.role[i])) {
            return true;
          }
        }
      }
      this.router.navigate([this.auth.getLoginUrl()]); //TODO: Change to 403 PAGE (403 forbidden)
      return false;
    }
    this.auth.setRedirectUrl(url);
    this.router.navigate([this.auth.getLoginUrl()]);
    return false;
}
Aakash Garg
  • 10,649
  • 2
  • 7
  • 25