Search code examples
mysqlangularjsauthorizationrole-basedrole-based-access-control

How to handle role based authorization in AngularJS?


I am creating a web app which will satisfy two requirements for the users. Note: I am new to AngularJS as a web development platform.

Front-end - 1: Its a search functionality where users can search for specific documents and studies based on keyword search and filters. This has been implemented using MySQL for fetching data and display using AngularJS.

Front-end - 2: Users will have the option to create a account on the web app. Purpose of the account is:

  1. Save their search queries.
  2. If the administrator associates each user with a specific role, then those users will get access to extra options such as modifying the documents present in the database as well as uploading new documents and host of other pages.

My question:

How to handle role based authorization in AngularJS? I am not able to figure out how to create a framework which involves following functionalities: - Users get a role associated to them - Prevent users from accessing pages or functionalities which are not associated with those roles

I have read up on few SO articles as well as tutorials but every tutorial ends with author saying that role based authorization should be handled on the server side and I understand why this is true.

It would be great if anyone can point me to tutorials or write-ups which has role-based authorization implemented on server side for AngularJS.

Thanks!


Solution

  • I use role based authorization on backend as well as on frontend. Since I am using UI-Router for routing, the best resource I found (and improved to my needs) is this article:

    link expired

    If you use UI Router, definitely check it out. Basically you need to set up your routes security and intercept all route changes. The article also includes a directive for hiding user interface elements, if the user doesn't have permission to access the content behind it.


    Edit: Adding some code.

    First, you need to have user's permissions stored somewhere, e.g. on user object serialized in localStorage:

    {"id":1,"name":"user","created_at":"2016-04-17 18:58:19","gender":"m","roles":["admin"]}
    

    Then, you have two important parts:

    • directive - to determine if element should be visible or not based on assigned permission
    • service - to handle authorization checking

    Directive:

    (function() {
      'use strict';
    
      angular
        .module('app')
        .directive('access', access);
    
      /** @ngInject */
      function access(authorization) {
        var directive = {
          restrict: 'A',
          link: linkFunc,
        };
    
        return directive;
    
        /** @ngInject */
        function linkFunc($scope, $element, $attrs) {
          var makeVisible = function () {
            $element.removeClass('hidden');
          };
    
          var makeHidden = function () {
            $element.addClass('hidden');
          };
    
          var determineVisibility = function (resetFirst) {
            var result;
    
            if (resetFirst) {
              makeVisible();
            }
    
            result = authorization.authorize(true, roles, $attrs.accessPermissionType);
    
            if (result === authorization.constants.authorised) {
              makeVisible();
            } else {
              makeHidden();
            }
          };
    
          var roles = $attrs.access.split(',');
    
          if (roles.length > 0) {
              determineVisibility(true);
          }
        }
      }
    
    })();
    

    You need to set your CSS so that elements with class hidden are not visible.

    Service:

    (function() {
      'use strict';
    
      angular
        .module('app')
        .factory('authorization', authorization);
    
      /** @ngInject */
      function authorization($rootScope) {
        var service = {
          authorize: authorize,
          constants: {
            authorised: 0,
            loginRequired: 1,
            notAuthorised: 2
          }
        };
    
        return service;
    
        function authorize(loginRequired, requiredPermissions, permissionCheckType) {
          var result = service.constants.authorised,
              user = $rootScope.currentUser,
              loweredPermissions = [],
              hasPermission = true,
              permission;
    
          permissionCheckType = permissionCheckType || 'atLeastOne';
    
          if (loginRequired === true && user === undefined) {
              result = service.constants.loginRequired;
    
          } else if ((loginRequired === true && user !== undefined) &&
                      (requiredPermissions === undefined || requiredPermissions.length === 0)) {
              result = service.constants.authorised;
    
          } else if (requiredPermissions) {
    
              loweredPermissions = [];
    
              angular.forEach(user.roles, function (permission) {
                  loweredPermissions.push(permission.toLowerCase());
              });
    
              for (var i = 0; i < requiredPermissions.length; i += 1) {
                  permission = requiredPermissions[i].toLowerCase();
    
                  if (permissionCheckType === 'combinationRequired') {
                      hasPermission = hasPermission && loweredPermissions.indexOf(permission) > -1;
                      // if all the permissions are required and hasPermission is false there is no point carrying on
                      if (hasPermission === false) {
                          break;
                      }
                  } else if (permissionCheckType === 'atLeastOne') {
                      hasPermission = loweredPermissions.indexOf(permission) > -1;
                      // if we only need one of the permissions and we have it there is no point carrying on
                      if (hasPermission) {
                          break;
                      }
                  }
              }
    
              result = hasPermission ?
                       service.constants.authorised :
                       service.constants.notAuthorised;
          }
    
          return result;
        }
      }
    })();
    

    Now, you can use the directive to show/hide element:

    <a ui-sref="app.administration" class="btn btn-primary pull-right" access="admin">Administration</a>
    

    Of course this will only hide the element in DOM, so you must do the authorization check on the server too.

    This first part solved to showing/hiding elements in user interface but you can also protect app routes.

    Route definition:

    (function() {
      'use strict';
    
      angular
        .module('app')
        .config(routeConfig);
    
      /** @ngInject */
      function routeConfig($stateProvider) {
        $stateProvider
          .state('app.dashboard', {
            url: '/dashboard',
            data: {
              access: {
                loginRequired: true
              }
            },
            templateUrl: 'template_path',
            controller: 'DashboardController as vm'
          }
      }
    })();
    

    and now just check for permission in $stateChangeStart event

    (function() {
      'use strict';
    
      angular
        .module('app')
        .run(runBlock);
    
      /** @ngInject */
      function runBlock($rootScope, $state, authorization) {
        $rootScope.$on('$stateChangeStart', function(event, toState) {
          // route authorization check
          if (toState.data !== undefined && toState.data.access !== undefined) {
            authorised = authorization.authorize(toState.data.access.loginRequired,
                                                 toState.data.access.requiredPermissions,
                                                 toState.data.access.permissionCheckType);
    
            if (authorised === authorization.constants.loginRequired) {
              event.preventDefault();
              $state.go('app.login');
            } else if (authorised === authorization.constants.notAuthorised) {
              event.preventDefault();
              $state.go('app.dashboard');
            }
          }
        });
      }
    
    })();