378

I am new to AngularJS, and I am a little confused of how I can use angular-"ui-router" in the following scenario:

I am building a web application which consists of two sections. The first section is the homepage with its login and signup views, and the second section is the dashboard (after successful login).

I have created an index.html for the home section with its angular app and ui-router config to handle /login and /signup views, and there is another file dashboard.html for the dashboard section with its app and ui-router config to handle many sub views.

Now I finished the dashboard section and don't know how to combine the two sections with their different angular apps. How could I tell the home app to redirect to the dashboard app?

Ahmed Hashem
  • 4,675
  • 3
  • 21
  • 28

10 Answers10

610

I'm in the process of making a nicer demo as well as cleaning up some of these services into a usable module, but here's what I've come up with. This is a complex process to work around some caveats, so hang in there. You'll need to break this down into several pieces.

Take a look at this plunk.

First, you need a service to store the user's identity. I call this principal. It can be checked to see if the user is logged in, and upon request, it can resolve an object that represents the essential information about the user's identity. This can be whatever you need, but the essentials would be a display name, a username, possibly an email, and the roles a user belongs to (if this applies to your app). Principal also has methods to do role checks.

.factory('principal', ['$q', '$http', '$timeout',
  function($q, $http, $timeout) {
    var _identity = undefined,
      _authenticated = false;

    return {
      isIdentityResolved: function() {
        return angular.isDefined(_identity);
      },
      isAuthenticated: function() {
        return _authenticated;
      },
      isInRole: function(role) {
        if (!_authenticated || !_identity.roles) return false;

        return _identity.roles.indexOf(role) != -1;
      },
      isInAnyRole: function(roles) {
        if (!_authenticated || !_identity.roles) return false;

        for (var i = 0; i < roles.length; i++) {
          if (this.isInRole(roles[i])) return true;
        }

        return false;
      },
      authenticate: function(identity) {
        _identity = identity;
        _authenticated = identity != null;
      },
      identity: function(force) {
        var deferred = $q.defer();

        if (force === true) _identity = undefined;

        // check and see if we have retrieved the 
        // identity data from the server. if we have, 
        // reuse it by immediately resolving
        if (angular.isDefined(_identity)) {
          deferred.resolve(_identity);

          return deferred.promise;
        }

        // otherwise, retrieve the identity data from the
        // server, update the identity object, and then 
        // resolve.
        //           $http.get('/svc/account/identity', 
        //                     { ignoreErrors: true })
        //                .success(function(data) {
        //                    _identity = data;
        //                    _authenticated = true;
        //                    deferred.resolve(_identity);
        //                })
        //                .error(function () {
        //                    _identity = null;
        //                    _authenticated = false;
        //                    deferred.resolve(_identity);
        //                });

        // for the sake of the demo, fake the lookup
        // by using a timeout to create a valid
        // fake identity. in reality,  you'll want 
        // something more like the $http request
        // commented out above. in this example, we fake 
        // looking up to find the user is
        // not logged in
        var self = this;
        $timeout(function() {
          self.authenticate(null);
          deferred.resolve(_identity);
        }, 1000);

        return deferred.promise;
      }
    };
  }
])

Second, you need a service that checks the state the user wants to go to, makes sure they're logged in (if necessary; not necessary for signin, password reset, etc.), and then does a role check (if your app needs this). If they are not authenticated, send them to the sign-in page. If they are authenticated, but fail a role check, send them to an access denied page. I call this service authorization.

.factory('authorization', ['$rootScope', '$state', 'principal',
  function($rootScope, $state, principal) {
    return {
      authorize: function() {
        return principal.identity()
          .then(function() {
            var isAuthenticated = principal.isAuthenticated();

            if ($rootScope.toState.data.roles
                && $rootScope.toState
                             .data.roles.length > 0 
                && !principal.isInAnyRole(
                   $rootScope.toState.data.roles))
            {
              if (isAuthenticated) {
                  // user is signed in but not
                  // authorized for desired state
                  $state.go('accessdenied');
              } else {
                // user is not authenticated. Stow
                // the state they wanted before you
                // send them to the sign-in state, so
                // you can return them when you're done
                $rootScope.returnToState
                    = $rootScope.toState;
                $rootScope.returnToStateParams
                    = $rootScope.toStateParams;

                // now, send them to the signin state
                // so they can log in
                $state.go('signin');
              }
            }
          });
      }
    };
  }
])

Now all you need to do is listen in on ui-router's $stateChangeStart. This gives you a chance to examine the current state, the state they want to go to, and insert your authorization check. If it fails, you can cancel the route transition, or change to a different route.

.run(['$rootScope', '$state', '$stateParams', 
      'authorization', 'principal',
    function($rootScope, $state, $stateParams, 
             authorization, principal)
{
      $rootScope.$on('$stateChangeStart', 
          function(event, toState, toStateParams)
      {
        // track the state the user wants to go to; 
        // authorization service needs this
        $rootScope.toState = toState;
        $rootScope.toStateParams = toStateParams;
        // if the principal is resolved, do an 
        // authorization check immediately. otherwise,
        // it'll be done when the state it resolved.
        if (principal.isIdentityResolved()) 
            authorization.authorize();
      });
    }
  ]);

The tricky part about tracking a user's identity is looking it up if you've already authenticated (say, you're visiting the page after a previous session, and saved an auth token in a cookie, or maybe you hard refreshed a page, or dropped onto a URL from a link). Because of the way ui-router works, you need to do your identity resolve once, before your auth checks. You can do this using the resolve option in your state config. I have one parent state for the site that all states inherit from, which forces the principal to be resolved before anything else happens.

$stateProvider.state('site', {
  'abstract': true,
  resolve: {
    authorize: ['authorization',
      function(authorization) {
        return authorization.authorize();
      }
    ]
  },
  template: '<div ui-view />'
})

There's another problem here... resolve only gets called once. Once your promise for identity lookup completes, it won't run the resolve delegate again. So we have to do your auth checks in two places: once pursuant to your identity promise resolving in resolve, which covers the first time your app loads, and once in $stateChangeStart if the resolution has been done, which covers any time you navigate around states.

OK, so what have we done so far?

  1. We check to see when the app loads if the user is logged in.
  2. We track info about the logged in user.
  3. We redirect them to sign in state for states that require the user to be logged in.
  4. We redirect them to an access denied state if they do not have authorization to access it.
  5. We have a mechanism to redirect users back to the original state they requested, if we needed them to log in.
  6. We can sign a user out (needs to be wired up in concert with any client or server code that manages your auth ticket).
  7. We don't need to send users back to the sign-in page every time they reload their browser or drop on a link.

Where do we go from here? Well, you can organize your states into regions that require sign in. You can require authenticated/authorized users by adding data with roles to these states (or a parent of them, if you want to use inheritance). Here, we restrict a resource to Admins:

.state('restricted', {
    parent: 'site',
    url: '/restricted',
    data: {
      roles: ['Admin']
    },
    views: {
      'content@': {
        templateUrl: 'restricted.html'
      }
    }
  })

Now you can control state-by-state what users can access a route. Any other concerns? Maybe varying only part of a view based on whether or not they are logged in? No problem. Use the principal.isAuthenticated() or even principal.isInRole() with any of the numerous ways you can conditionally display a template or an element.

First, inject principal into a controller or whatever, and stick it to the scope so you can use it easily in your view:

.scope('HomeCtrl', ['$scope', 'principal', 
    function($scope, principal)
{
  $scope.principal = principal;
});

Show or hide an element:

<div ng-show="principal.isAuthenticated()">
   I'm logged in
</div>
<div ng-hide="principal.isAuthenticated()">
  I'm not logged in
</div>

Etc., so on, so forth. Anyways, in your example app, you would have a state for home page that would let unauthenticated users drop by. They could have links to the sign-in or sign-up states, or have those forms built into that page. Whatever suits you.

The dashboard pages could all inherit from a state that requires the users to be logged in and, say, be a User role member. All the authorization stuff we've discussed would flow from there.

lanoxx
  • 12,249
  • 13
  • 87
  • 142
moribvndvs
  • 42,191
  • 11
  • 135
  • 149
  • 28
    Thanks, this really helped me to get my own code together. On a side note, if you get an infinite routing loop (UI Router bug), try `$location.path` instead of `$state.go`. – JacobF Apr 23 '14 at 11:11
  • 2
    This is a great answer and it has helped me alot. When I set user = principal in my controller and try to call say user.identity().name in my view to get the currently logged in users name I only seem to get the promise object {then: fn, catch: fn, finally:} returned and not the actual _identity object. If I use user.identity.then(fn(user)) I can get the user object but this seems like a lot of code for the view am I missing something? – Mark Jun 21 '14 at 14:19
  • 4
    @Ir1sh I would resolve the identity first in the controller and assign it to `$scope.user` in your `then` function. You can still reference `user` in your views; when it is resolved, the view will be updated. – moribvndvs Jun 21 '14 at 21:46
  • 2
    @HackedByChinese I think your demo is no longer working. – Blowsie Jul 01 '14 at 13:01
  • HackedByChinese: if I call my app e.g. localhost/app/index.html#/state22 then $stateChangeStart is also triggered, so why do you need the parentstate "site"? @Blowsie No, his demo is working well (IE 11 tested). – Sebastian Jul 02 '14 at 12:27
  • @Blowsie I see a little bug if I sign out and then try to sign in again, if that's the issue you are referring to. – moribvndvs Jul 02 '14 at 13:48
  • @Sebastian I need to make sure the first state the browser goes to calls my `resolve` code to force it to check if I've already been signed in. For example, if I log in and go to state22 and manually refresh my browser, I don't want it to send me to the login page again. My `resolve` logic takes care of this for me, _provided that `resolve` is set on each state where I want this behavior_ (which I assume would be any state in the application). I don't want to have to manually add `resolve` to every state in my application; that's too much typing. I save some chars by using `parent: 'site'`. – moribvndvs Jul 02 '14 at 14:00
  • Okay, I saw it only when I made a "stateless" call to my page without calling `otherwise('somestate')`, then the statechangestart event get not called. So I will need to use `parent: 'site'` – Sebastian Jul 03 '14 at 06:10
  • 7
    @jvannistelrooy I had problems with go() to, but after putting it inside the then after calling a noop function like this `$q.when(angular.noop).then(function(){$state.go('myState')`, everything works as expected. If I call `$state.go` while another state transition is not completed, then it will not work (I think that's the reason why it won't work). – Sebastian Jul 03 '14 at 11:29
  • @HackedByChinese it must have been a temporary issue with plunkr or my machine, its working fine now. I don't like the fact all routes are children of the site route. Is there a way round this? I tried the resolver on all of the routes, but as a result all of the child routes will also call `authorize`. Resulting it it being called my times per page change – Blowsie Jul 07 '14 at 09:59
  • Maybe I'm missing something, but do you have any logic to send a user to the login page when they are not logged in and trying to access a page that requires login? (It seems to only work if the State has roles the user is not in. But you're only current role is 'Admin'. I did a similar setup, I have a 'root' parent state and a 'root.auth' parent state that all pages that require login are under. Then I have roles to restrict access beyond just logged in. – Bob Monteverde Aug 20 '14 at 02:04
  • I tweaked authorize to have logged in required states (and ones that don't require). My issue is, when a user not logged in goes directly to a page that requires that, while the authorize() call is contacting the session api on server, I get about 4 calls to the session (I think it's looping till the session comes back). Anyone else have this, and have a clean solution? Otherwise, Beautiful job, while I didn't use this 100% directly, I definitely stole most of the ideas from here, really happy with the result, minus that. – Bob Monteverde Aug 20 '14 at 02:11
  • One last thing. I also still need to deal with if the user logs out in a different window, or the session on backend closes while user still is on the page 'logged in'. I'm thinking about watching the API calls in each state's resolve, if they come back not authorized, go to login state. Got any better techniques, maybe one that doesn't require code in every logged in state. – Bob Monteverde Aug 20 '14 at 02:12
  • @BobMonteverde You can add a flag to your states' data, `allowAnonymous`, and update the `authorization.authorize` logic to let unauthenticated users in if this flag is true for the `toState`, otherwise roles are checked as before. I can't comment on the looping calls you're seeing; open a new question and we can explore. As for the logout, you can definitely watch for 401 w/ an http interceptor (you can even queue the failed calls and replay them once logged in again). Also, look at https://github.com/hackedbychinese/ng-idle to handle idle users. – moribvndvs Aug 20 '14 at 04:36
  • @Sebastian Could you explain what `$q.when(angular.noop)` does and why it's working? Thanks – Sydney Aug 29 '14 at 19:01
  • 1
    @Sydney it is just a dummy promise giving other promises the chance to get called before the state change gets called. – Sebastian Sep 08 '14 at 05:58
  • @HackedByChinese +1, Hi there, just asking if you have new/upgraded implementation of your code. Maybe something that supports offline mode. Thanks! – fiberOptics Sep 28 '14 at 15:10
  • @HackedByChinese I found that this is working on web browser but not in PhoneGap. I'm having issue on the `resolve`, it always redirect to `$stateChangeError`. Any idea? – fiberOptics Oct 10 '14 at 09:53
  • Would anyone know why when using the accepted answer / setup described above the address bar no longer displays the url and all the fragments and query string params? Since implementing this the address bar no longer allows our app to be bookmarked. – Frankie Loscavio Oct 22 '14 at 15:01
  • 1
    template: '', is a requirement of abstract states of ui.router https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#abstract-states – technoSpino Mar 24 '15 at 10:54
  • What is the use case for principle.identity(true)? – Andi Mar 31 '15 at 07:42
  • 2
    @Andi If you want or need to force it to re-resolve the identity (thereby making it issue the web request to check your identity with the remote server, or whatever your mechanism is) rather than continue using the identity you have cached at the client side. For example, if you made a request to a resource and got a 401, you might want to flush out the cached identity to make sure a user's claims are still valid before letting them do or access protected resources. Or perhaps you'll periodically do it to make sure the user is actually still logged on and authorized. – moribvndvs Mar 31 '15 at 07:52
  • Not a technical question, but why do you call it "principal"? The name confuses me; I don't know what to expect from such a service at face value. Couldn't it be called something like "currentUser" or "visitor"? – Hugh Guiney Apr 23 '15 at 10:43
  • You can call it whatever you like. I come from the .NET world, where a Principal represents both the identity and roles assigned to a physical user, so the term was natural for me. If you're more comfortable with something else, it won't change anything so long as you change all your references to match. – moribvndvs Apr 23 '15 at 20:37
  • I got the following error while I'm loading the Index page where is the `ng-show="principal.isAuthenticated()"` `0x800a139e - JavaScript runtime error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting! Watchers fired in the last 5 iterations: []` – kamyk May 16 '16 at 07:41
  • Can I have some experinece feedback from using this example, if it's possible what are the consideration to make in order to write it in Typescript, please? – Naou Aug 08 '16 at 14:53
  • $stateChangeStart doesn't fire because all state events are now disabled by default since ui-router version 1.0.0alpha0 . Because state events are now deprecated, could you update your example to use transition hooks? https://github.com/angular-ui/ui-router/releases/tag/1.0.0alpha0 – user64141 Sep 18 '16 at 02:20
  • 1
    @user64141 There is a file `stateEvents.js` that emulates old behavior, implemented with new Transition object, you can take a look there. It will be something like this: `$transitions.onStart({to: '*'}, function($transition$) { $rootScope.toState = $transition$.to(); $rootScope.toStateParams = $transition$.params('to'); });` – Martin Adámek Sep 26 '16 at 16:39
  • 1
    No offense inteded but this code sample you provided in your answer is unreadable! – Playdome.io Apr 02 '17 at 08:52
120

The solutions posted so far are needlessly complicated, in my opinion. There's a simpler way. The documentation of ui-router says listen to $locationChangeSuccess and use $urlRouter.sync() to check a state transition, halt it, or resume it. But even that actually doesn't work.

However, here are two simple alternatives. Pick one:

Solution 1: listening on $locationChangeSuccess

You can listen to $locationChangeSuccess and you can perform some logic, even asynchronous logic there. Based on that logic, you can let the function return undefined, which will cause the state transition to continue as normal, or you can do $state.go('logInPage'), if the user needs to be authenticated. Here's an example:

angular.module('App', ['ui.router'])

// In the run phase of your Angular application  
.run(function($rootScope, user, $state) {

  // Listen to '$locationChangeSuccess', not '$stateChangeStart'
  $rootScope.$on('$locationChangeSuccess', function() {
    user
      .logIn()
      .catch(function() {
        // log-in promise failed. Redirect to log-in page.
        $state.go('logInPage')
      })
  })
})

Keep in mind that this doesn't actually prevent the target state from loading, but it does redirect to the log-in page if the user is unauthorized. That's okay since real protection is on the server, anyway.

Solution 2: using state resolve

In this solution, you use ui-router resolve feature.

You basically reject the promise in resolve if the user is not authenticated and then redirect them to the log-in page.

Here's how it goes:

angular.module('App', ['ui.router'])

.config(
  function($stateProvider) {
    $stateProvider
      .state('logInPage', {
        url: '/logInPage',
        templateUrl: 'sections/logInPage.html',
        controller: 'logInPageCtrl',
      })
      .state('myProtectedContent', {
        url: '/myProtectedContent',
        templateUrl: 'sections/myProtectedContent.html',
        controller: 'myProtectedContentCtrl',
        resolve: { authenticate: authenticate }
      })
      .state('alsoProtectedContent', {
        url: '/alsoProtectedContent',
        templateUrl: 'sections/alsoProtectedContent.html',
        controller: 'alsoProtectedContentCtrl',
        resolve: { authenticate: authenticate }
      })

    function authenticate($q, user, $state, $timeout) {
      if (user.isAuthenticated()) {
        // Resolve the promise successfully
        return $q.when()
      } else {
        // The next bit of code is asynchronously tricky.

        $timeout(function() {
          // This code runs after the authentication promise has been rejected.
          // Go to the log-in page
          $state.go('logInPage')
        })

        // Reject the authentication promise to prevent the state from loading
        return $q.reject()
      }
    }
  }
)

Unlike the first solution, this solution actually prevents the target state from loading.

M.K. Safi
  • 6,560
  • 10
  • 40
  • 58
  • 6
    @FredLackey say unauthenticated user is in `state A`. They click a link to go to `protected state B` but you want to redirect them to `logInPage`. If there's no `$timeout`, `ui-router` will simply halt all state transitions, so the user would be stuck in `state A`. The `$timeout` allows `ui-router` to first prevent the initial transition to `protected state B` because the resolve was rejected and after that's done, it redirects to `logInPage`. – M.K. Safi Apr 21 '15 at 17:53
  • Where is the `authenticate` function actually called? – CodyBugstein Jun 09 '15 at 03:46
  • @Imray `authenticate` function is passed as a parameter to `ui-router`. You don't have to call it yourself. `ui-router` calls it. – M.K. Safi Jun 09 '15 at 16:05
  • Why are you using '$locationChangeSuccess' instead of '$stateChangeStart'? – Draex_ Aug 08 '15 at 07:01
  • @PeterDraexDräxler I was mostly following the documentation. Did you notice any difference by using `$stateChangeStart`? – M.K. Safi Aug 08 '15 at 14:39
  • @MKSafi $locationChangeSuccess is fired after the new state is fully loaded. $stateChangeStart is fired before loading of the new state. Therefore with $locationChangeSuccess the "forbidden" page gets displayed for half a second, while with $stateChangeStart it doesn't. – Draex_ Aug 08 '15 at 17:59
  • @PeterDraexDräxler Oh ok. I vaguely recall trying `$stateChangeStart` while I was hacking at this, but it didn't quite do what I needed. I don't remember why now. – M.K. Safi Aug 08 '15 at 21:40
  • Thanks so much that helped a lot. I did it in a different way. I stored the user object as true in a cookie when he logged in and stored it as false when he logged out and was successfully able to do it. Thanks... Any possible security issue because of this? – Saras Arya Feb 24 '16 at 14:34
  • I am attempting to implement Solution 2 and it's working, however my controller is instantiated during the process end the UI for it is shown briefly before the user is redirected to the login page. Is there an alternative to this? This is obviously not the desired behavior. – Mike Feltman Mar 07 '16 at 14:31
  • in second solucion, how to control when session finishes on the server? i mean, how do you check in js when you session has expired on the server? what would it happen when the session exipres on the server and then want to access a protected page? – oware Mar 17 '16 at 19:06
  • @M.K.Safi i have used second solution, but i am not able to redirect to home page, after it is successfully authenticated, i have tried `$state.go`, window.location etc.. inside `if (user.isAuthenticated()) {` and success promise of http request , but it didn't work. Could you please tell me why. no errors in console – codelearner Apr 29 '16 at 05:51
  • In this solution it doesn't appear possible to set a redirect state parameter to send a user to the page they wanted after the have logged in. – Ashley Apr 26 '17 at 17:52
  • have a look at the $transistions hooks: https://ui-router.github.io/ng1/docs/latest/interfaces/transition.ihookregistry.html#onstart – kaiser Nov 18 '18 at 22:20
42

The easiest solution is to use $stateChangeStart and event.preventDefault() to cancel the state change when the user is not authenticated and redirect him to the auth state that is the login page.

angular
  .module('myApp', [
    'ui.router',
  ])
    .run(['$rootScope', 'User', '$state',
    function ($rootScope, User, $state) {
      $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
        if (toState.name !== 'auth' && !User.authenticaded()) {
          event.preventDefault();
          $state.go('auth');
        }
      });
    }]
  );
T J
  • 42,762
  • 13
  • 83
  • 138
sebest
  • 2,119
  • 1
  • 14
  • 5
  • 6
    I don't think this will work if User.authenticaded() is an async call. That's the holy grail everyone is after. For example, if every state *except* "login" is secured, I want to confirm the user is still authenticated *BEFORE* loading any state. Using resolves sucks because they only resolve once, and in order to prevent child states from loading, you have to inject the resolve into *EVERY CHILD*. – Jason Oct 23 '15 at 17:53
  • authenticated is not an async call in my case: ` this.authenticaded = function() { if (this.currentAccountID !== null) { return true; } return false; }; ` – sebest Nov 13 '15 at 03:30
  • As per: http://stackoverflow.com/a/38374313/849829, 'run' comes way above 'service's and hence the problems. Checking localstorage for the authenticated status seems to be a good approach. – Deepak Thomas Jan 31 '17 at 04:45
22

I think you need a service that handle the authentication process (and its storage).

In this service you'll need some basic methods :

  • isAuthenticated()
  • login()
  • logout()
  • etc ...

This service should be injected in your controllers of each module :

  • In your dashboard section, use this service to check if user is authenticated (service.isAuthenticated() method) . if not, redirect to /login
  • In your login section, just use the form data to authenticate the user through your service.login() method

A good and robust example for this behavior is the project angular-app and specifically its security module which is based over the awesome HTTP Auth Interceptor Module

Hope this helps

Jscti
  • 14,096
  • 4
  • 62
  • 87
21

I Created this module to help make this process piece of cake

You can do things like:

$routeProvider
  .state('secret',
    {
      ...
      permissions: {
        only: ['admin', 'god']
      }
    });

Or also

$routeProvider
  .state('userpanel',
    {
      ...
      permissions: {
        except: ['not-logged-in']
      }
    });

It's brand new but worth checking out!

https://github.com/Narzerus/angular-permission

Rafael Vidaurre
  • 474
  • 4
  • 10
  • 2
    whats to stop me editing source at runtime and removing your 'admin' || 'god' and continuing ? – Pogrindis Sep 25 '14 at 09:49
  • 12
    I would hope that any data requests that require authorization are also being verified at the server. – Ben Ripley Sep 30 '14 at 13:36
  • 24
    This is not meant for security, client-sided authorization never is as you can always change the values. You could even intercept responses from server side and evaluate them as "authorized" The point of permissions/authorization in client side are to avoid letting the user do forbidden stuff for ux purposes. For example if you are handling an admin-only action, even if the user maliciously tricks the client to allow sending a restricted request to the server, the server would still return a 401 response. This is of course always responsibility of the api being implemented @BenRipley indeed – Rafael Vidaurre Oct 16 '14 at 19:36
  • 3
    Great response to the question Rafael. Always protect the api's because the front-end is the most reverse engineerable, spoofable thing there is almost. – Frankie Loscavio Oct 22 '14 at 13:12
  • When refreshing the navigator (F5), roles info usually is lost with angular-permission, I posted a solution at https://github.com/Narzerus/angular-permission/issues/36 just in case you decide to use this module! – Aquiles Jul 15 '15 at 14:41
  • What about browser history? angular-permission code prevent default state redirect https://github.com/Narzerus/angular-permission/blob/master/src/core/permissionModule.js#L24 and then do addition state transition https://github.com/Narzerus/angular-permission/blob/master/src/core/permissionModule.js#L100-L105 This code brakes browser history, so if you click back button it does not work – Bohdan Lyzanets Feb 23 '16 at 16:51
  • 1
    This problem with the history is solved for quite a while now @Bohdan. You can safely use it even with ui-router extras. – masterspambot Apr 16 '16 at 14:34
16

I wanted to share another solution working with the ui router 1.0.0.X

As you may know, stateChangeStart and stateChangeSuccess are now deprecated. https://github.com/angular-ui/ui-router/issues/2655

Instead you should use $transitions http://angular-ui.github.io/ui-router/1.0.0-alpha.1/interfaces/transition.ihookregistry.html

This is how I achieved it:

First I have and AuthService with some useful functions

angular.module('myApp')

        .factory('AuthService',
                ['$http', '$cookies', '$rootScope',
                    function ($http, $cookies, $rootScope) {
                        var service = {};

                        // Authenticates throug a rest service
                        service.authenticate = function (username, password, callback) {

                            $http.post('api/login', {username: username, password: password})
                                    .success(function (response) {
                                        callback(response);
                                    });
                        };

                        // Creates a cookie and set the Authorization header
                        service.setCredentials = function (response) {
                            $rootScope.globals = response.token;

                            $http.defaults.headers.common['Authorization'] = 'Bearer ' + response.token;
                            $cookies.put('globals', $rootScope.globals);
                        };

                        // Checks if it's authenticated
                        service.isAuthenticated = function() {
                            return !($cookies.get('globals') === undefined);
                        };

                        // Clear credentials when logout
                        service.clearCredentials = function () {
                            $rootScope.globals = undefined;
                            $cookies.remove('globals');
                            $http.defaults.headers.common.Authorization = 'Bearer ';
                        };

                        return service;
                    }]);

Then I have this configuration:

angular.module('myApp', [
    'ui.router',
    'ngCookies'
])
        .config(['$stateProvider', '$urlRouterProvider',
            function ($stateProvider, $urlRouterProvider) {
                $urlRouterProvider.otherwise('/resumen');
                $stateProvider
                        .state("dashboard", {
                            url: "/dashboard",
                            templateUrl: "partials/dashboard.html",
                            controller: "dashCtrl",
                            data: {
                                authRequired: true
                            }
                        })
                        .state("login", {
                            url: "/login",
                            templateUrl: "partials/login.html",
                            controller: "loginController"
                        })
            }])

        .run(['$rootScope', '$transitions', '$state', '$cookies', '$http', 'AuthService',
            function ($rootScope, $transitions, $state, $cookies, $http, AuthService) {

                // keep user logged in after page refresh
                $rootScope.globals = $cookies.get('globals') || {};
                $http.defaults.headers.common['Authorization'] = 'Bearer ' + $rootScope.globals;

                $transitions.onStart({
                    to: function (state) {
                        return state.data != null && state.data.authRequired === true;
                    }
                }, function () {
                    if (!AuthService.isAuthenticated()) {
                        return $state.target("login");
                    }
                });
            }]);

You can see that I use

data: {
   authRequired: true
}

to mark the state only accessible if is authenticated.

then, on the .run I use the transitions to check the autheticated state

$transitions.onStart({
    to: function (state) {
        return state.data != null && state.data.authRequired === true;
    }
}, function () {
    if (!AuthService.isAuthenticated()) {
        return $state.target("login");
    }
});

I build this example using some code found on the $transitions documentation. I'm pretty new with the ui router but it works.

Hope it can helps anyone.

5

Here is how we got out of the infinite routing loop and still used $state.go instead of $location.path

if('401' !== toState.name) {
  if (principal.isIdentityResolved()) authorization.authorize();
}
T J
  • 42,762
  • 13
  • 83
  • 138
Jason Girdner
  • 59
  • 1
  • 1
  • 1
    Would anyone know why when using the accepted answer / setup described above the address bar no longer displays the url and all the fragments and query string params? Since implementing this the address bar no longer allows our app to be bookmarked. – Frankie Loscavio Oct 22 '14 at 13:44
  • 1
    Isn't this supposed to be a comment on one of the existing answers? Because there is no such code in OP and it is not even clear which answer/what code this is referring to – T J Jun 29 '16 at 06:18
3

I have another solution: that solution works perfectly when you have only content you want to show when you are logged in. Define a rule where you checking if you are logged in and its not path of whitelist routes.

$urlRouterProvider.rule(function ($injector, $location) {
   var UserService = $injector.get('UserService');
   var path = $location.path(), normalized = path.toLowerCase();

   if (!UserService.isLoggedIn() && path.indexOf('login') === -1) {
     $location.path('/login/signin');
   }
});

In my example i ask if i am not logged in and the current route i want to route is not part of `/login', because my whitelist routes are the following

/login/signup // registering new user
/login/signin // login to app

so i have instant access to this two routes and every other route will be checked if you are online.

Here is my whole routing file for the login module

export default (
  $stateProvider,
  $locationProvider,
  $urlRouterProvider
) => {

  $stateProvider.state('login', {
    parent: 'app',
    url: '/login',
    abstract: true,
    template: '<ui-view></ui-view>'
  })

  $stateProvider.state('signin', {
    parent: 'login',
    url: '/signin',
    template: '<login-signin-directive></login-signin-directive>'
  });

  $stateProvider.state('lock', {
    parent: 'login',
    url: '/lock',
    template: '<login-lock-directive></login-lock-directive>'
  });

  $stateProvider.state('signup', {
    parent: 'login',
    url: '/signup',
    template: '<login-signup-directive></login-signup-directive>'
  });

  $urlRouterProvider.rule(function ($injector, $location) {
    var UserService = $injector.get('UserService');
    var path = $location.path();

    if (!UserService.isLoggedIn() && path.indexOf('login') === -1) {
         $location.path('/login/signin');
    }
  });

  $urlRouterProvider.otherwise('/error/not-found');
}

() => { /* code */ } is ES6 syntax, use instead function() { /* code */ }

Chris Incoqnito
  • 235
  • 3
  • 12
3

Use $http Interceptor

By using an $http interceptor you can send headers to Back-end or the other way around and do your checks that way.

Great article on $http interceptors

Example:

$httpProvider.interceptors.push(function ($q) {
        return {
            'response': function (response) {

                // TODO Create check for user authentication. With every request send "headers" or do some other check
                return response;
            },
            'responseError': function (reject) {

                // Forbidden
                if(reject.status == 403) {
                    console.log('This page is forbidden.');
                    window.location = '/';
                // Unauthorized
                } else if(reject.status == 401) {
                    console.log("You're not authorized to view this page.");
                    window.location = '/';
                }

                return $q.reject(reject);
            }
        };
    });

Put this in your .config or .run function.

TSlegaitis
  • 1,231
  • 15
  • 29
2

First you'll need a service that you can inject into your controllers that has some idea of app authentication state. Persisting auth details with local storage is a decent way to approach it.

Next, you'll need to check the state of auth right before state changes. Since your app has some pages that need to be authenticated and others that don't, create a parent route that checks auth, and make all other pages that require the same be a child of that parent.

Finally, you'll need some way to tell if your currently logged in user can perform certain operations. This can be achieved by adding a 'can' function to your auth service. Can takes two parameters: - action - required - (ie 'manage_dashboards' or 'create_new_dashboard') - object - optional - object being operated on. For example, if you had a dashboard object, you may want to check to see if dashboard.ownerId === loggedInUser.id. (Of course, information passed from the client should never be trusted and you should always verify this on the server before writing it to your database).

angular.module('myApp', ['ngStorage']).config([
   '$stateProvider',
function(
   $stateProvider
) {
   $stateProvider
     .state('home', {...}) //not authed
     .state('sign-up', {...}) //not authed
     .state('login', {...}) //not authed
     .state('authed', {...}) //authed, make all authed states children
     .state('authed.dashboard', {...})
}])
.service('context', [
   '$localStorage',
function(
   $localStorage
) {
   var _user = $localStorage.get('user');
   return {
      getUser: function() {
         return _user;
      },
      authed: function() {
         return (_user !== null);
      },
      // server should return some kind of token so the app 
      // can continue to load authenticated content without having to
      // re-authenticate each time
      login: function() {
         return $http.post('/login.json').then(function(reply) {
            if (reply.authenticated === true) {
               $localStorage.set(_userKey, reply.user);
            }
         });
      },
      // this request should expire that token, rendering it useless
      // for requests outside of this session
      logout: function() {
         return $http.post('logout.json').then(function(reply) {
            if (reply.authenticated === true) {
               $localStorage.set(_userKey, reply.user);
            }
         });
      },
      can: function(action, object) {
         if (!this.authed()) {
            return false;
         }

         var user = this.getUser();

         if (user && user.type === 'admin') {
             return true;
         }

         switch(action) {
            case 'manage_dashboards':
               return (user.type === 'manager');
         }

         return false;


      }
   }
}])
.controller('AuthCtrl', [
   'context', 
   '$scope', 
function(
   context, 
   $scope
) {
   $scope.$root.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
      //only require auth if we're moving to another authed page
      if (toState && toState.name.indexOf('authed') > -1) {
         requireAuth();
      }
   });

   function requireAuth() {
      if (!context.authed()) {
         $state.go('login');
      }
   }
}]

** DISCLAIMER: The above code is pseudo-code and comes with no guarantees **

colefner
  • 1,812
  • 1
  • 16
  • 11