On a previous article, we discussed how to add authorization to the routes for an AngularJS SPA app. Our previous solution just stops the route change cycle by raising an error which is visible on the browser console. Since this is really not user friendly and provides no feedback to the user, we want to be able to instead redirect the user and display a better message.
Route Specifications
In order to improve the user experience, we need to add another route on our application which can be used to show a view with a better description of the not authorized error. If we look at our previous specifications, we notice that there are only two specified routes. We can now add a third one which we can use to redirect the user.
Route | Claim | Required |
/login | no | |
/claims | app.claims | yes |
/noaccess | no |
The specification now shows three routes. The /login route has public access, as it is needed to allow the users to login to the app. The /claims route requires the app.about claim to be present in the user security context. This is route that we validate against our claims. We also have the /noaccess route which is used to redirect users when the authorization validation fails.
This is how our JSON route configuration looks now:
var appRoutes = [{ title: "Login", url: "/", templateUrl: "views/main.html", controller:null, controllerAs: null, requiredAuth: false }, { title: "Claims", //todo-auth add claims module url: "/claims", templateUrl: "views/claims.html", controller: "app.ctrl.claims", controllerAs: "ctrl", redirectTo: '/noaccess', claims: "app.claims", requiredAuth: true }, { title: "No Access", url: "/noaccess", templateUrl: "views/noaccess.html", controller: null, controllerAs: null, requiredAuth: false }]; |
We should notice the redirectTo attribute on the claims route. This is used to indicate which route to use in case we need to redirect the user. Since we now have the /noaccess route, we also define the view information that our app needs to load and the corresponding HTML for that view.
<script type="text/ng-template" id="views/noaccess.html"> <div class="row"> <div class="col-lg-12"> <div class="panel panel-default"> <div class="panel-heading">Access Denied</div> <!-- /.panel-heading --> <div class="panel-body text-center"> <i class="fa fa-ban fa-5x text-danger" /> <h3>This area has not been shared with you.</h3> </div> <!-- /.panel-body --> <div class="panel-footer"> </div> </div> <!-- /.panel --> </div> </div> </script> |
Resolve and Updating the Route
On the route resolve process, before we raise the error, we need to set the current route information with an additional property (noAccess). We read the redirectTo value from the route configuration and set the noAccess property to that value. This is to indicate where the app should navigate since the intended route was denied. Let’s take a look at the snippet the handle the route resolve logic (appConfig function)
resolve: { "hasClaim": ["app.svc.auth", "$route", function ($svcAuth, $route) { var result = false; if (route.requiredAuth) { var result = $svcAuth.hasClaim(route.claims); if (!result) { $route.current.noAccess = route.redirectTo; throw new Error('No access to route: ' + route.url); } else { $route.current.noAccess = null; } } return result; }] } |
We continue to raise the error to terminate the route change thus preventing the user from seeing the unauthorized view. Since this error is raised on the route resolve event, we could now catch it on our app.run function.
Handling the Error and Redirect
When a route changes successfully or fails, the route provider raises events that can be handled during the app.run cycle. In our example, we are interested in handling the $routeChangeError as this event is raised when we terminate the route.
Our app can listen to that event by implementing a new function and associating to the app.run block in which we can inject the $rootScope and $location service. The $rootScope listens to the events, and $location is used to navigate to a different area of the app.
app.run(['$rootScope', '$location', runApp]); function runApp($rootScope, $location) { $rootScope.$on('$routeChangeError', function (evt, current, previous, reject) { $location.path(current.noAccess); console.log(reject.message); }); } |
Now that we have our new implementation, we can see it in action below.
Does it Work?
To test if this is working, we can delete the app.claims claim from the /claims route by visiting the claims view and clicking on the trash can icon. We can then navigate back to home (menu) and try to click on the claims menu link again. Since the claim is removed, we should now be redirected to the noaccess view which displays a better message.
I hope this is able to provide you with additional ideas on how to implement authorization on your apps. Continue to follow for other authorization entries.