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: "test@test.com", 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.
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 {
....
}
)
}