In a previous article, we covered how to protect application routes with security claims which is the process that allows us to control a user access based on assigned permissions. During our implementation, we realize that there are some areas of improvement. The most clear one is that are we showing navigational links to the users when they may not have the necessary authorization to see those elements. To improve that, the obvious approach would be to just hide those links and avoid any unnecessary user clicks.
Route Specifications
A basic approach to protect any user interface on an app is to add a directive to the UI elements. But before we start adding a directive, we need to know what claim determines if a user has access to a specific element. For that, we need to understand our application route specifications. In our example below, we can see that the /claims route requires a security claim.
Route | Claim | Required |
/login | no | |
/claims | app.claims | yes |
/noaccess | no |
Now that we understand what claims are required for authorization, we can focus on how to build our menus based on the route specifications. If we take a look at our static inline implementation of the navbar, we can see that our HTML looks like this:
<div class="collapsed navbar-collapse" id="app-menu"> <ul class="nav navbar-nav"> <li clas='active'><a href="#/"><i class="fa fa-home"></i> Home</a></li> <li><a href="#/claims"><i class="fa fa-key"></i> Claims</a></li> </ul> </div> |
We could work with this HTML and add our directive there, but if our route configuration changes, we will need to modify the HTML to match that change. We could do much better and just use the route configuration to build our navbar dynamically. Let’s take a look at that.
Use Route Configuration to Build the Menus
In our static implementation, we define the menu links without mapping them to the actual routes. In order to leverage the route information for our menus, we need to build the menu links dynamically. We can do this by adding a controller that can manage the menu for us.
In some cases, we may not want to display all the routes on the menu. This is the case for the /noaccess route which is intended to be displayed just when there are errors, but not for the user to see it and click it.
We can control the visibility of menus by adding a show property on our routes configuration. This can be used to show/hide the element using the ngShow directive. Our route configuration should look as follows:
var appRoutes = [{ title: "Login", url: "/", templateUrl: "views/main.html", controller:null, controllerAs: null, requiredAuth: false, show: true, icon: 'fa-home' }, { title: "Claims", url: "/claims", templateUrl: "views/claims.html", controller: "app.ctrl.claims", controllerAs: "ctrl", redirectTo: '/noaccess', claims: "app.claims", requiredAuth: true, show: true, icon:'fa-key' }, { title: "No Access", url: "/noaccess", templateUrl: "views/noaccess.html", controller: null, controllerAs: null, requiredAuth: false }]; |
Only the Login and Claims routes are set to show:true. We can now continue to build our controller and build our navbar dynamically.
app.controller('app.ctrl.menu', ['$appRoutes', ctrlMenu]); function ctrlMenu($appRoutes, $svcAuth) { var ctrl = this; ctrl.menu = $appRoutes; } |
The menu controller is very simple. We only need to inject the route configuration. On the view, we declare our controller and use the ngRepeat directive to iterate the items in the array. Our new dynamic menu mark-up should look as follows:
<div class="collapse navbar-collapse" id="app-menu"> <ul class="nav navbar-nav" ng-controller="app.ctrl.menu as ctrl"> <li ng-repeat="item in ctrl.menu"> <a href="{{item.url}}" ng-show="item.show"> <i class="fa {{item.icon}}"></i> <span ng-bind="item.title"></span> </a> </li> </ul> </div> |
Authorize Directive
We are now ready to add the authorize directive to our menus. As we declare the directive on the markup, we pass the menu:claims property. This is what can enable us to control the visibility of the menus based on the user’s permissions. Our new change looks like this:
<li ng-repeat="item in ctrl.menu"> <a authorize="item.claim" href="{{item.url}}" ng-show="item.show"> <i class="fa {{item.icon}}"></i> <span ng-bind="item.title"></span> </a> </li> |
app.directive("authorize", ['app.svc.auth', dirAuthorize]); function dirAuthorize($svcAuth) { return { restrict: 'A', scope: { authorize: '=' }, link: function (scope, elem, attrs) { var watch = function () { //get the claim value var claims = scope.authorize; if (claims) { var result = $svcAuth.hasClaim(claims); if (!result) { elem.hide(); } } }; watch(); } }; }; |
The Authorize directive handles the authorization logic. It reads the claim value, and it uses the $svcAuth services to find out if there is a claim with the same value in the user context. If the claim is found, the element is displayed, and we should feel confident that the same validation should succeed during our route authorization. If the claim does not exist, the menu link should not be visible, and the user would not be able to attempt to navigate to that area of the application.
Note
We need to note that this is only removing un-authorized elements from the user interface. This is just to improve the user experience by preventing un-authorized clicks. We still need to make sure that the route that is executed on a click is also protected.
Does It Work
We can take a look at our example below. As we can see all the menus are visible for now because the corresponding claims exists. To test it, we can click on the claims menu item and delete the app.claims entry by clicking on the trash can icon. As the claim is removed, the menu entry should also be removed because we are essentially removing the claim from the user security context.
That was a fun series of blog entries on claims based authorization. We now are able to protect our routes, redirect unauthorized users and add authorization to user interface elements all using the same reusable components.