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.
OK, after much hair pulling, here is what I figured out.
resolve
is the appropriate place to initiate any async calls and ensure they complete before the state is transitioned to.resolve
takes place, that way if someone refreshes the browser, your async resolution still happens and your authentication works properly. You can use the parent
property on a state
to make another state that would otherwise not be inherited by naming/dot notation. This helps in preventing your state names from becoming unmanageable.resolve
, you can't access the toState
or toStateParams
of the state it is trying to transition to. However, the $stateChangeStart
event will happen before your resolve
is resolved. So, you can copy toState
and toStateParams
from the event args to your $rootScope
, and inject $rootScope
into your resolve
function. Now you can access the state and params it is trying to transition to.$state.go()
to send them to the login page, or do whatever you need to do. There is a caveat to that, of course.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 in resolve
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 in resolve
but in the event. if the resource(s) are not resolved, then the check in resolve
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:
$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.onEnter
method.This plunk is a working example.
We hit a similar issue. We felt we needed to make the logic which makes the HTTP call accessible to the logic that handles the response. They're separate in the code, so a service is a good way to do this.
We resolved that separation by wrapping the $http.get
call in a service which
caches the response and calls the success callback immediately if the cache is
already populated. E.g.
app.service('authorizationService', ['authority', function (authority) {
var requestData = undefined;
return {
get: function (successCallback) {
if (typeof requestData !== 'undefined') {
successCallback(requestData);
}
else {
$http.get('/svc/account/identity').success(function (data) {
requestData = data;
successCallback(data);
});
}
}
};
}]);
This acts as a guard around the request being successful. You could then call authorizationService.get()
within your $stateChangeStart
handler safely.
This approach is vulnerable to a race condition if a request is in already progress when authorizationService.get()
is called. It might be possible to introduce some XHR bookkeeping to prevent that.
Alternatively you could publish a custom event once the HTTP request has
completed and register a subscriber for that event within the
$stateChangeStart
handler. You would need to unregister that handler later
though, perhaps in a $stateChangeEnd
handler.
None of this can complete until the authorisation is done, so you should inform the user that they need to wait by showing a loading view.
Also, there's some interesting discussion of authentication with ui-router
in
How to Filter Routes? on the AngularJS Google Group.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With