Monthly Archives: January 2016

Developing a permission-based authorization system in a AngularJS app

In this post I’m going to show an implementation of a simple authentication and authorization system in a AngularJS web application. More in detail, the solution provides a declarative way to restrict access to both views and page content. Accessing user information and permissions from controllers and templates is also made really simple. This solution uses a service, a directive and session storage to implement a permission-based access control system.

The solution expects a back-end providing user profile data and permissions in this simple format:

{
   name: "John Doe",
   permissions: ['list_orders', 'read_statistics']
 // ...
}

Restricting access to views

Access control on views is set up on the $routeProvider configuration, so that you can annotate each route with two attributes: requiresAuthentication and permissions. The first is a boolean indicating if the user has to be logged in to access the view. The permissions element is a string array containing the requested permissions in a disjunctive (OR) fashion, so that the user is authorized to access the route if he owns at least one of the permissions.

angular
  .module('app', [
    'ngResource',
    'ngRoute',
    'AuthServices'
  ])
  .config(function ($routeProvider) {
    $routeProvider
      .when('/login', {
        templateUrl: 'views/login.html',
        controller: 'LoginCtrl'
      })
      .when('/home', {
        templateUrl: 'views/home.html',
        controller: 'HomeCtrl',
        requiresAuthentication: true
      })
      .when('/sales/orders/new', {
        templateUrl: 'views/sales/new_order.html',
        controller: 'OrderDetailCtrl',
        requiresAuthentication: true,
        permissions: ["administration"]
      })
      .when('/sales/orders', {
        templateUrl: 'views/sales/orders.html',
        controller: 'OrdersCtrl',
        requiresAuthentication: true,
        permissions: ["administration", "list_orders"]
      })
 });

Here we have a login view with public access, a home view accessible by all authenticated users, a new order view accessible only by users having the “administration” permission, and a order list view accessible by all users having an “administration” or “list_orders” permission.

Restricting access to page content

Another use case of the system is the access control on the page content. To show an element only to users having a particular permission, just use the permission directive on the DOM element. In this example, we have a navigation bar where we want to show only links to sections the user is authorized to access. As in the routes case, the permission attribute value is an array of permissions, so that the binded DOM element will be displayed only if the active user has at least one of them.

<div class="sidebar" ng-show="user" ng-cloak>
   <ul class="navigation">

      <li permission="['administration']">
         <span>Administration area</span>
         <ul>
           <li><a href="#/users">Users</a></li>
           <li><a href="#/settings">Settings</a></li>
         </ul>
      </li>

      <li permission="['administration', 'list_orders']">
         <span>Sales</span>
         <ul>
           <li permission="['administration']">
             <a href="#/sales/orders/new">New order</a>
           </li>
           <li permission="['administration', 'list_orders']">
              <a href="#/sales/orders">Orders list</a>
           </li>
         </ul>
      </li>

   </ul>

The Auth service and the Permission directive

The Auth service is the main component of the system, implementing the various functionalities. The user profile is saved in the session storage (wrapped by the ngStorage module) after login. A reference to the current user is also added to the root scope of the app automatically, to make it easy referencing it from the templates.

angular.module('AuthServices', ['ngResource', 'ngStorage'])
.factory('Auth', function($resource, $rootScope, $sessionStorage, $q){
    
    /**
     *  User profile resource
     */
    var Profile = $resource('/api/profile', {}, {
        login: {
            method: "POST",
            isArray : false
        }
    });
    
    var auth = {};
    
    /**
     *  Saves the current user in the root scope
     *  Call this in the app run() method
     */
    auth.init = function(){
        if (auth.isLoggedIn()){
            $rootScope.user = auth.currentUser();
        }
    };
        
    auth.login = function(username, password){
        return $q(function(resolve, reject){
            Profile.login({username:username, password:password}).$promise
            .then(function(data) {                        
                $sessionStorage.user = data;    
                $rootScope.user = $sessionStorage.user;
                resolve();
            }, function() {
                reject();
            });
        });
    };
    

    auth.logout = function() {
        delete $sessionStorage.user;
        delete $rootScope.user;
    };
    
    
    auth.checkPermissionForView = function(view) {
        if (!view.requiresAuthentication) {
            return true;
        }
        
        return userHasPermissionForView(view);
    };
    
    
    var userHasPermissionForView = function(view){
        if(!auth.isLoggedIn()){
            return false;
        }
        
        if(!view.permissions || !view.permissions.length){
            return true;
        }
        
        return auth.userHasPermission(view.permissions);
    };
    
    
    auth.userHasPermission = function(permissions){
        if(!auth.isLoggedIn()){
            return false;
        }
        
        var found = false;
        angular.forEach(permissions, function(permission, index){
            if ($sessionStorage.user.user_permissions.indexOf(permission) >= 0){
                found = true;
                return;
            }                        
        });
        
        return found;
    };
    
    
    auth.currentUser = function(){
        return $sessionStorage.user;
    };
    
    
    auth.isLoggedIn = function(){
        return $sessionStorage.user != null;
    };
    

    return auth;
});

The permission directive can be bound to DOM elements and directly references the Auth service:

angular.module('app')   
.directive('permission', ['Auth', function(Auth) {
   return {
       restrict: 'A',
       scope: {
          permission: '='
       },

       link: function (scope, elem, attrs) {
            scope.$watch(Auth.isLoggedIn, function() {
                if (Auth.userHasPermission(scope.permission)) {
                    elem.show();
                } else {
                    elem.hide();
                }
            });                
       }
   }
}]);

 

Accessing current user data from controllers and templates

Accessing the user data and permissions is quite simple, like in this controller:

angular.module('app')
  .controller('OrdersCtrl', function ($scope, Auth, Sales) {

    if (Auth.userHasPermission(["administration"])){
        // some evil logic here
        var userName = Auth.currentUser().name;
        // ...
    }

});

All you have to do is to add a dependency on the Auth service. To access the user data in a template, simply reference user in root scope:

<div ng-show="user" ng-cloak>
   <span>{{user.full_name}}</span> <a ng-click="logout()">Logout</a>
</div>

 

Putting things together

Access control on views is implemented by listening on route change events. Another thing to recall is that the root scope of the application is reset when the page gets refreshed (e.g., after hitting F5). We handle both this issues in the app run method.

angular.module('app', [
    'ngResource',
    'ngRoute',
    'AuthServices'
])
.run(['$rootScope', '$location', 'Auth', function ($rootScope, $location, Auth) {
    Auth.init();
    
    $rootScope.$on('$routeChangeStart', function (event, next) {
        if (!Auth.checkPermissionForView(next)){
            event.preventDefault();
            $location.path("/login");
        }
    });
  }]);

By calling Auth.init here we create again a reference to the user data in the root scope.

Logging in and out

A login controller using the Auth service:

angular.module('app').controller('LoginCtrl', function($scope, $location, Auth) {

    $scope.email = "";
    $scope.password = "";
    $scope.failed = false;

    $scope.login = function() {
        Auth.login($scope.email, $scope.password)
          .then(function() {
              $location.path("/home");
          }, function() {
              $scope.failed = true;
          });
    };

});

Handling logout in the main application controller:

angular.module('app')
  .controller('MainCtrl', function ($scope, $rootScope, $location, Auth) {
      
      $rootScope.logout = function(){
        Auth.logout();
        $location.path("/login");
      };
      
  });

Conclusions

The proposed system is a prototype solution for handling authentication and authorization in a AngularJS app with a concept of user permissions. To implement this in your application, however, there are some back-end-specific details regarding the authentication that might have to be tuned to fit your own architecture.