6

Angular doesn't provide any authorization/access permission on routing (I'm talking default Angular route 1.x and not beta 2.0 or UI route). But I do have to implement it.

The problem I'm having is that I have a service that calls server to provide this info and returns a promise. This data however is only obtained once and then cached on the client, but it still needs to be obtained once.

I would now like to handle $routeChangeStart event that checks whether next route defines a particular property authorize: someRole. This handler should then get that data using my previously mentioned service and act accordingly to returned data.

Any ideas beside adding resolves to all my routes? Can I do this centrally somehow? Once for all routes that apply?

Final solution

With the help of accepted answer I was able to implement a rather simple and centralized solution that does async authorization. Click here to see it in action or check its inner working code.

0

4 Answers 4

5

The most simple way is to deal with current route's resolve dependencies, and $routeChangeStart is a good place to manage this. Here's an example.

app.run(function ($rootScope, $location) {
  var unrestricted = ['', '/login'];

  $rootScope.$on('$routeChangeStart', function (e, to) {
    if (unrestricted.indexOf(to.originalPath) >= 0)
      return;

    to.resolve = to.resolve || {};
    // can be overridden by route definition
    to.resolve.auth = to.resolve.auth || 'authService';
  });

  $rootScope.$on('$routeChangeError', function (e, to, from, reason) {
    if (reason.noAuth) {
      // 'to' path and params should be passed to login as well
      $location.path('/login');
    }
  });
});

Another option would be adding default method to $routeProvider and patching $routeProvider.when to extend route definition from default object.

Sign up to request clarification or add additional context in comments.

11 Comments

Sometimes I do patching but I wouldn't do it in this case as I'm updating this lib every time it increments. This may invite more problems than other techniques. But your solution may be in the right direction. So you've added a resolve to restricted routes. Great. One thing left is to also handle $routeChangeSuccess where you'd check authorization resolved promise and use $location.path() to redirect to login if authorization fails. I'm not sure if one can change location at this point of routing but it's worth a try, right? Can you provide a working example of this?
The things are not really that complicated at this point. You have to check for resolver rejection reason in $routeChangeError. I've added a plunker.
Patching existing objects to improve their functionality is honourable and established practice, many 3 and 4-star ng extensions do that. It comes with responsibility, specs to test the patches are essential, because patches may be broken. Considering the patch I've mentioned, $routeProvider.when patch causes zero problems when done right. It just catches the object that original 'when' returns and merges 'default' object with it. Just always stick to '.apply(this, arguments)'.
Basically I find your idea a much better and robust way by using $routeChangeStart and $routeChangeError. It's very elegant and doesn't change any intrinsic parts of Angular (no patching). Basically we can consider this normal implementation vs hack
Yes, that's how ngRoute expects it to be solved. It looks neat in its current form. But I found that event-rich (routing-related too) code tends to be messy and unmaintainable, I consider this a problem.
|
1

ui-router have a lot of events that you can easy manipulate. I always use it.

State Change Events have everything you need. Something like this will be implement in the AngularJS 2.x.

But if you are looking the solution for native Angular 1.x.y router this solution will not help you. Sorry

4 Comments

But afaik $stateChangeStart is not much different from the angular's router's $routeChangeStart. You can cancel a route event in either but I don't think that ui-router's event can handle async promises in its handler... Can you provide an example how to do this using ui-router?
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){ event.preventDefault(); /*here I can do something with my promise and then use toState variable for $state.go function as a parameter I would use some spinner for this, too*/ })
You could've edited your answer, you know... Usually a much better option to do so. And also better for others getting to your answer in the future since they don't have to read comments (hopefully not collapsed).
But apart from your code in a comment I dare you to make a simple jsfiddle to prove your point. Because when you get to the here I can do something with my promise I bet you'll be faced with the actual problem.
1

If you can use ui-router, you could do this:

.state('root', {
      url: '',
      abstract: true,
      templateUrl: 'some-template.html',
      resolve: {
        user: ['Auth', function (Auth) {
          return Auth.resolveUser();
        }]
      }
    })

Auth.resolveUser() is just a backend call to load the current user. It returns a promise so the route will wait for that to load before changing.

The route is abstract so other controllers must inherit from it in order to work and you have the added benefit that all child controllers have access to the current user via the resolve.

Now you catch the $stateChangeStart event in app.run():

$rootScope.$on('$stateChangeStart', function (event, next) {
        if (!Auth.signedIn()) { //this operates on the already loaded user
          //user not authenticated
          // all controllers need authentication unless otherwise specified
          if (!next.data || !next.data.anonymous) {
            event.preventDefault();
            $state.go('account.login');
          }
        }else{
         //authenticated 
         // next.data.roles - this is a custom property on a route.
         if(!Auth.hasRequiredRole(next.data.roles)){
            event.preventDefault();
            $state.go('account.my'); //or whatever
         }             
        }
      });

Then a route that requires a role can look like this :

.state('root.dashboard', {
         //it's a child of the *root* route
          url: '/dashboard',
          data: {
             roles: ['manager', 'admin']
          }
          ...
        });

Hope it makes sense.

2 Comments

It does yes. So the only (and main difference) difference between implementation with normal vs. ui-router is the abstract route. Everything else is pretty much the same. Am I right?
Yes, pretty much. Have you considered not doing async routing though? And just load the user from the start on the server when the initial html is rendered ? Then you have your user/permissions etc already loaded - no need for an extra request (that's if you have a backend server)
0

I've approached this issue many times, I've also developed a module (github). My module (built on top of ui.router) is based on $stateChangeStart (ui.router event) but the concept is the same with the default ng.route module, it's just a different implementation way.

In conclusion I think that handling routing changing events is not the good way to perform an authentication checking: For example, when we need to obtain the acl via ajax the events can't help us.

A good way, I think, could be to automatically append a resolve to each "protected" state...

Unfortunately ui.Router doesn't provides an API to intercept the state creation so I started my module rework with a little workaround on top of $stateProvider.state method.

Definitively, I'm looking for different opinions in order to find the correct way to implement a Authentication Service in AngularJS.

if are there anyone that is interested in this research... please, open an issue on my github and the discuss

1 Comment

I think you could implement it very similarly with ui-router as I did it with the help of accepted answer with ngRoute in this example. Although ui-router has an additional gem in the form of urlRouter.sync() that will continue routing after it's been stopped.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.