Angular How-to: Implement Role-based security

Premier

March 7th, 2018

Laurie Atkinson, Premier Developer Consultant, shows us how to customize the behavior of an Angular app based on the user’s permissions. This includes page navigation, hiding and disabling of UI elements, and generation of menus.

Applications often include requirements to customize their appearance and behavior based on the user’s role or permission. Users should only be presented with certain choices based on their role or a set of actions they have permission to perform. This is not a replacement for securing the data at the API level, but improves the usability on the client. This post provides sample code that you can use to implement this feature in your Angular app.

Create an authorization service

Centralize the checking of permissions into an Angular service.

authorization.service.ts

import { Injectable } from '@angular/core'; import { AuthGroup } from '../models/authorization.types'; import { AuthorizationDataService } from './authorization-data.service'; @Injectable() export class AuthorizationService { permissions: Array<string>; // Store the actions for which this user has permission constructor(private authorizationDataService: AuthorizationDataService) { } hasPermission(authGroup: AuthGroup) { if (this.permissions && this.permissions.find(permission => { return permission === authGroup; })) { return true; } return false; } // This method is called once and a list of permissions is stored in the permissions property initializePermissions() { return new Promise((resolve, reject) => { // Call API to retrieve the list of actions this user is permitted to perform. (Details not provided here.) // In this case, the method returns a Promise, but it could have been implemented as an Observable this.authorizationDataService.getPermissions() .then(permissions => { this.permissions = permissions; resolve(); }) .catch((e) => { reject(e); }); }); } }

authorization.types.ts

export type AuthGroup = 'VIEW_ONLY' | 'UPDATE_FULL' | 'CREATE';

Create attribute directives to hide and disable elements

To hide or disable an element based on permission, use the following code to create two directives. This will enable the Angular templates to use this syntax:

<div [myHideIfUnauthorized]="updatePermission"> <!-- a property set or passed into the component -->

disable-if-unauthorized.directive.ts

import { Directive, ElementRef, OnInit, Input } from '@angular/core'; import { AuthorizationService } from '../../services/authorization.service'; import { AuthGroup } from '../models/authorization.types'; @Directive({ selector: '[myDisableIfUnauthorized]' }) export class MyDisableIfUnauthorizedDirective implements OnInit { @Input('myDisableIfUnauthorized') permission: AuthGroup; // Required permission passed in constructor(private el: ElementRef, private authorizationService: AuthorizationService) { } ngOnInit() { if (!this.authorizationService.hasPermission(this.permission)) { this.el.nativeElement.disabled = true; } } }

hide-if-unauthorized.directive.ts

import { Directive, ElementRef, OnInit , Input } from '@angular/core'; import { AuthorizationService } from '../../services/authorization.service'; import { AuthGroup } from '../models/authorization.types'; @Directive({ selector: '[myHideIfUnauthorized]' }) export class MyHideIfUnauthorizedDirective implements OnInit { @Input('myHideIfUnauthorized') permission: AuthGroup; // Required permission passed in constructor(private el: ElementRef, private authorizationService: AuthorizationService) { } ngOnInit() { if (!this.authorizationService.hasPermission(this.permission)) { this.el.nativeElement.style.display = 'none'; } } }

Create a CanActivate guard to prevent unauthorized routing

Angular includes a feature to prevent navigation to a page by implementing a CanActivate guard and specifying it in the route configuration. Unfortunately, there is no option to pass a parameter into the guard service, but a work-around is to use the data property of the route. When using this CanActivate guard in the route table, the programmer must also provide the route data value. This example uses a value named auth.

auth-guard.service.ts

import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot } from '@angular/router'; import { AuthorizationService } from './authorization.service'; import { AuthGroup } from '../models/authorization.types'; @Injectable() export class AuthGuardService implements CanActivate { constructor(protected router: Router, protected authorizationService: AuthorizationService) { } canActivate(route: ActivatedRouteSnapshot): Promise<boolean> | boolean { return this.hasRequiredPermission(route.data['auth']); } protected hasRequiredPermission(authGroup: AuthGroup): Promise<boolean> | boolean { // If user’s permissions already retrieved from the API if (this.authorizationService.permissions) { if (authGroup) { return this.authorizationService.hasPermission(authGroup); } else { return this.authorizationService.hasPermission(null); } } else { // Otherwise, must request permissions from the API first const promise = new Promise<boolean>((resolve, reject) => { this.authorizationService.initializePermissions() .then(() => { if (authGroup) { resolve(this.authorizationService.hasPermission(authGroup)); } else { resolve(this.authorizationService.hasPermission(null)); } }).catch(() => { resolve(false); }); }); return promise; } } }

Include the canActivate property of the route definition together with the data property in order to pass in the required permission.

routing.module.ts

const routes: Routes = [ { path: 'feature', canActivate: [AuthGuardService], // Could nest parent auth requirements as well as child children: [ { path: '', children: [ { path: 'searchresults', component: SearchResultsComponent, canActivate: [AuthGuardService], data: { auth: 'VIEW_ONLY' }, resolve: { searchResults: SearchResultsResolver } },

Call the authorization service elsewhere in the app

In addition to the attribute directives and the CanActivate guard for routing, the authorization service can be called throughout the app. For instance, a menu service could use the permission checking method to hide menu items if the user does not have the required permission.

menu.service.ts

private showMenuItem(authGroup: AuthGroup) { return this.authorizationService.hasPermission(authGroup); }

Remember, this is all merely JavaScript and a determined and savvy user could still work around these safeguards, but the goal is to improve the experience for the user. It is still the job of the serverside code to secure the data behind the API.