Introduction
AngularJS has come a long way since its introduction. It is a comprehensive JavaScript framework for Single Page Application (SPA) development. It has some awesome features like 2-way binding, directives, etc. This topic will focus on Angular security. This is client side security which you can implement with Angular. In addition to client side route security (which we would discuss in this tip), you would need to secure access at server side as well. Client side security helps in avoiding extra round trips to server after making user session object at client. However, if someone tricks the browser, then server side security should be able to reject unauthorized access. In this tip, I would restrict my discussion to client side security.
We would make one authorization service, which would fetch the User authorization/role specific data from server about a user role. And, later, this user role data would be reused for all routes visited by user, during his session. A very interesting aspect which you would notice about angular service is that, service data is persisted throughout the user session. In this example, I have considered only Windows Authentication, for sake of simplicity. To understand this post, you need basic knowledge of AngularJS, Angular Routing and promises. I will walk you through, step by step, on the details of the implementation. It is not something I have invented new in angularJS. However, I have tried to put together the things which are available in Angular and can be combined to make a re-usable security code. I am open to suggestions for this article write-up improvement. Please let me know if you see anything which is incorrect or missing. I will make the corrections as necessary. Hope you enjoy reading it!
Step 1: Define Global Variables in app-module
Define roles for the application:
These are the roles which you would like to keep for authorization in the application. It is necessary to keep them global as these would be used by both at App module scope and service scope.
var roles = {
superUser: 0,
admin: 1,
user: 2
};
Define route for unauthorized access for the application:
This is the route where you would want the user to go, if he is not authorized to see any page.
We would be using this variable later, in the App module routes.
var routeForUnauthorizedAccess = '/SomeAngularRouteForUnauthorizedAccess';
Step 2: Define the Service for Authorization
This is the heart of the whole authorization. Angular service (or factory) is singleton and persisted across different routes of application, for a given user session. We can leverage this feature of service to do authorization on different routes. In order for you to create the service, you would need a service (like Web API) at server side, which can give you a set of roles for a given user. Once roles are procured from server side, then Angular service can persist this knowledge to be re-used for all routes, during the session of user. I have tried to explain the service code below with inline comments.
appModule.factory('authorizationService', function ($resource, $q, $rootScope, $location) {
return {
permissionModel: {
permission: {},
isPermissionLoaded: false
},
permissionCheck: function (roleCollection) {
var deferred = $q.defer();
var parentPointer = this;
if (this.permissionModel.isPermissionLoaded) {
this.getPermission(this.permissionModel, roleCollection, deferred);
} else {
$resource('/api/permissionService').get().$promise.then(function (response) {
parentPointer.permissionModel.permission = response;
parentPointer.permissionModel.isPermissionLoaded = true;
parentPointer.getPermission(parentPointer.permissionModel, roleCollection, deferred);
});
}
return deferred.promise;
},
getPermission: function (permissionModel, roleCollection, deferred) {
var ifPermissionPassed = false;
angular.forEach(roleCollection, function (role) {
switch (role) {
case roles.superUser:
if (permissionModel.permission.isSuperUser) {
ifPermissionPassed = true;
}
break;
case roles.admin:
if (permissionModel.permission.isAdministrator) {
ifPermissionPassed = true;
}
break;
case roles.user:
if (permissionModel.permission.isUser) {
ifPermissionPassed = true;
}
break;
default:
ifPermissionPassed = false;
}
});
if (!ifPermissionPassed) {
$location.path(routeForUnauthorizedAccess);
$rootScope.$on('$locationChangeSuccess', function (next, current) {
deferred.resolve();
});
} else {
deferred.resolve();
}
}
};
});
Step 3: Use Security in Routing
Let's use all our hard work done so far to secure the routes. So far, we created a service which is capable of checking if a user falls under a particular role or not. This knowledge can be used in the angular routes now. In the angular module, where we define routes, we can put these checks to see if the user has access to any of the roles specified for a route. I have put combinations of roles in these routes, to ensure better understanding.
var appModule = angular.module("appModule", ['ngRoute', 'ngResource'])
.config(function($routeProvider, $locationProvider) {
$routeProvider
.when('/superUserSpecificRoute', {
templateUrl: '/templates/superUser.html',
caseInsensitiveMatch: true,
controller: 'superUserController',
resolve: {
permission: function(authorizationService, $route) {
return authorizationService.permissionCheck([roles.superUser]);
},
}
})
.when('/userSpecificRoute', {
templateUrl: '/templates/user.html',
caseInsensitiveMatch: true,
controller: 'userController',
resolve: {
permission: function(authorizationService, $route) {
return authorizationService.permissionCheck([roles.user]);
},
}
})
.when('/adminSpecificRoute', {
templateUrl: '/templates/admin.html',
caseInsensitiveMatch: true,
controller: 'adminController',
resolve: {
permission: function(authorizationService, $route) {
return authorizationService.permissionCheck([roles.admin]);
},
}
})
.when('/adminSuperUserSpecificRoute', {
templateUrl: '/templates/adminSuperUser.html',
caseInsensitiveMatch: true,
controller: 'adminSuperUserController',
resolve: {
permission: function(authorizationService, $route) {
return authorizationService.permissionCheck([roles.admin, roles.superUser]);
},
}
})
.when(routeForUnauthorizedAccess,
{
templateUrl: '/templates/UnauthorizedAccess.html',
caseInsensitiveMatch: true
})
});
Summary
We saw in this tip as to how we can define authorization on various routes of Angular JS based application. As an alternative to securing routes, you may choose to not show the link (Or Menu links) to user who does not have access to it, however, putting the access on route itself at a central place in application ensures that even if you miss hiding a link (which ultimately becomes a route) for unauthorized user, the route configuration will ensure that unauthorized user will not be taken to the route.
Lastly, but importantly, I would like to emphasize that, all of the above stands invalid if the user tricks the browser. Therefore, it is important to have server side check for any read/update of data.