AngularJS: Authentication when using ngRoute
I have been on an AngularJS kick recently. I talk about what it is in the last post, so go read that one for details. Essentially, it's an MVC framework for JavaScript. A really cool plugin for Angular is ngRoute
,which gives it it's SPA behavior. Basically what it does is it let's you map different url's to an html file and a controller to display on the main page of the application. Setting it up is a whole process and w3schools has a great tutorial about how to set it up.
Like everything great in this world, there are going to be trade offs.One thing that is really easy to do in a traditional web set up, is checking if a user is logged in and is authorized before starting to load any given page. In PHP, the process is quite simple, just simply check the user's credentials before loading HTML content, and if the user is not authorized, redirect to the login page. Here's some pseudocode for that process.
<?php
session_start();
if(!isValidSession()) {
redirect_to_login_page()
}
?>
<!DOCTYPE html>
The logic for authentication and redirecting really depends on the server setup and on the developer.
This process is not quite as simple when it comes to AngularJS with ngRoute for obvious reasons. This authentication happens when the page is loading, and in an SPA, the browser just loads a master index file and the framework determines what to display within that file. This means that the authentication has to happen when the routing engine is handling the request. First, we need to build logic on the back-end and create a service that can communicate to that logic from the JavaScript. Obviously, how you set up your back-end is completely up to you, but for the sake of this article, just assume that the back-end is authenticating the session based on the given roles and returning a boolean
in json
format. I am going to assume that you know how to setup and AngularJS service, so I won't go into much detail on how to do it, but essentially, you need something along the lines of the code below.
angular.module('app')
.factory('AuthAPI', function($http) {
authorize_user: function(data) {
return $http({
method: "GET",
params: {roles: data.join('|')},
url: path_to_authentication
});
}
This creates a service with a function that makes the request to the authentication script. Basically the function takes an array of roles to authenticate and returns the promise of the request. That bit is very important. The promise is an asynchronous call to the back-end, so any code that calls this will not wait for the request to be complete before continuing. We will worry about this later, just keep all this in mind. In our route provider, we need to be able to choose which routes are authorized and which are not. ngRoute
allows for a resolve
object to be added to the options of a route, which calls a method before loading the template and the controller. For example, here are two routes, one that will be authenticated, and one that won't.
var app = angular.module('app', ['ngRoute']);
app.config(function($routeProvider) {
$routeProvider
.when('/login', {
templateUrl: 'login.html',
controller: 'LoginController'
})
.when('/profile', {
templateUrl: 'profile.html',
controller: 'ProfileController',
resolve: {
authorize: function(AuthAPI, $location) {
return AuthAPI.authorize_user(['user']).then(
function(success) {
if(!success.data.isAuthorized) {
$location.path("/login");
}
},
function(error) {
$location.path("/login");
}
);
}
}
});
});
So of course, we don't need to authorize the login screen, but we do need to authorize the profile screen. So the basic logic is if there is an error in the request, or if the user is not authorized, redirect the page to the login screen. Recall that the service returns a promise of the request. The .then()
method is what chains the promise. Basically it's another promise that is called once the first promise is called. In this case, we are passing two callback functions, one for when the request is successful, and the other for when there was any error. The success parameter holds the information of the request, including field data
that holds the response of the request. This will hold whether the session was authorized or not. Now, let's look at why we need to return
the chained promise instead of just calling the service's function. If we remove that return
, you'll notice that the profile page will load for a second and then if the user is not authorized, it will then redirect. This is undesired behavior, obviously, because we don't want to profile page to load at all if the session isn't authorized. Because of this, we need to field to return the chained promise, so the route's resolve
won't be complete until the promises are both resolved. Once the resolve
is complete, then the template and controller will load. If the session is unauthorized, the location is rerouted to somewhere else and the template and controllers wont load at all.
So this method will work, and there's no real problem using it. However, you're going to have to write this authentication logic over and over again and it might be a pain. To mitigate this pain, we are going to make this process a bit more clever. Instead of adding a resolve
to each individual route, we are going to inject it based on a condition before the route is fully processed. Basically what we are going to do, is instead of adding the resolve
to the route, we are going to add a field called authorize
that is an array of strings indicating what roles are authorized for the given route. So instead of:
.when('/profile', { templateUrl: 'profile.html', controller: 'ProfileController', resolve: { authorize: function(AuthAPI, $location) { return AuthAPI.authorize_user(['user']).then( function(success) { if(!success.data.isAuthorized) { $location.path("/login"); } }, function(error) { $location.path("/login"); } ); } } });
We are going to do
.when('/profile', { templateUrl: 'profile.html', controller: 'ProfileController', authorize: ['user'] });
This is so much easier to read, and is more reusable for any route. But now, the authorization doesn't happen. In the current state, all we did was add an arbitrary array to the route, so we need to put the resolve
back into the route before it loads. so after your config, we need to add the following code:
app.run(function($rootScope) {
$rootScope.$on('$routeChangeStart', function(event, to, from) {
if(to.authroize && to.authorize.length > 0) {
to.resolve = {
authorize: function(AuthAPI, $location) {
return AuthAPI.authorize_user(['user']).then( function(success) { if(!success.data.isAuthorized) { $location.path("/login"); } }, function(error) { $location.path("/login"); } );
}
}
}
});
});
The .run()
function is called when the framework is loaded, think of it as a "main" method for AngularJS. So basically what this does is create an event handler, which is the $rootScope.$on
bit for the event $routeChangeStart
. The three parameters are event
: the event itself, to
: the target route, from
: the base route. All we really need is the data in the target route. You can get fancy and use some data in the other parameters to do more complex things, but you don't have to. So first we need to check if the route requires authorization. We do this wit the expression: to.authroize && to.authorize.length > 0
which first checks if the authorize
property exists, and that it is not empty. Of course, you can have your own criteria, but for the purposes of this article, that is as far as we are going to go. Once we determined that we need to authorize the route, we inject the resolve
from before, only making it so that is uses the authorize
array in to
as the data to pass into the request. Now, this will happen for any route with that authorize
array, which makes this a much for flexible and readable solution. You can also easily check if the to
already has a resolve
, and simply append the authorize
function, but we're not getting that complex in this article.
So there you have it, the basics of authorization when using ngRoute
, I hope you found this helpful! If you have any questions or comments, leave them bellow.