6

I have an angular app that uses authorization logic and ui-router to bar unauthorized users for certain states/views. I follow the standard approach of listening for a stateChange event, which triggers my authorization logic. This all works well until the dreaded page re-load.

I store session data (including authorization status) in local storage so that on page reloads I can use a parent state in ui-router to first resolve/get the authorization status from local storage prior to attempting to change views. Here is the configuration of my app parent state object:

$stateProvider.
state('app', {
  url: '/app',
  abstract: true,
  controller: 'appCtrl',
  data: {
    authorizedRoles: [USER_ROLES.all]
  },
  templateUrl: 'partials/app.html',
  resolve: {

    //Try to restore from the previous session before loading any of the app child states
    RestoredSession: ['SessionService',
             function(SessionService){
                return SessionService.restoreSession();
              }]
  }
})

...various app. child states

And here is my onStateChange listener:

//listen for a ui.router $stateChangeStart event and test the new path to see if the currentUser
//is authorized to view that page

.run(         ['$rootScope', 'AUTH_EVENTS', 'SessionService', 
  function ($rootScope,   AUTH_EVENTS,   SessionService) {

  $rootScope.$on('$stateChangeStart', function (event, next) {
    var authorizedRoles = next.data.authorizedRoles;
    //If the requested page allows guest access, then continue to stateChange
    if (authorizedRoles.indexOf('guest') !== -1 || authorizedRoles.indexOf('*') !== -1) return;

    //If the requested page requires authorization, check login and auth privileges
    if (!SessionService.isAuthorized(authorizedRoles)) {

      event.preventDefault();

      if (SessionService.existingSession()) {

        // user is not allowed
        $rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
        console.log("User attempted to access page for which he is not authorized");

      } else {

        // user is not logged in
        $rootScope.$broadcast(AUTH_EVENTS.notLoggedIn);
        console.log("User attempted to access page when he is not logged in");

      }
    }
  });

}]);

My problem is that the stateChangeStart event is triggering prior to the app resolve such that the listener stops the state change (via the event.preventDefault), and then my resolve loads the stored session data, which often establishes that the user was authorized all along. If I could require execution of the resolve prior to the event triggering then I'd be golden.

Any ideas out there???

BTW, here is a similar SO question that went unanswered: Defer Angular UI Router $stateChangeStart until server authorization response receieved

Community
  • 1
  • 1
chuck w
  • 1,741
  • 2
  • 15
  • 34

4 Answers4

1

Turns out that all I needed to do was move the loading of config data to the .run() block instead of trying to do it in the parent app state's resolve.

//listen for a ui.router $stateChangeStart event and test the new path to see if the currentUser
//is authorized to view that page
.run(         ['$rootScope', 'AUTH_EVENTS','SessionService', 'localStorageService',
  function ($rootScope,   AUTH_EVENTS,  SessionService,   localStorageService) 
  {
  $rootScope.$on('$stateChangeStart', function (event, next) {

    //function to check to see if the currentUser has one of the required roles to authorize the next state.
    var checkAuthorization = function(authorizedRoles){

         //If the requested page allows guest access, then continue to stateChange
         if (authorizedRoles.indexOf('guest') !== -1 || authorizedRoles.indexOf('*') !== -1) return;
         //If the requested page requires authorization, check login and auth privileges
         if (!SessionService.isAuthorized(authorizedRoles)) {
           event.preventDefault();
           if (SessionService.existingSession()) {
             // user is not allowed
             $rootScope.$broadcast(AUTH_EVENTS.notAuthorized);
             console.log("User attempted to access page for which he is not authorized");
           } else {
             // user is not logged in
             $rootScope.$broadcast(AUTH_EVENTS.notLoggedIn);
             console.log("User attempted to access page when he is not logged in");
           }
         }
       };

    //Before calling checkAuthorization(), test to see if the state change was triggered by a reload
    //If so, load config data before triggering the `checkAuthorization()` function.
    if (SessionService.freshLoad === true  || typeof SessionService.freshLoad === 'undefined'){
      SessionService.freshLoad = false;
      var storedUser = localStorageService.get('currentUser');

      //If we have a stored user but no existing session, then we know that we have stored
      //user data to reload before the checkAuthorization() function.
      if (typeof storedUser !== "undefined" && storedUser !== null && !SessionService.existingSession()) {
        SessionService.restoreSession();
      }
    }

  checkAuthorization(next.data.authorizedRoles);

  });

}]);
chuck w
  • 1,741
  • 2
  • 15
  • 34
0

I have found a good way of resolving a data asynchronously during $stateChangeStart in another answer here. Here is the code:

rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState) {

    if (dataService.isInitialized()) {
        proceedAsUsual();
    } 
    else {

        event.preventDefault();

        dataService.intialize().success(function () {
                $state.go(toState, toParams);
        });
    }
});

Then you can just remember that your data is already initialized in the service the way you like, e.g.:

function dataService() {

    var initialized = false;

    return {
        initialize: initialize,
        isInitialized: isInitialized
    }

    function intialize() {

        return $http.get(...)
                    .success(function(response) {
                            initialized=true;
                    });

    }

    function isInitialized() {
        return initialized;
    }
};
Community
  • 1
  • 1
Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207
0

Coming in a little late here, but I think this will help.

The $on method returns a deregistration function for the listener. This allows cancellation of the event prior to custom handling in the listener.

var setInterceptedListener = function($scope) {
    var removeListener = $rootScope.$on('$stateChangeStart',
        function (event, toState, toParams, fromState, fromParams) {   
            // cancel state change
            event.preventDefault();

            // mock prompt for user input
            Prompt.continue('Continue?').then(function(result) {
                // if yes then deregister the listener in order to proceed.
                if (result == 'yes') {
                    removeListener();
                    $state.go(toState, toParams);
                }
            });
        });

        // deregister on scope teardown
        $scope.$on("$destroy", removeListener);
    };

To use this, simple add this method to a service and call setInterceptedListener($scope).

mmamane
  • 353
  • 4
  • 11
-1

This is client side security which you can implement in regular Angular versions. I have tried and tested this. (Please find my article here:- http://www.codeproject.com/Tips/811782/AngularJS-Routing-Security ). In addition to client side route security, you need to secure access at server side also. Client side security helps in avoiding extra round trip to server. However, if someone tricks the browser , then server server side security should be able to reject unauthorized access.

Hope this helps!

Step 1: Define Global variables in app-module

-define roles for the application

  var roles = {
        superUser: 0,
        admin: 1,
        user: 2
    };

-Define route For Unauthorized Access for the application

 var routeForUnauthorizedAccess = '/SomeAngularRouteForUnauthorizedAccess';

Step 2: Define the service for authorization

appModule.factory('authorizationService', function ($resource, $q, $rootScope, $location) {
    return {
    // We would cache the permission for the session, to avoid roundtrip to server for subsequent requests
    permissionModel: { permission: {}, isPermissionLoaded: false  },

    permissionCheck: function (roleCollection) {
    // we will return a promise .
            var deferred = $q.defer();

    //this is just to keep a pointer to parent scope from within promise scope.
            var parentPointer = this;

    //Checking if permisison object(list of roles for logged in user) is already filled from service
            if (this.permissionModel.isPermissionLoaded) {

    //Check if the current user has required role to access the route
                    this.getPermission(this.permissionModel, roleCollection, deferred);
} else {
    //if permission is not obtained yet, we will get it from  server.
    // 'api/permissionService' is the path of server web service , used for this example.

                    $resource('/api/permissionService').get().$promise.then(function (response) {
    //when server service responds then we will fill the permission object
                    parentPointer.permissionModel.permission = response;

    //Indicator is set to true that permission object is filled and can be re-used for subsequent route request for the session of the user
                    parentPointer.permissionModel.isPermissionLoaded = true;

    //Check if the current user has required role to access the route
                    parentPointer.getPermission(parentPointer.permissionModel, roleCollection, deferred);
}
                );
}
            return deferred.promise;
},

        //Method to check if the current user has required role to access the route
        //'permissionModel' has permission information obtained from server for current user
        //'roleCollection' is the list of roles which are authorized to access route
        //'deferred' is the object through which we shall resolve promise
    getPermission: function (permissionModel, roleCollection, deferred) {
        var ifPermissionPassed = false;

        angular.forEach(roleCollection, function (role) {
            switch (role) {
                case roles.superUser:
                    if (permissionModel.permission.isSuperUser) {
                        ifPermissionPassed = true;
                    }
                    break;
                case roles.admin:
                    if (permissionModel.permission.isAdministrator) {
                        ifPermissionPassed = true;
                    }
                    break;
                case roles.user:
                    if (permissionModel.permission.isUser) {
                        ifPermissionPassed = true;
                    }
                    break;
                default:
                    ifPermissionPassed = false;
            }
        });
        if (!ifPermissionPassed) {
            //If user does not have required access, we will route the user to unauthorized access page
            $location.path(routeForUnauthorizedAccess);
            //As there could be some delay when location change event happens, we will keep a watch on $locationChangeSuccess event
            // and would resolve promise when this event occurs.
            $rootScope.$on('$locationChangeSuccess', function (next, current) {
                deferred.resolve();
            });
        } else {
            deferred.resolve();
        }
    }

};
});

Step 3: Use security in routing: Lets use use all our hardword done so far, to secure the routes

var appModule = angular.module("appModule", ['ngRoute', 'ngResource'])
    .config(function ($routeProvider, $locationProvider) {
        $routeProvider
            .when('/superUserSpecificRoute', {
                templateUrl: '/templates/superUser.html',//path of the view/template of route
                caseInsensitiveMatch: true,
                controller: 'superUserController',//angular controller which would be used for the route
                resolve: {//Here we would use all the hardwork we have done above and make call to the authorization Service 
                    //resolve is a great feature in angular, which ensures that a route controller(in this case superUserController ) is invoked for a route only after the promises mentioned under it are resolved.
                    permission: function(authorizationService, $route) {
                        return authorizationService.permissionCheck([roles.superUser]);
                    },
                }
            })
        .when('/userSpecificRoute', {
            templateUrl: '/templates/user.html',
            caseInsensitiveMatch: true,
            controller: 'userController',
            resolve: {
                permission: function (authorizationService, $route) {
                    return authorizationService.permissionCheck([roles.user]);
                },
            }
           })
             .when('/adminSpecificRoute', {
                 templateUrl: '/templates/admin.html',
                 caseInsensitiveMatch: true,
                 controller: 'adminController',
                 resolve: {
                     permission: function(authorizationService, $route) {
                         return authorizationService.permissionCheck([roles.admin]);
                     },
                 }
             })
             .when('/adminSuperUserSpecificRoute', {
                 templateUrl: '/templates/adminSuperUser.html',
                 caseInsensitiveMatch: true,
                 controller: 'adminSuperUserController',
                 resolve: {
                     permission: function(authorizationService, $route) {
                         return authorizationService.permissionCheck([roles.admin,roles.superUser]);
                     },
                 }
             })
    });
Pramod Sharma
  • 376
  • 5
  • 14
  • 1
    Hmm, the original question asked for a solution for `ui-router` and you are using `ngRoute` in your answer. I don't think this is what the author was seeking. – treejanitor Jan 11 '15 at 10:24