Overview
When we talk about routing with AngularJS, we make reference to a service that allows us to map URL routes to view templates (HTML) and controllers. This is what allows a single page application to navigate to different areas of the app.
In the context of authorization, we need to allow access to protected routes/views to only the users with the corresponding claims or access. This is even if the user attempts to force load the route by typing the URL or running a command on the browser console.
Route Specifications
In order to control the authorization of the routes, we must first understand the claims that are assigned to secure our routes. There may be some routes that have public access and required no claims. For the secured routes, we need to be able to know the claim value that should be used to verify the access. As an example, let’s take a look at our route specifications:
Route | Claim | Required |
/login | no | |
/claims | app.claims | yes |
The specifications indicate that the app has two 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.claims claim to be present in the user security context. Let's take a look at our JSON model which represents our route information with the corresponding claims:
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 ]; |
The claims and requiredAuth attributes are used to check if the route should be protected, and if so, what claim should be used to validate for the access. We look at this in more detail on our route implementation later on.
Configuring Routes
We start our implementation by defining our module and configuring our routes. The route configuration is usually done in the app.config function where we can inject the routeProvider (ng-route) or stateProviders (ui-router). In our case, we are using routeProvider, so our implementation looks as follows:
function appConfig($routeProvider, $appRoutes) { $appRoutes.forEach(function (route) { configRoute(route); function configRoute(route) { $routeProvider .when(route.url, { templateUrl: route.templateUrl, controller: route.controller, controllerAs: route.controllerAs, }); } }); $routeProvider.otherwise("/"); } |
We have defined the $appRoutes JSON which contains the application route definition with view, controller and claim association. This has the necessary information to wire our routes. For now, our current implementation just handles the routing to different areas of the app. There is no restriction on the access. We can implement the security/authorization next.
Authorize Routes- Resolve
For authorization, we rely on an authorization service which sets the user context with the corresponding claims. For now, we just simply set the claims with the service as a property. For a production app, there should be integration with an identity provider.
During the app configuration phase, we can inject the authorization service ($svcAuth) to the route configuration. We add the security validation to the routeProvider resolve function which waits for a promise to resolve successfully before the route changes. This allows us to verify the access before the route changes and presents the view to the user.
In cases when the required claim does not exist in the user context, we terminate the route change by throwing an error. Otherwise, we resolve the call to true which allows the route to change successfully.
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 can now take a look at our complete implementation with the authorization enhancements.
Authorize Service
For this implementation, the auth service is used by the configuration phase of the application. When configuring our routes, we use the auth service to validate the user claims during the route resolve call. The auth service can also be used with other components like directives and controllers.
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 get an error which can be seen on the browser console, and the app would no longer navigate to the claims view.
Areas of Improvements
We should notice that during the resolve route process, we raised an error to terminate the route. This is not really an elegant way to signal to the user that he/she has no access to that area of the application. We could do better by redirecting the user to an area of the application that can provide a detail message.
We should also notice that we are displaying the navigation menus without any security validation. Since we know the user context, we could secure our navigation menu and avoid unauthorized clicks on the app. This would be a much better user experience and additional layer of authorization.
Hope this help us in protecting our routes.