At some point an application needs authorization. This means different levels of access behave differently on a web site (or anything for that matter). It can be anything from seeing data to whole area’s that are not accessible by a group of users.
In non Single Page Applications (SPA), a claim or role is associated with data or an area of the application, either the user has this role or claim or he does not. In a SPA it’s the same, but with a huge disclaimer. A SPA is downloaded to the browser. At this point the browser has total control over the code. A nefarious person can change the code to do his bidding.
Because SPAs can’t be secured, authentication and authorization in a SPA is simply user experience. All meaningful security must be done on the web server. This article does not cover securing your API against attacks. I recommend watching a video from Pluralsight or reading a paper that addresses security for your server technology.
The intent of this article to show you how I added an authorization user experience to my Angular 1.x SPA.
Security Scopes
I have identified 3 areas of the UI that need authorization: Elements (HTML), Routes, and Data.
Just a reminder, securing a SPA is no substitute to securing the server. Permissions on the client is simply to keep the honest people honest and to provide the user with a good experience.
The 3 areas in detail:
Elements
You’ll need to hide specific HTML elements. It could be a label, a table with data, a button, or any element on the page.
Routes
You’ll want to hide entire routes. In certain cases you don’t want the user accessing a view. By securing the route a user can’t to navigate to the view. They instead will be shown a “You are not authorized to navigate to this view” message.
Data
Sometimes hiding the elements in the view is not enough. An astute user can simply view the source and see the hidden data in the HTML source or watch it stream to the browser. What we want is the data not to be retrieve in the first place..
Adding security is tricky. At first I tried constraining the access at the HTTP API (on the client). I quickly realized this wouldn’t work. A user might not have direct access to the data, but this doesn’t mean they don’t indirectly access to the data. At the HTTP API layer (usually one of the lowest in the application) we can’t tell the context of the call and therefore can’t apply security concerns to it.
Below I have provided coding samples:
Code
I created a service for the authorization checking code. This is the heart of the authorization. All authorization requests use this service to check if the user is authorized for the particular action.
angular.module('services')
.service('AuthorizationContext',function(_, Session){
this.authorizedExecution = function(key, action){
//Looking for the claim key that was passed in. If it exists in the claim set, then execute the action.
Session.claims(function(claims){
var claim = findKey(key, claims);
//If Claim was found then execute the call.
//If it was not found, do nothing
if(claim !== undefined){
action();
}
});
};
this.authorized = function(key, callback){
//Looking for the claim key that was passed in. If it exists in the claim set, then execute the action.
Session.claims(function(claims){
var claim = findKey(key, claims);
//If they don't have any security key, then move forward and authorization.
var valid = claim !== undefined;
callback(valid);
});
};
//this.agencyViewKey = '401D91E7-6EA0-46B4-9A10-530E3483CE15';
function findKey(key, claims){
var claim = _.find(claims, function(item){
return item.value === key;
});
return claim;
}
});
Authorize Directive
The authorize directive can be applied to any HTML element that you want to hide from users without a specific level of access. If the user has the access token as part of their claims they are allow to see the element. If they don’t it’s hidden from them.
angular.module(directives')
.directive('authorize', ['$compile', 'AuthorizationContext', function($compile, AuthorizationContext) {
return {
restrict: 'A',
replace: true,
//can't have isolated the scope in a shared directive
link:function ($scope, element, attributes) {
var securityKey = attributes.authorize;
AuthorizationContext.authorized(securityKey, function(authorized){
var el = angular.element(element);
el.attr('ng-show', authorized);
//remove the attribute, otherwise it creates an infinite loop.
el.removeAttr('authorize');
$compile(el)($scope);
});
}
};
}]);
Elements
I rely heavily on tabs in my application. I apply the authorize directive to the tab that I want to hide from users without the proper claims.
<tabset>
<tab ng-cloak heading="Users" authorize="{{allowUserManagement}}">
...html content
</tab>
</tabset>
Routes
I’m using the ui-router
. Unfortunately for those who are not, I don’t have code for the out of the box AngularJS router.
In the $stateChangeStart
I authenticate the route. This is the code in that event.
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams){
AuthenticationManager.authenticate(event, toState, toParams);
});
The function that authorizes the route. If it’s authorized, the route is allowed to continue. If it’s not authorized, a message is displayed to the user and they are directed to the home page.
function authorizedRoute(toState, location, toaster, breadCrumbs){
if(toState.authorization !== undefined){
AuthorizationContext.authorized(toState.authorization, function(authorized){
if(!authorized){
toaster.pop('error', 'Error', 'You are not authorized to view this page.');
location.path("/search");
} else {
breadCrumbs();
}
});
} else{
breadCrumbs();
}
}
In this router definition you’ll notice a property called ‘authorization’. If the user has this claim they are allowed to proceed.
angular.module('agency',
[
'ui.router',
'services'
])
.config(function config($stateProvider){
$stateProvider.state( 'agency', {
url: '/agency',
controller: 'agency.index',
templateUrl: 'agency/agency.tpl.html',
authenticate: true,
authorization:'401d91e7-6ea0-46b4-9a10-530e3483ce15',
data:{ pageTitle: 'Agency' }
});
});
Data
In some cases, you don’t want to make a request to the server for the data. If the user has the claim they’ll be allowed to make the request.
The above AuthorizationContext
at beginning of the article show the code for authoriedExecution
. Here you see it’s usage.
AuthorizationContext.authorizedExecution(Keys.authorization.allowUserManagement, function(){
//execute code, if the loggedin user has rights.
});
As I mentioned above, this is no substitute for securing the server. This code works for providing a wonder user experience.