Handling basic route authorization in AngularJS

August 16, 2015Woody Rousseau5 min read

angular-authorization

Supercharging AngularUI Router with basic route authorization features

AngularUI Router is undoubtedly the routing framework to use when working on any Angular application that requires the slightest routing features. It allows organizing your different (and possibly nested!) views into a state machine, with each state optionally attached to routes and custom behaviors.

However, when some routes require in your application for the user to be logged in or to possess any kind of authorization, you may find yourself having to reinvent the whole wheel to allow such restrictions.

In this blog post, I will introduce a very basic, yet functional, way to:

  • Limit access to states
  • Redirect users to another state when access was denied
  • Memorize the state the user was trying to reach, in order to allow redirection as soon as the user successfully logs in

Always keep in mind that client-side authentication, although improving the user experience, does not replace the more secured server-side authentication which should always be implemented first when security is a concern.

Reading this article requires a basic understanding of AngularJS, AngularUI Router, and of Lodash.

The sample app state machine

We introduce a minimum example, with an application with four routes, two of which being restricted and being given the authorization flag as well as a redirectTo option to specify where the user should be redirected if not authorized. An additional memory flag is given to the 'secret' state in order to specify that the fact that the user was trying to reach this state should be memorized.

.config(function ($stateProvider, $urlRouterProvider) {

  $urlRouterProvider.otherwise('/');

  $stateProvider
  .state('home', {
    url: '/',
    template: '<h1>Home</h1>'
  })
  .state("login", {
    url: "/login",
    template: '<h1>Log In</h1>'
  })
  .state('private', {
    url: '/private',
    template: '<h1>Private</h1>',
    data: {
      authorization: true,
      redirectTo: 'login'
    }
  })
  .state('secret', {
    url: '/secret',
    template: '<h1>Secret</h1>',
    data: {
      authorization: true,
      redirectTo: 'login',
      memory: true
    }
  });

});

The Authorization service

This service must include a boolean determining wether or not the user is currently authorized to access restricted routes, as well as which state the user was last trying to reach.

It also provides a function to clear both information, as well as a go method which is to be called when the user logs in with success. It authorizes the user, and also performs a $state.go, except that it tries to use the memorized state if available, relying on the given state fallback argument if not.

.service('Authorization', function($state) {

  this.authorized = false;
  this.memorizedState = null;

  var
  clear = function() {
    this.authorized = false;
    this.memorizedState = null;
  },

  go = function(fallback) {
    this.authorized = true;
    var targetState = this.memorizedState ? this.memorizedState : fallback;
    $state.go(targetState);
  };

  return {
    authorized: this.authorized,
    memorizedState: this.memorizedState,
    clear: clear,
    go: go
  };
});

Logging in can then easily be done by calling this method, which authorizes the user, and redirects him to the private state, or to any memorized state if it does have one.

Authorization.go('private');

Logging out is just as easy, and can be followed by a redirection to a non restricted state.

Authorization.clear();
$state.go('home');

In most cases, you will want to hold the authorization information in the local storage, so that the user stays logged in even after restarting the browser.

Restricting access

The first step is to restrict access to the states which were given the authorization flag. Let's work step by step in a angular run block:

.run(function(_, $rootScope, $state, Authorization) {

  $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
    if (!Authorization.authorized && _.has(toState, 'data.authorization') && _.has(toState, 'data.redirectTo')) {
      $state.go(toState.data.redirectTo);
    }
  });
});

We listen to the $stateChangeSuccess event, to allow a possible resolve block for the target state to be processed. We then redirect the user to the redirectTo state name.

Setting the memorized state

In order to use the Authorization.go function which tries to redirect the user to the memorized state, such a state needs to be set in the run block as well. Here is an updated version where such a feature is applied to each state given a truthy memory in the state configuration.

.run(function(_, $rootScope, $state, Authorization) {

  $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
    if (!Authorization.authorized && _.has(toState, 'data.authorization') && _.has(toState, 'data.redirectTo')) {
      if (_.has(toState, 'data.memory') && toState.data.memory) {
        Authorization.memorizedState = toState.name;
      }
      $state.go(toState.data.redirectTo);
    }
  });

});

Forgetting about the memorized state

With the simple implementation, some issues may arise when the user does not choose to immediately log in after being redirected, and moves instead to another non-restricted state. The proper behavior would then be to forget about the memorized state, so that when the user eventually logs in, the fallback state parameter given to the Authorization.go is used instead. Here is the final version of the run block.

.run(function(_, $rootScope, $state, Authorization) {

  $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
    if (!Authorization.authorized) {
      if (Authorization.memorizedState && (!_.has(fromState, 'data.redirectTo') || toState.name !== fromState.data.redirectTo)) {
        Authorization.clear();
      }
      if (_.has(toState, 'data.authorization') && _.has(toState, 'data.redirectTo')) {
        if (_.has(toState, 'data.memory') && toState.data.memory) {
          Authorization.memorizedState = toState.name;
        }
        $state.go(toState.data.redirectTo);
      }
    }

  });
});

The tricky part is that clearing the memorized state should only be done when the user moves away from the login page, and thus should not be cleared when toState.name !== fromState.data.redirectTo.

Demo / Library

A simple demo is given in this Codepen.

You can navigate between the four states, the two first of which not requiring being authentified. Trying to reach the 'Private Page' or the 'Secret Page' will redirect you to the 'Login' state.
By default, logging in will get you to the 'Private Page', but if you log after trying to reach the 'Secret Page', you will be redirected to it directly.

I've provided an implementation of this system in the angular-authorization repository on GitHub. It probably has issues, so any feedback, bug reports, feature requests, or pull requests are more than welcome!

Woody Rousseau

Woody Rousseau

Web Developer at Theodo