Angularjs Ui-Router Login Authentication

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.

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.

angularjs + ui-router: redirect to login page when user is not logged in on each state change

For some reason this code worked:

app.run(function($rootScope, $state, $location, UserService){
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
let loggedIn = UserService.isLoggedIn();
if (toState.name !== 'app.login' && !loggedIn){
$location.url('/login');
}
});
});

I changed the way I'm redirecting to the login page from $state.go to
$location.url.

Each time I used the $state.go it fired another $stateChangeStart event and this was, apparently, wrong.
In this way the event.preventDefault() is not necessary.

Angularjs ui-router. How to redirect to login page

The point is, do not redirect if not needed === if already redirected to intended state. There is a working plunker with similar solution

.run(function($rootScope, $location, $state, authenticationSvc) {

$rootScope.$on( '$stateChangeStart', function(e, toState , toParams
, fromState, fromParams) {

var isLogin = toState.name === "login";
if(isLogin){
return; // no need to redirect
}

// now, redirect only not authenticated

var userInfo = authenticationSvc.getUserInfo();

if(userInfo.authenticated === false) {
e.preventDefault(); // stop current execution
$state.go('login'); // go to login
}
});
});

Check these for similar explanation:

  • Angular ui router - Redirection doesn't work at all
  • How can I fix 'Maximum call stack size exceeded' AngularJS

UI Router and Satellizer force login

I found the answer in this Github Repo

the idea is to have role based authentication as discussed in the Repo readme.

so the code for forbidding the unauthorised access to /profile would be something like the following

  $stateProvider.state("profile", {
url: "/profile",
templateUrl: "app/profile/profile.html",
controller: "ProfileCtrl",
controllerAs: "user",
data: {
permissions: {
except: ['guest'],
redirectTo: 'sign_in'
}
}
});

given that you defined the guest role

definePermissions = function(Permission, Identity) {
Permission.defineRole('guest', function(stateParams) {
return !Identity.currentUser;
});
};


Related Topics



Leave a reply



Submit