Reputation:
I'm building a SPA with Angular 1.5 where users have multiple roles [admin, 'see everything', 'edit specific stuff', basic user]. I spent whole day googling, stumbling across ton of different solutions, most seem to be outdated. My backend is Slim application, using token authentication.
My questions: How do I implement displaying same ng-view differently for each role? Should backend return my role and token after I login? How do I check if user is logged in when he tries to change location? I assume just checking if token is stored on client wont be enough, since I have no way of knowing when if it's expired?
I'm really new to token based authentication, and to Angular. I have lack of understanding how front end should be handled in my case, and I have no idea where to start. I'd appreciate any answer, suggestion, tip that'll help me understand what and how things should be done. Thank you.
Upvotes: 3
Views: 2764
Reputation: 1442
First of all, use UI-Router to manage your states i.e. urls.
Upon successful login, your backend must return an access token and but may or may not return role with it. But you can always send another request to get user data and roles and save them to your local storage. You can create a service e.g. LoggedInUser
which gets user information and roles.
angular
.module('myApp')
.factory('LoggedInUser', [
'$q',
'AppConfig',
'User',
function ($q,
AppConfig,
User) {
function getUser() {
var defer = $q.defer();
var userId = localStorage.getItem('$MyApp$userId');
var user = null;
var userStringified = localStorage.getItem('$MyApp$currentUser');
try {
user = JSON.parse(userStringified);
} catch (error) {
defer.reject(error);
}
if (user) {
defer.resolve(user);
} else {
User.findById({ // This is a server query
id: userId,
filter: {
include: 'roles'
}
})
.$promise
.then(function (userData) {
localStorage.setItem('$MyApp$currentUser', JSON.stringify(userData));
defer.resolve(userData);
}, function (error) {
defer.reject(error);
});
}
return defer.promise;
}
function clearUser() {
localStorage.removeItem('$MyApp$currentUser');
}
return {
getUser: getUser,
clearUser: clearUser
}
}]);
You can use Angular Interceptors to intercept server responses having a particular status code or any custom field that you can add from server code e.g. code: 'TOKEN_EXPIRED'
on the response object and then take some action like logging out the user or sending another request from interceptor to get another token in order to continue operation.
It depends upon how important your request is. In my case I can show a message, logout the user.
Handling multiple roles have different meaning for front end and back end.
In front end, restriction is placed at two levels
You may want to prevent a state from being active based on certain condition like
In order to have this restriction, you will need 2 data items, one is to check if state needs authentication and another to check allowed roles of that state. I'm using authenticate
having true/false
and allowedRoles
having an array of allowed roles. You can then use them in $stateChangeStart
event. Here is the code:
In some js file where you are configuring states:
$stateProvider
.state('home', {
abstract: true,
url: '',
templateUrl: 'app/home/home.html',
controller: 'HomeController',
resolve: {
loggedInUser: [
'LoggedInUser',
'$window',
'$q',
'User',
function (LoggedInUser,
$window,
$q,
User) {
var defer = $q.defer();
LoggedInUser.getUser()
.then(function (user) {
defer.resolve(user);
}, function (error) {
$window.alert('User is not authenticated.');
defer.reject(error);
});
return defer.promise;
}
]
},
data: {
authenticate: true
}
})
.state('home.dashboard', {
url: '/dashboard',
templateUrl: 'app/dashboard/dashboard.html',
controller: 'DashboardController',
data: {
roles: ['admin', 'basic-user']
}
})
.state('home.list-users', {
url: '/list-users',
templateUrl: 'app/users/list/list-users.html',
controller: 'ListUsersController',
data: {
roles: ['admin']
}
})
// More states
In app.js
, run
block
$rootScope.$on('$stateChangeStart', function (event, next, nextParams) {
var userLoggedIn = User.isAuthenticated();
/* If next state needs authentication and user is not logged in
* then redirect to login page.
*/
var authenticationRequired = (next.data && next.data.authenticate);
if (authenticationRequired) {
if (userLoggedIn) {
// Check role of logged in user and allowed roles of state to see if state is protected
var allowedRoles = next.data && next.data.roles;
if (allowedRoles && allowedRoles.length > 0) {
console.log('State access allowed to: ' + allowedRoles);
LoggedInUser.getUser()
.then(function (result) {
var allowed = false;
var user = result;
var role = user.role;
var allowed = (allowedRoles.indexOf(role) >= 0);
console.log(role + ' can access ' + allowed);
if (!allowed) {
var isAdministrator = user.isAdministrator;
if (isAdministrator) {
console.log('User is administrator: ' + isAdministrator);
role = 'administrator';
allowed = (allowedRoles.indexOf(role) >= 0);
console.log(role + ' can access ' + allowed);
}
}
if (!allowed) {
event.preventDefault(); //prevent current page from loading
$state.go('home.dashboard');
AppModalBox.show('danger', 'Not Authorized', 'You are not authorized to view this page.');
}
}, function (error) {
console.log(error);
});
}
} else {
event.preventDefault(); // Prevent current page from loading
$state.go('publicHome.auth.login');
}
} else {
/* This code block handles publicly accessible page like login, forgot password etc.
* Public states (pages) have redirectToLoggedInState data set to either true or false.
* redirectToLoggedInState set to true/false means that public page cannot/can be accessed by the logged in user.
*
* If user is logged in and any public state having redirectToLoggedInState data set to true
* then redirect to dashboard page and prevent opening page.
*/
var redirectToLoggedInState = (next.data && next.data.redirectToLoggedInState);
if (userLoggedIn && redirectToLoggedInState) {
event.preventDefault(); //prevent current page from loading
$state.go('home.dashboard');
}
}
});
Data defined on super state is available on child states so you can group all your private states (which needs authentication) under one state (In the above example, it is
home
) and putdata.authenticate
on super most state.
In order to have this restriction, you can use the same LoggedInUser
service in your controller or use the resolve in the super most state loggedInUser
. Attach roles to $scope
and use ng-if
to show and hide section based on roles.
Resolves defined on the super most state are accessible in all child states.
Here is the code:
angular.module('myApp')
.controller('DashboardController', [
'$scope',
'loggedInUser',
function ($scope,
loggedInUser) {
$scope.roles = loggedInUser.roles;
}]);
You can get access token from localStorage
, which you must have saved upon successful login, and pass it in every request. In back end, parse the request to retrieve access token, get user id for this access token, get roles of the user, and check if user having these roles is allowed to perform operation (like query) or not.
I am using NodeJS - ExpressJS where middlewares are used to handle such role based authentication.
Upvotes: 3
Reputation: 821
please check
http://arthur.gonigberg.com/2013/06/29/angularjs-role-based-auth/
use the resolve to make any custom functions to be checked in the route
$routeProvider.when('/app', { templateUrl: 'app.html', controller: 'AppController' resolve: { //... } });
you can find examples of using resolve here
http://odetocode.com/blogs/scott/archive/2014/05/20/using-resolve-in-angularjs-routes.aspx
Upvotes: 0