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.
href="http://plnkr.co/edit/UkHDqFD8P7tTEqSaCOcc?p=preview" rel="noreferrer">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 href="https://github.com/angular-ui/ui-router/wiki"
rel="noreferrer">$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:
' />'
})
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?
- We check to see when
the app loads if the user is logged in. - We track info
about the logged in user. - We redirect them to sign in
state for states that require the user to be logged
in. - We redirect them to an access denied state
if they do not have authorization to access it. - We have a
mechanism to redirect users back to the original state they requested, if we needed them
to log in. - We can sign a user out (needs to be wired up
in concert with any client or server code that manages your auth
ticket). - 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:
ng-show="principal.isAuthenticated()">
I'm logged
in
No comments:
Post a Comment