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.

  • Larry

    HI,

    Do you have a demo on github for this excellent tutorial? Please share it to me. I beg you.

  • https://www.ohhdennyservices.com K Denny

    Greetings! This solutions is EXACTLY what I have been looking for but having some small issues trying to get it to read my service. Do you have plunkr of this or demo for how all this works together by any chance?

    Peace & Blessings

  • Ahmet Serkan Güngören

    Hi ,
    can you share source ?

  • chandida

    how can you implement the same in angular 1.5 using components?

  • Aram Manukyan

    Greaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat!!!!!!!!!!!!!!!!!!!!!

  • viresh

    great bro…can you share the complete source code of this?.

  • Hayot Ismatov

    Goooooooooood JOB )))

  • Pingback: Developing a permission-based auth system in AngularJS - AngularJS News()

  • Pingback: Developing a permission-based auth system in AngularJS | Webammer()

  • ananth raghav

    All hail the author! Very well written. I have implemented the same and it damn works pretty smooth.

  • Christian Böhm

    Excellent blog! Really usefull in order to start with. Let me add one extra idea about this… instead of hardcoding the allowed permissions in the html, it would be better to get them from the controller, so that if is not so easy for the user to see which is the expected permission key and change it.

  • apmeena

    Can I use the same thing with $stateProvider? Thanks.

  • Michael Bennett

    Should line 22 of the AuthServices be auth.currentUser()? I was getting an error on page refresh on like 22 until I made it auth.currentUser(). Thanks for the code.

    • http://www.stefanoscerra.it/ Stefano Scerra

      Yes, you’re right, fixed it. Thank you

  • leeu1911

    As your Angular app expects this:

    {
    name: “John Doe”,
    permissions: ['list_orders', 'read_statistics']
    // …
    }

    Shouldn’t line 75 of the AuthServices be:
    if ($sessionStorage.user.permissions.indexOf(permission) >= 0){

    Thanks for the nice tutorial anw.