I have written an app where I need to retrieve the currently logged in user's info when the application runs, before routing is handled. I use ui-router to support multiple/nested views and provide richer, stateful routing.
When a user logs in, they may store a cookie representing their auth token. I include that token with a call to a service to retrieve the user's info, which includes what groups they belong to. The resulting identity is then set in a service, where it can be retrieved and used in the rest of the application. More importantly, the router will use that identity to make sure they are logged in and belong to the appropriate group before transitioning them to the requested state.
I have code something like this:
app
.config(['$stateProvider', function($stateProvider) {
// two states; one is the protected main content, the other is the sign-in screen
$stateProvider
.state('main', {
url: '/',
data: {
roles: ['Customer', 'Staff', 'Admin']
},
views: {} // omitted
})
.state('account.signin', {
url: '/signin',
views: {} // omitted
});
}])
.run(['$rootScope', '$state', '$http', 'authority', 'principal', function($rootScope, $state, $http, authority, principal) {
$rootScope.$on('$stateChangeStart', function (event, toState) { // listen for when trying to transition states...
var isAuthenticated = principal.isAuthenticated(); // check if the user is logged in
if (!toState.data.roles || toState.data.roles.length == 0) return; // short circuit if the state has no role restrictions
if (!principal.isInAnyRole(toState.data.roles)) { // checks to see what roles the principal is a member of
event.preventDefault(); // role check failed, so...
if (isAuthenticated) $state.go('account.accessdenied'); // tell them they are accessing restricted feature
else $state.go('account.signin'); // or they simply aren't logged in yet
}
});
$http.get('/svc/account/identity') // now, looks up the current principal
.success(function(data) {
authority.authorize(data); // and then stores the principal in the service (which can be injected by requiring "principal" dependency, seen above)
}); // this does its job, but I need it to finish before responding to any routes/states
}]);
It all works as expected if I log in, navigate around, log out, etc. The issue is that if I refresh or drop on a URL while I am logged in, I get sent to the signin screen because the identity service call has not finished before the state changes. After that call completes, though, I could feasibly continue working as expected if there is a link or something to- for example- the main
state, so I'm almost there.
I am aware that you can make states wait to resolve parameters before transitioning, but I'm not sure how to proceed.
Answer
OK, after much hair pulling, here is what I figured out.
- As you might expect,
resolve
is the appropriate place to initiate any async calls and ensure they complete before the state is transitioned to. - You will want to make an abstract parent state for all states that ensures your
resolve
takes place, that way if someone refreshes the browser, your async resolution still happens and your authentication works properly. You can use theparent
property on astate
to make another state that would otherwise not be inherited by naming/dot notation. This helps in preventing your state names from becoming unmanageable. - While you can inject whatever services you need into your
resolve
, you can't access thetoState
ortoStateParams
of the state it is trying to transition to. However, the$stateChangeStart
event will happen before yourresolve
is resolved. So, you can copytoState
andtoStateParams
from the event args to your$rootScope
, and inject$rootScope
into yourresolve
function. Now you can access the state and params it is trying to transition to. - Once you have resolved your resource(s), you can use promises to do your authorization check, and if it fails, use
$state.go()
to send them to the login page, or do whatever you need to do. There is a caveat to that, of course. - Once
resolve
is done in the parent state, ui-router won't resolve it again. That means your security check won't occur! Argh! The solution to this is to have a two-part check. Once inresolve
as we've already discussed. The second time is in the$stateChangeStart
event. The key here is to check and see if the resource(s) are resolved. If they are, do the same security check you did inresolve
but in the event. if the resource(s) are not resolved, then the check inresolve
will pick it up. To pull this off, you need to manage your resources within a service so you can appropriately manage state.
Some other misc. notes:
- Don't bother trying to cram all of the authz logic into
$stateChangeStart
. While you can prevent the event and do your async resolution (which effectively stops the change until you are ready), and then try and resume the state change in your promise success handler, there are some issues preventing that from working properly. - You can't change states in the current state's
onEnter
method.
This plunk is a working example.
No comments:
Post a Comment