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.

Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404

4 Answers4

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.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 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? – Robert Koritnik Oct 08 '15 at 20:25
  • 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. – Estus Flask Oct 08 '15 at 21:09
  • 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)'. – Estus Flask Oct 08 '15 at 21:20
  • 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* – Robert Koritnik Oct 08 '15 at 23:14
  • 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. – Estus Flask Oct 09 '15 at 00:48
  • Would this support authorization for specific roles? It doesn't look like it would – sirrocco Oct 09 '15 at 02:04
  • @sirrocco It is the responsibility of authService, it can either check the authorisation against $route.current.originalPath or be a function that gets route path as a parameter. The former will look a bit cleaner, the latter will have a bit of advantage on testability. All that router needs to know is that you're authorized or not. – Estus Flask Oct 09 '15 at 02:32
  • oh, so $route.current.originalPath is the url that you're trying to get to? It would make sense if so but I don't think it is. Sorry if I'm missing something obvious. – sirrocco Oct 09 '15 at 03:58
  • Here's my [interactive example](http://embed.plnkr.co/Xx8qMj/preview) that doesn't use any `unrestricted` variable but rather per-route settings. It could easily be changed to provide role authorization. It also implements authorization service that provides the promise that has to be *waited for*. – Robert Koritnik Oct 09 '15 at 05:49
  • @scirroco: Look at my example in previous comment which could easily be changed to support role-based authorization. Few simple changes: 1. `authService.myInfo()` should return user data with role info, 2. route property should provide role requirements like `authorize: "reader"` or even `authorize:"reader|writer"` (check with regexp) or `authorize:["reader","writer"]` (check with *array.indexOf*) and `authService.authorize` should check user roles info against these. **Job done!** – Robert Koritnik Oct 09 '15 at 05:58
  • Ah, ok, it makes sense. I thought you had to send the service as a string to have it instantiated. Then authorization wouldn't have worked but like this , yeah it's clear. Thanks for the example – sirrocco Oct 13 '15 at 03:15
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

Kirill Husiatyn
  • 828
  • 2
  • 12
  • 20
  • 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? – Robert Koritnik Oct 08 '15 at 14:15
  • `$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*/ })` – Kirill Husiatyn Oct 08 '15 at 14:18
  • 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). – Robert Koritnik Oct 08 '15 at 18:35
  • 1
    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. – Robert Koritnik Oct 08 '15 at 18:36
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.

sirrocco
  • 7,975
  • 4
  • 59
  • 81
  • 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? – Robert Koritnik Oct 08 '15 at 18:39
  • 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) – sirrocco Oct 09 '15 at 01:54
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

jjdfvgds
  • 1
  • 1
  • 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](http://embed.plnkr.co/Xx8qMj/preview). Although ui-router has an additional *gem* in the form of `urlRouter.sync()` that will continue routing after it's been stopped. – Robert Koritnik Oct 09 '15 at 09:36