Angular How-to: Implement Role-based security


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 it 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.
// 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 –> 

<div [myDisableIfUnauthorized]="updatePermission">

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: '',
data: { auth: 'VIEW_ONLY' },

                children: [
                    {
                        path: 'searchresults',
                        component: SearchResultsComponent,
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 server-side code to secure the data behind the API

Comments (3)

  1. Very nice implementation. Thanks for sharing.

  2. Mar says:

    Thank you Oscar for this article but I have a few questions:
    1. Where should initializePermissions() first execute? Ideally that is?
    2. When calling the service directly, can you describe what authGroup actually entails, as in this.authorizationService.hasPermission(authGroup)? Does authGroup in this example, equal ‘VIEW_ONLY’?

    1. Laurie Atkinson says:

      1) initializePermissions() is called inside hasRequiredPermission() in the AuthGuardService. The first time the app calls this method this.authorizationService.permissions is null and it will go into the else statement. If you are not using this service, you would write something similar in your class.

      2) Yes. It is just a string, but using TypeScript union type to limit the valid values. In the example there are only 3 valid values for authGroup.

      export type AuthGroup = ‘VIEW_ONLY’ | ‘UPDATE_FULL’ | ‘CREATE’;

Skip to main content