I have developed an app which uses succesfully ADAL JS library for authentication with azure. However I also need to implement authorization, and I meant that I need to restrict views to specific groups.
I already have a REST API which given a user id or email can return me the groups he belongs to.
However I am not sure how to plug an angular service to cosume that REST API and plug that into the routes configuration.
app.js
(function () {
angular.module('inspinia', [
'ui.router', // Routing
'oc.lazyLoad', // ocLazyLoad
'ui.bootstrap', // Ui Bootstrap
'pascalprecht.translate', // Angular Translate
'ngIdle', // Idle timer
'AdalAngular', // ADAL JS Angular
'ngRoute' // Routing
])
})();
config.js
function config($stateProvider, $urlRouterProvider, $ocLazyLoadProvider, IdleProvider, KeepaliveProvider,adalAuthenticationServiceProvider, $httpProvider) {
// Configure Idle settings
IdleProvider.idle(5); // in seconds
IdleProvider.timeout(120); // in seconds
$urlRouterProvider.otherwise("/dashboards/dashboard_1");
$ocLazyLoadProvider.config({
// Set to true if you want to see what and when is dynamically loaded
debug: false
});
$stateProvider
.state('dashboards', {
abstract: true,
url: "/dashboards",
templateUrl: "views/common/content.html",
})
.state('dashboards.dashboard_1', {
url: "/dashboard_1",
templateUrl: "views/dashboard_1.html",
requireADLogin: true,
resolve: {
loadPlugin: function ($ocLazyLoad) {
return $ocLazyLoad.load([
{
serie: true,
name: 'angular-flot',
files: [ 'js/plugins/flot/jquery.flot.js', 'js/plugins/flot/jquery.flot.time.js', 'js/plugins/flot/jquery.flot.tooltip.min.js', 'js/plugins/flot/jquery.flot.spline.js', 'js/plugins/flot/jquery.flot.resize.js', 'js/plugins/flot/jquery.flot.pie.js', 'js/plugins/flot/curvedLines.js', 'js/plugins/flot/angular-flot.js', ]
},
{
name: 'angles',
files: ['js/plugins/chartJs/angles.js', 'js/plugins/chartJs/Chart.min.js']
},
{
name: 'angular-peity',
files: ['js/plugins/peity/jquery.peity.min.js', 'js/plugins/peity/angular-peity.js']
}
]);
}
}
})
Ideally it would be awesome i could add a new property to each state called Groups with values:
Something like:
url: "/dashboard_1",
templateUrl: "views/dashboard_1.html",
requireADLogin: true,
groups: "Admin, Accounting, Marketing"
and then the custom service under the hood will validate this.
I'm not familiar specifically with ADAL.js, but assuming you can say to the server, "does this user have any of these roles" in a http request, then you could intercept the $stateChangeStart, prevent the state change by calling event.preventDefault(), ask the server if the current user is in any of roles specified for in the toState as having access and then take action either way - send to an access denied page, or continue on to the toState.
The following is a flawed implementation. The working version is locked away at work, and I'm at home right now, but hopefully it will give you some ideas.
(function (app) {
"use strict";
app.run(run);
function run($rootScope, $log, $state, $q, authService) {
/**
* The canceller is passed to the authService.isTokenValid()
*
* The canceller is a promise, that, when resolved, cancels the current
* token validation request
*/
let _canceller;
/**
* When state is changed, ensure that the current user has access
* #param event
* #param toState
*/
function onStateChangeStart(event, toState, toParams) {
// When token is valid, continue with navigation to the state
function onValidToken() {
if (toState.name === 'login'){
$state.go('home');
} else {
$state.go(toState, toParams, {notify: false}).then((state) => {
$rootScope.$broadcast('$stateChangeSuccess', state, null);
});
}
}
// When the token is not valid, set state to login
function onInvalidToken() {
if (toState.name === 'login'){
$state.go(toState, toParams, {notify: false}).then((state) => {
$rootScope.$broadcast('$stateChangeSuccess', state, null);
});
} else {
$log.warn(`Access denied to state ${toState.name}`);
$state.go('login');
}
}
// On completion of token validation, resolve the canceller
function onFinally() {
if (_canceller) {
_canceller.resolve();
}
}
// If the state requiresLogin
if (toState.requiresLogin || toState.name === 'login') {
// stop navigation
event.preventDefault();
// Cancel any current requests
if (_canceller) {
_canceller.resolve();
}
// create a new promise
_canceller = $q.defer();
// validate
authService.isTokenValid(_canceller)
.then(onValidToken, onInvalidToken)
.finally(onFinally);
}
}
$rootScope.$on('$stateChangeStart', onStateChangeStart);
}
}(angular.module('app.features')));
Related
I'm not sure if this is a duplicate or not, but I didn't manage to find anything that worked for me, so I'm posting this question.
I have a situation where I need to get values from database before directing user to certain routes, so I could decide what content to show.
If I move e.preventDefault() right before $state.go(..) then it works, but not properly. Problem is that it starts to load default state and when it gets a response from http, only then it redirects to main.home. So let's say, if the db request takes like 2 seconds, then it takes 2 seconds before it redirects to main.home, which means that user sees the content it is not supposed to for approximately 2 seconds.
Is there a way to prevent default at the beginning of state change and redirect user at the end of state change?
Also, if we could prevent default at the beginning of state change, then how could we continue to default state?
(function(){
"use strict";
angular.module('app.routes').run(['$rootScope', '$state', '$http', function($rootScope, $state, $http){
/* State change start */
$rootScope.$on('$stateChangeStart', function(e, to, toParams, from, fromParams){
e.preventDefault();
$http
.get('/url')
.error(function(err){
console.log(err);
})
.then(function(response){
if( response.data === 2 ){
// e.preventDefault()
$state.go('main.home');
}
// direct to default state
})
}
}]);
});
You could add a resolve section to your $stateProviderConfig.
Inside the resolve you can make a request to the databse and check required conditions. If case you don't want user to acces this page you can use $state.go() to redirect him elsewhere.
Sample config:
.state({
name: 'main.home',
template: 'index.html',
resolve: {
accessGranted: ['$http', '$state', '$q',
function($http, $state, $q) {
let deffered = $q.defer();
$http({
method: 'GET',
url: '/url'
}).then(function(data) {
if (data === 2) {
// ok to pass the user
deffered.resolve(true);
} else {
//no access, redirect
$state.go('main.unauthorized');
}
}, function(data) {
console.log(data);
//connection error, redirect
$state.go('main.unauthorized');
});
return deffered.promise;
}
]
}
});
Documentation of the resolve is available here
Note that you could use Promise object instead of $q service in case you don't need to support IE
One way to handle this situation is adding an interceptor as follows.
.config(function ($httpProvider) {
$httpProvider.interceptors.push('stateChangeInterceptor');
}).factory('stateChangeInterceptor', function ($q, $window,$rootScope) {
return {
'response': function(response) {
var isValid = true;//Write your logic here to validate the user/action.
/*
* Here you need to allow all the template urls and ajax urls which doesn't
*/
if(isValid){
return response;
}
else{
$rootScope.$broadcast("notValid",{statusCode : 'INVALID'});
}
},
'responseError': function(rejection) {
return $q.reject(rejection);
}
}
})
Then handle the message 'notValid' as follows
.run(function($state,$rootScope){
$rootScope.$on("notValid",function(event,message){
$state.transitionTo('whereever');
});
})
I have a angular webapp that is using a pre-produced theme/framework called fuse (http://withinpixels.com/themes/fuse), this theme already has an app structure and code written to make the creation of apps easier.
We added some pages (which include sidemenu items), the problem is, when you tap one of the links in the sidebar, the whole page seems to be reloaded or at least a animate-slide-up is played 2 times on the index main div, I traced down one part of the problem to the configuration module of the page:
$stateProvider.state('app.pages_dashboard', {
url : '/dashboard',
views : {
'main#' : {
templateUrl: 'app/core/layouts/vertical-navigation.html',
controller : 'MainController as vm'
},
'content#app.pages_dashboard': {
templateUrl: 'app/main/dashboard/dashboard.html',
controller : 'DashboardController as vm'
},
'navigation#app.pages_dashboard': {
templateUrl: 'app/navigation/layouts/vertical-navigation/navigation.html',
controller : 'NavigationController as vm'
},
'toolbar#app.pages_dashboard': {
templateUrl: 'app/toolbar/layouts/vertical-navigation/toolbar.html',
controller : 'ToolbarController as vm'
},
},
bodyClass: 'login',
needAuth: true,
onStateChangeStart: function(event, state, auth, api) {
console.log('onStateChangeStart on DASHBOARD');
api.getUserCard.save({}, {}, function (response){
if (!response.result) {
state.go('app.pages_claimcard');
}
});
}
});
and the configuration module of the app
angular
.module('fuse')
.run(runBlock);
/** #ngInject */
function runBlock($rootScope, $timeout, $state, $auth, api)
{
console.log('INDEX.RUN loaded');
// Activate loading indicator
var stateChangeStartEvent = $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams)
{
// console.log('started change event');
// console.log(toState);
// console.log(fromState);
// check if authentication needed
if (toState.needAuth) {
// redirect to login page
if (!$auth.isAuthenticated()) {
event.preventDefault();
$state.go('app.pages_auth_login');
}
}
if (toState.onStateChangeStart) {
// THIS CAUSES ONE OF THE RELOADS
// toState.onStateChangeStart(event, $state, $auth, api);
}
$rootScope.loadingProgress = true;
});
// De-activate loading indicator
var stateChangeSuccessEvent = $rootScope.$on('$stateChangeSuccess', function ()
{
$timeout(function ()
{
$rootScope.loadingProgress = false;
});
});
// Store state in the root scope for easy access
$rootScope.state = $state;
// Cleanup
$rootScope.$on('$destroy', function ()
{
stateChangeStartEvent();
stateChangeSuccessEvent();
});
}
as you can see I commented the the toState OnStateChangeStart function, and that got rid of one the 'reloads' of the application, so basically have 2 questions:
Why does the onStateChangeStart function on the toState state causes the page to reload?
I have no idea what might be causing the other page reload, any ideas?
I found the problem, my states were defined as:
'app.pages.dashboard'
however there was never a declaration for
'app.pages'
so UI-router was trying its best to sort this mess, anyways, always remember to properly declare your states and everything should be fine.
I have the following question... or situation. I have states defined in my AngularJS app, like so...
$stateProvider
.state('myApp', {
abstract: true,
template: '<ui-view/>'
})
.state('myApp.stateOne', {
url: 'state1',
templateUrl: '/an/views/state-1.html',
controller: 'StateOneCtrl'
})
.state('myApp.stateTwo', {
url: 'state2',
templateUrl: '/an/views/state-2.html'
controller: 'StateTwoCtrl'
})
.state('myApp.stateThree', {
url: 'state3',
templateUrl: '/an/views/state-3.html'
controller: 'StateThreeCtrl'
})
There are more states and I have changed the naming for this example, but suppose I need to check if the user is allowed to see / load 'mayApp.stateThree'. I can determine this by asking the backend. I have a service (in this example called IsAllowedService) to deal with this requests / provide the access and normally I would write the logic to do the check in the .run() block in my app.js file for example:
.run(['IsAllowedService', '$state', function (IsAllowedService, $state) {
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState) {
// check if we are going to sfm.addContacts and if we are allowed to...
if (toState.name === 'myApp.stateThree') {
IsAllowedService.checkIfIsAllowed().then(function (resp) {
if(resp.allowed === false) {
$state.go('myApp.stateOne');
}
});
}
});
}]);
This works well but doesn't wait until we get the result from the service so 'mayApp.stateThree' is loaded then we a redirected if necessary. So we get a quick flash of the page before we are redirected. I could put the same code into the 'StateThreeCtrl' but I still get the flash / FOUC. Would it be possible to resolve this when defining the states, I know this won't work but something like this...
.state('myApp.stateThree', {
url: '/an/state3',
templateUrl: '/an/views/state-3.html'
controller: 'StateThreeCtrl',
resolve: {
isAllowed : function () {
IsAllowedService.checkIfIsAllowed().then(function (resp) {
return resp;
})
}
}
I realise that I wouldn't be able to inject the service (or even the $http service) but is it possible for me to somehow pause the loading of the view / controller of 'mayApp.stateThree' until I get the result from IsAllowedService.checkIfIsAllowed(). Any advice on how to structure my app / code would be appreciated. I have used ng-cloak in my HTML view but this did nothing!
Actually you're doing it almost right in the application's run block. Except you are not preventing anything. You can achieve that by adding:
event.preventDefault(); //Prevent from going to the page
Furthermore, you can add custom data to your $states , which will allow you to verify those conditions with your criteria. e.g.:
$stateProvider.state('home', {
controller: 'HomeController as home',
url: '/home',
templateUrl: 'home.html',
data: { roles: [ROLES.ANONYMOUS] }}); //This can be any condition
$stateProvider.state('user', {
controller: 'UserController as user',
url: '/user',
templateUrl: 'user.html',
data: { roles: [ROLES.ADMIN, ROLES.USER] }});
You can retrieve this custom data in the $stateChangeStart event:
$rootScope.$on('$stateChangeStart', function (event, next) {
if (!yourService.isAuthorized(next.data.roles)) {
event.preventDefault(); //Prevent from going to the page -> no flickering
$state.go('403'); //Or whatever is desired.
}
});
You see the flickering because you're using a Promise and the first page only gets redirected when the promise is furfilled. You can stop the flickering by preventing the default action, authorize and continue your flow as you desire when the promise resolves.
if (toState.name === 'myApp.stateThree') {
event.preventDefault(); //preventing the request.
IsAllowedService.checkIfIsAllowed().then(function (resp) {
if(resp.allowed === false) {
$state.go('myApp.stateOne');
} else { //he actually is allowed to go to state three.
$state.go('myApp.stateThree');
}
}, function() { //in case the server has no answer
$state.go('myApp.stateOne'); //you probably want to prevent it too
} );
In my opinion, if these conditions do not change during runtime, i.e. user role based, you can retrieve them upon user verification so you don't need a promise to begin with. Hope this helps.
I made a similar post before and added a working plunker.
I'm using ui-router in my angular application. Currently I've two routes /signin & /user.
Initially it shows /signin when the user clicks on the login button, I'm sending a ajax request and getting the user id. I'm storing the user id in localstorage and changing the state to /user.
Now, what I want, if a user is not loggedin, and user changes the addressbar to /user, it'll not change the view, instead it'll change the addressbar url to /signin again.
I'm try to use resolve, but it's not working. My code is:-
module.exports = function($stateProvider, $injector) {
$stateProvider
.state('signin', {
url: '/signin',
template: require('../templates/signin.html'),
controller: 'LoginController'
})
.state('user', {
url: '/user/:id',
template: require('../templates/user.html'),
resolve:{
checkLogin: function(){
var $state = $injector.get('$state');
console.log("in resolve");
if (! window.localStorage.getItem('user-id')) {
console.log("in if")
$state.go('signin');
}
}
},
controller: 'UserController'
})
}
Please help me to solve this problem.
I don't think it's allowed to change states in the middle of a state transition.
So, the way to address it is to have the checkLogin resolve parameter (I changed it below to userId) to be a function that either returns a value or a promise (in this case, a rejected promise, if you can't get the user-id).
You'd then need to handle this in $rootScope.$on('$stateChangeError') and check the error code.
resolve: {
userId: function ($q, $window) {
var userId = $window.localStorage.getItem('user-id');
if (!userId) {
return $q.reject("signin")
}
return userId;
}
}
And redirect in the $stateChangeError handler:
$rootScope.$on('$stateChangeError', function (event, toState, toParams, fromState, fromParams, error) {
if (error === "signin") {
$state.go("signin");
}
});
If someone has this problem, you can solve it, using timeout service. It will put state switching call at the end of queue.
Also, you should use promises. Rejecting it will prevent initialization of that state:
resolve:{
checkLogin: function(){
var deferred = $q.defer();
var $state = $injector.get('$state');
if (!window.localStorage.getItem('user-id')) {
$timeout(function(){$state.go('signin');});
deferred.reject();
} else {
deferred.resolve();
}
return deferred.promise;
}
},
So let's say I have the following user property and I want to restrict access to a page. This is in my Firebase noSQL database but I think this could pertain to obtaining data from anywhere.
{
"users": {
"simplelogin:1": {
"properties": { "admin_user": true }
}
}
}
So in my javascript I have the following:
var user_properties = new Firebase("https://<MY-URL>.com/users/"+auth.uid+"/properties");
user_properties.once("value", function(properties) {
if(properties.val().admin_user == false)
window.location.replace("/");
});
So, on the page load of the "admin page", I load this javascript. And if they aren't an admin, the page is supposed to redirect.
However, I'm having the problem where the admin page will load for a second while it gathers the data and then redirect.
Does anyone have any suggestions on how I can make the page redirect before the page even loads?
Security rules in Firebase can prevent the page from data from being viewed without permission. Then you simply need a client-side solution to redirect the page. The simple answer here is to use resolve in your routes.
You can find a complete implementation of this approach in the angularFire-seed project. Here's the relevant code:
"use strict";
angular.module('myApp.routes', ['ngRoute', 'simpleLogin'])
.constant('ROUTES', {
'/home': {
templateUrl: 'partials/home.html',
controller: 'HomeCtrl',
resolve: {
// forces the page to wait for this promise to resolve before controller is loaded
// the controller can then inject `user` as a dependency. This could also be done
// in the controller, but this makes things cleaner (controller doesn't need to worry
// about auth status or timing of displaying its UI components)
user: ['simpleLogin', function(simpleLogin) {
return simpleLogin.getUser();
}]
}
},
'/chat': {
templateUrl: 'partials/chat.html',
controller: 'ChatCtrl'
},
'/login': {
templateUrl: 'partials/login.html',
controller: 'LoginCtrl'
},
'/account': {
templateUrl: 'partials/account.html',
controller: 'AccountCtrl',
// require user to be logged in to view this route
// the whenAuthenticated method below will resolve the current user
// before this controller loads and redirect if necessary
authRequired: true
}
})
/**
* Adds a special `whenAuthenticated` method onto $routeProvider. This special method,
* when called, invokes the requireUser() service (see simpleLogin.js).
*
* The promise either resolves to the authenticated user object and makes it available to
* dependency injection (see AuthCtrl), or rejects the promise if user is not logged in,
* forcing a redirect to the /login page
*/
.config(['$routeProvider', function($routeProvider) {
// credits for this idea: https://groups.google.com/forum/#!msg/angular/dPr9BpIZID0/MgWVluo_Tg8J
// unfortunately, a decorator cannot be use here because they are not applied until after
// the .config calls resolve, so they can't be used during route configuration, so we have
// to hack it directly onto the $routeProvider object
$routeProvider.whenAuthenticated = function(path, route) {
route.resolve = route.resolve || {};
route.resolve.user = ['requireUser', function(requireUser) {
return requireUser();
}];
$routeProvider.when(path, route);
}
}])
// configure views; the authRequired parameter is used for specifying pages
// which should only be available while logged in
.config(['$routeProvider', 'ROUTES', function($routeProvider, ROUTES) {
angular.forEach(ROUTES, function(route, path) {
if( route.authRequired ) {
// adds a {resolve: user: {...}} promise which is rejected if
// the user is not authenticated or fulfills with the user object
// on success (the user object is then available to dependency injection)
$routeProvider.whenAuthenticated(path, route);
}
else {
// all other routes are added normally
$routeProvider.when(path, route);
}
});
// routes which are not in our map are redirected to /home
$routeProvider.otherwise({redirectTo: '/home'});
}])
/**
* Apply some route security. Any route's resolve method can reject the promise with
* { authRequired: true } to force a redirect. This method enforces that and also watches
* for changes in auth status which might require us to navigate away from a path
* that we can no longer view.
*/
.run(['$rootScope', '$location', 'simpleLogin', 'ROUTES', 'loginRedirectPath',
function($rootScope, $location, simpleLogin, ROUTES, loginRedirectPath) {
// watch for login status changes and redirect if appropriate
simpleLogin.watch(check, $rootScope);
// some of our routes may reject resolve promises with the special {authRequired: true} error
// this redirects to the login page whenever that is encountered
$rootScope.$on("$routeChangeError", function(e, next, prev, err) {
if( angular.isObject(err) && err.authRequired ) {
$location.path(loginRedirectPath);
}
});
function check(user) {
if( !user && authRequired($location.path()) ) {
$location.path(loginRedirectPath);
}
}
function authRequired(path) {
return ROUTES.hasOwnProperty(path) && ROUTES[path].authRequired;
}
}
]);