Tuesday 17 October 2017

javascript - AngularJS ui-router login authentication

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?




  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:



            ng-show="principal.isAuthenticated()">
I'm logged
in

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




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.

No comments:

Post a Comment

php - file_get_contents shows unexpected output while reading a file

I want to output an inline jpg image as a base64 encoded string, however when I do this : $contents = file_get_contents($filename); print &q...