Handling basic route authorization in AngularJS

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 !

____

You liked this article? You’d probably be a good match for our ever-growing tech team at Theodo. Take a look at our job offers on http://www.theodo.fr/en/joinus/


You liked this article? You'd probably be a good match for our ever-growing tech team at Theodo.

Join Us

  • I’m just learning Angular as part of the Ionic mobile framework but this seems to be the most idiomatic example of authorization and login functionality that I’ve seen so far.

    I’ll put this to good use, thanks!

  • Coding Otter

    In the .run on $statechange event the only way i got it working was:

    (…)

    if (_.has(toState.data, ‘authorization’) {

    }

    (…)

    insetad of:

    if (_.has(toState, ‘data.authorization’) {}

  • Woody Rousseau

    Hi there,

    Thanks for the comment, indeed, since the 4.x lodash update, `_.has` does not allow deep checking of properties. Your solution will work fine.

    Cheers

  • aakash

    Y can’t use $stateChangeStart instead of $stateChangeSuccess. In stateChangeSuccess, controller runs if its not authorized state.

  • Michele Imperiali

    I actually have the same problem aakash pointed out… if I use the “$stateChangeSuccess” it works but I see I get errors in the backend because the controller of the page I’m trying to reach (being unauthorized) fires. So I thought to change it in “$stateChangeStart” but it doesn’t work… not sure why. Any clue?

  • Michele Imperiali

    I also would like to point out that this code clears the user status on page refresh. Basically when the page is refreshed the Authorization.authorized variable is reset to false and the user must provide again user name and password. To solve this issue I’ve stored the authorized variable in the localstorage (not sure if this has drawbacks).

  • Gustavo Matias

    Great article! I was going with that route before finding out a good angular auth library. You guys should check out https://github.com/Narzerus/angular-permission and see which one works best :)

  • ungular

    [Ionic framework]
    I tried this:
    .run(function($ionicPlatform, _, $rootScope, $state, Authorization) {
    got this Error: [$injector:unpr] Unknown provider: _Provider <- _
    I have moved the _ to rootScope as:
    $rootScope.$on('$stateChangeSuccess', function(event, _, toState, toParams, fromState, fromParams){
    got Error:
    TypeError: Object doesn't support property or method 'has'

    any solution?