Interceptors for managing restricted pages in AngularJS - javascript

I just started a few days ago with AngularJS and I'm having issues with my interceptor that intercepts 401 statuses from server responses.
It broadcasts a message of the type "loginRequired" when a 401 is returned and a redirect is triggered on that event.
The issue is that if I try to access a restricted page while not being logged in, I can see the page flash for a moment before I'm redirected to the login page. I'm still fairly a beginner in asynchronous stuff, promises etc. Could somebody point out what I'm doing wrong?
Here's my interceptor. As you can see it's really simple but I slimmed it down to explain my point and I'm trying to understand things before developing it further.
The interceptor
var services = angular.module('services', []);
services.factory('myInterceptor', ['$q', '$rootScope',
function($q,$rootScope) {
var myInterceptor = {
'responseError': function(rejection) {
$rootScope.$broadcast('event:loginRequired');
return $q.reject(rejection);
}
};
return myInterceptor;
}
]);
The injection of my interceptor
myApp.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('myInterceptor');
}]);
The route for the restricted page
.when('/restrictedPage', {
templateUrl: 'partials/restrictedPage.html',
controller: 'RestrictedPageController'
}).
The restricted page controller
controllers.controller('RestrictedPageController', function($scope) {
//Some times the alert pops up, sometimes not.
alert("Damn it I shouldn't be there");
});
The $rootScope event watcher
$rootScope.$on('event:loginRequired', function() {
//Only redirect if we aren't on free access page
if ($location.path() == "/freeAccess")
return;
//else go to the login page
$location.path('/home').replace();
});
My issue is clearly with the way I handle the interceptor and $q. I found another way of creating the interceptor on github but it's not the way the official documentation uses, so I think it might be the old way and it's not as clean as putting it in a factory in my opinion. He just puts this code after defining the routes in the config function of his module. But this code works and I don't get the page flash.
Another way I found on Github
var interceptor = ['$rootScope', '$q', '$log',
function(scope, $q, $log) {
function success(response) {
return response;
}
function error(response) {
var status = response.status;
if (status == 401) {
var deferred = $q.defer();
var req = {
config: response.config,
deferred: deferred
};
scope.$broadcast('event:loginRequired');
return deferred.promise;
}
// otherwise
return $q.reject(response);
}
return function(promise) {
return promise.then(success, error);
};
}
];
$httpProvider.responseInterceptors.push(interceptor);
But my goal is not just to "make it work" and I hate the mantra "If it's not broken don't fix it". I want to understand what's the issue with my code. Thanks!

Instead of broadcasting 'event:loginRequired' from your interceptor, try performing the location path change within your interceptor. The broadcast would be increasing the delay between receiving the 401 and changing the location and may be the cause of the screen 'flash'.
services.factory('myInterceptor', ['$q', '$rootScope', '$location',
function($q, $rootScope, $location) {
var myInterceptor = {
'responseError': function(rejection) {
if (response.status === 401 && $location.path() !== '/freeAccess') {
//else go to the login page
$location.path('/home').replace();
}
// otherwise
return $q.reject(response);
}
};
return myInterceptor;
}
]);
You could also perform a HTTP request when your app module first runs to determine right away if the user is authorised:
myApp.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('myInterceptor');
}])
.run(function($http) {
//if this returns 401, your interceptor will be triggered
$http.get('some-endpoint-to-determine-auth');
});

Related

AngularJS: Retain Login information when page refresh

I am trying to retain the user information when page refresh. I used cookieStore for this purpose. So my run module in the Angular App looks like this.
.run(['$rootScope', '$cookieStore', '$state', function($rootScope, $cookieStore, $state) {
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, $location){
var requireLogin = toState.data.requireLogin;
if(typeof $rootScope.user == 'undefined'){
$rootScope.user=$cookieStore.get("user");
$rootScope.sessionid=$cookieStore.get("session");
}
if(requireLogin && typeof $rootScope.user === 'undefined'){
event.preventDefault();
$state.go('login', null, {notify: false}).then(function(state) {
$rootScope.$broadcast('$stateChangeSuccess', state, null);
});
}
});
Two main things I wanted to achieve from this is,
Have to get the user and sessioninfo from the browser's local storage, when page refresh.
If user is undefined, then it has to be redirected to the login page. It is for restricting the users to go to intermediate pages without login.
If user is undefined, and data is not available in the local storage, the first if statement gives error and the second if statement doesnot work.
So when the user tries to visit any page the first time without going to login page, it is not redirecting to the login page, because the code failed in first if statement, the second if not working.
How can I achieve both the functionalities together?
Thank you
You can create an authInterceptor factory and push it in interceptors.
This method will always check if user is logged in or not on each page and will throw user on login page if he is not authenticated
For purposes of global error handling, authentication, or any kind of
synchronous or asynchronous pre-processing of request or
postprocessing of responses, it is desirable to be able to intercept
requests before they are handed to the server and responses before
they are handed over to the application code that initiated these
requests. The interceptors leverage the promise APIs to fulfill this need for >both synchronous and asynchronous pre-processing.
Learn more about Interceptors
'use strict';
angular.module('app', [
'ngCookies',
'ngResource',
'ngSanitize',
'ngRoute'
])
.config(function($routeProvider, $locationProvider, $httpProvider) {
$routeProvider
.otherwise({
redirectTo: '/'
});
$httpProvider.interceptors.push('authInterceptor');
})
.factory('authInterceptor', function($rootScope, $q, $cookieStore, $location) {
return {
// Add authorization token to headers
request: function(config) {
config.headers = config.headers || {};
if ($cookieStore.get('token')) {
config.headers.Authorization = 'Bearer ' + $cookieStore.get('token');
}
return config;
},
// Intercept 401s and redirect you to login
responseError: function(response) {
if (response.status === 401) {
$location.path('/login');
// remove any stale tokens
$cookieStore.remove('token');
return $q.reject(response);
} else {
return $q.reject(response);
}
}
};
})
.run(function($rootScope, $location, Auth) {
// Redirect to login if route requires auth and you're not logged in
$rootScope.$on('$routeChangeStart', function(event, next) {
Auth.isLoggedInAsync(function(loggedIn) {
if (next.authenticate && !loggedIn) {
$location.path('/login');
}
});
});
})
.factory('Auth', function Auth($location, $rootScope, $http, User, $cookieStore, $q) {
var currentUser = {};
if ($cookieStore.get('token')) {
currentUser = User.get();
}
return {
/**
* Gets all available info on authenticated user
*
* #return {Object} user
*/
getCurrentUser: function() {
return currentUser;
},
/**
* Check if a user is logged in
*
* #return {Boolean}
*/
isLoggedIn: function() {
return currentUser.hasOwnProperty('role');
},
/**
* Waits for currentUser to resolve before checking if user is logged in
*/
isLoggedInAsync: function(cb) {
if (currentUser.hasOwnProperty('$promise')) {
currentUser.$promise.then(function() {
cb(true);
}).catch(function() {
cb(false);
});
} else if (currentUser.hasOwnProperty('role')) {
cb(true);
} else {
cb(false);
}
}
};
})
.factory('User', function($resource) {
return $resource('/api/users/:id/:controller', {
id: '#_id'
}
});
});
With Above Mechanism, you can use Auth service to get user info in any controller or directives as:
.controller('MainCtrl', function ($scope, Auth) {
$scope.currentUser = Auth.getCurrentUser;
});
in template file:
<div ng-controller="MainCtrl">
<p> Hi {{currentUser().name}}!</p>
</div>
Note: You need to create a proper REST API in order to get correct user data
I would look into using ngStorage. I use the sessionStorage object which will retain the data even on refresh. If you need further implentation example please let me know but the documentation is great.
https://github.com/gsklee/ngStorage

AngularJS: How to implement global error handler and show errors

for my web page I have several angular apps. For those apps I want to create a global error handler which tracks errors with codes 500, 401 and so on and displays them as alerts.
Here is what I have so far:
I've created a global error handler module which I then inject in my apps
angular.module('globalErrorHandlerModule', [])
.factory('myHttpInterceptor', ['$rootScope', '$q', function ($rootScope, $q) {
return {
'responseError': function (rejection) {
if(rejection.status == 500){
// show error
}
return $q.reject(rejection);
}
};
}])
.config(function ($httpProvider) {
$httpProvider.interceptors.push('myHttpInterceptor');
});
angular.module('myApp', ['globalErrorHandlerModule'])
Now what I'm struggling with is actually displaying the error in an alert. What's the best way to do this? I've tried creating a separate error app and injecting the error module and share a data factory in between, but the data never gets updated in the app. Something like this:
angular.module('globalErrorHandlerModule', [])
.factory('myHttpInterceptor', ['$rootScope', '$q', 'Data', function ($rootScope, $q, Data) {
return {
'responseError': function (rejection) {
if(rejection.status == 500){
// set error
Data.error.message = '500 error';
}
return $q.reject(rejection);
}
};
}])
.factory('Data', function () {
var _error = {
message: "init"
};
return {
error: _error
};
})
.config(function ($httpProvider) {
$httpProvider.interceptors.push('myHttpInterceptor');
});
angular.module('globalErrorHandlerApp', ['globalErrorHandlerModule'])
.controller('GlobalErrorCtrl', function ($scope, Data) {
$scope.test = Data.error.message;
});
And then displaying the error as follows:
<div ng-controller="GlobalErrorCtrl">
Error {{test}}
</div>
But as mentioned I only see my initial value, and no updates to the error message. I've also tried broadcasting but that didn't work either. I'm sure there's a better way to implement something like this, I just haven't found it yet. Thanks for any tips pointing me in the right direction.
try with this
angular.module('globalErrorHandlerApp', ['globalErrorHandlerModule'])
.controller('GlobalErrorCtrl', function ($scope, Data) {
$scope.test = Data.error;
});
its a better idea watch an object than a string.
let me know if help you
<div ng-controller="GlobalErrorCtrl">
Error <span> {{test.message}} </span>
</div>

Angular, delay of initial load

I am using a generator here - https://github.com/DaftMonk/generator-angular-fullstack, and I am having a small problem when it initially loads.
There is a second or 2 when it loads when you can see the index.html template without partial css on it, and then it loads the user to the login page. I would like to get rid of this, so when you go to the root, you would go straight to /login. I'm not sure where to begin with this, as it could be many things. I should not I have alot of bower'd in scripts, maybe they are slowing this down?
My first thought was it was the authentication, but maybe it has nothing to do with it. But here is the app js minus the dependancies
.config(function ($routeProvider, $locationProvider, $httpProvider) {
$routeProvider
.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(true);
$httpProvider.interceptors.push('authInterceptor');
})
.factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) {
return {
// Add authorization token to headers
request: function (config) {
config.headers = config.headers || {};
if ($cookieStore.get('token')) {
config.headers.Authorization = 'Bearer ' + $cookieStore.get('token');
}
return config;
},
// Intercept 401s and redirect you to login
responseError: function(response) {
if(response.status === 401) {
$location.path('/login');
// remove any stale tokens
$cookieStore.remove('token');
return $q.reject(response);
}
else {
return $q.reject(response);
}
}
};
})
.run(function ($rootScope, $location, Auth) {
// Redirect to login if route requires auth and you're not logged in
$rootScope.$on('$routeChangeStart', function (event, next) {
Auth.isLoggedInAsync(function(loggedIn) {
if (next.authenticate && !loggedIn) {
$location.path('/login');
}
});
});
});
It could entirely not be this, but I think it might be. I don't care if the page is blank for second, but it's showing the items in the top like the header menu that is in the base index.html with a little css styling on it, but it looks terrible. Any direction in where to look to fix this would be MUCH appreciated, as I have no idea where to even start.
Thanks for reading!

AngularJS: Injecting service into a HTTP interceptor (Circular dependency)

I'm trying to write a HTTP interceptor for my AngularJS app to handle authentication.
This code works, but I'm concerned about manually injecting a service since I thought Angular is supposed to handle this automatically:
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push(function ($location, $injector) {
return {
'request': function (config) {
//injected manually to get around circular dependency problem.
var AuthService = $injector.get('AuthService');
console.log(AuthService);
console.log('in request interceptor');
if (!AuthService.isAuthenticated() && $location.path != '/login') {
console.log('user is not logged in.');
$location.path('/login');
}
return config;
}
};
})
}]);
What I started out doing, but ran into circular dependency problems:
app.config(function ($provide, $httpProvider) {
$provide.factory('HttpInterceptor', function ($q, $location, AuthService) {
return {
'request': function (config) {
console.log('in request interceptor.');
if (!AuthService.isAuthenticated() && $location.path != '/login') {
console.log('user is not logged in.');
$location.path('/login');
}
return config;
}
};
});
$httpProvider.interceptors.push('HttpInterceptor');
});
Another reason why I'm concerned is that the section on $http in the Angular Docs seem to show a way to get dependencies injected the "regular way" into a Http interceptor. See their code snippet under "Interceptors":
// register the interceptor as a service
$provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) {
return {
// optional method
'request': function(config) {
// do something on success
return config || $q.when(config);
},
// optional method
'requestError': function(rejection) {
// do something on error
if (canRecover(rejection)) {
return responseOrNewPromise
}
return $q.reject(rejection);
},
// optional method
'response': function(response) {
// do something on success
return response || $q.when(response);
},
// optional method
'responseError': function(rejection) {
// do something on error
if (canRecover(rejection)) {
return responseOrNewPromise
}
return $q.reject(rejection);
};
}
});
$httpProvider.interceptors.push('myHttpInterceptor');
Where should the above code go?
I guess my question is what's the right way to go about doing this?
Thanks, and I hope my question was clear enough.
This is what I ended up doing
.config(['$httpProvider', function ($httpProvider) {
//enable cors
$httpProvider.defaults.useXDomain = true;
$httpProvider.interceptors.push(['$location', '$injector', '$q', function ($location, $injector, $q) {
return {
'request': function (config) {
//injected manually to get around circular dependency problem.
var AuthService = $injector.get('Auth');
if (!AuthService.isAuthenticated()) {
$location.path('/login');
} else {
//add session_id as a bearer token in header of all outgoing HTTP requests.
var currentUser = AuthService.getCurrentUser();
if (currentUser !== null) {
var sessionId = AuthService.getCurrentUser().sessionId;
if (sessionId) {
config.headers.Authorization = 'Bearer ' + sessionId;
}
}
}
//add headers
return config;
},
'responseError': function (rejection) {
if (rejection.status === 401) {
//injected manually to get around circular dependency problem.
var AuthService = $injector.get('Auth');
//if server returns 401 despite user being authenticated on app side, it means session timed out on server
if (AuthService.isAuthenticated()) {
AuthService.appLogOut();
}
$location.path('/login');
return $q.reject(rejection);
}
}
};
}]);
}]);
Note: The $injector.get calls should be within the methods of the interceptor, if you try to use them elsewhere you will continue to get a circular dependency error in JS.
You have a circular dependency between $http and your AuthService.
What you are doing by using the $injector service is solving the chicken-and-egg problem by delaying the dependency of $http on the AuthService.
I believe that what you did is actually the simplest way of doing it.
You could also do this by:
Registering the interceptor later (doing so in a run() block instead of a config() block might already do the trick). But can you guarantee that $http hasn't been called already?
"Injecting" $http manually into the AuthService when you're registering the interceptor by calling AuthService.setHttp() or something.
...
I think using the $injector directly is an antipattern.
A way to break the circular dependency is to use an event:
Instead of injecting $state, inject $rootScope.
Instead of redirecting directly, do
this.$rootScope.$emit("unauthorized");
plus
angular
.module('foo')
.run(function($rootScope, $state) {
$rootScope.$on('unauthorized', () => {
$state.transitionTo('login');
});
});
Bad logic made such results
Actually there is no point of seeking is user authored or not in Http Interceptor. I would recomend to wrap your all HTTP requests into single .service (or .factory, or into .provider), and use it for ALL requests. On each time you call function, you can check is user logged in or not. If all is ok, allow send request.
In your case, Angular application will send request in any case, you just checking authorization there, and after that JavaScript will send request.
Core of your problem
myHttpInterceptor is called under $httpProvider instance. Your AuthService uses $http, or $resource, and here you have dependency recursion, or circular dependency. If your remove that dependency from AuthService, than you will not see that error.
Also as #Pieter Herroelen pointed, you could place this interceptor in your module module.run, but this will be more like a hack, not a solution.
If your up to do clean and self descriptive code, you must go with some of SOLID principles.
At least Single Responsibility principle will help you a lot in such situations.
If you're just checking for the Auth state (isAuthorized()) I would recommend to put that state in a separate module, say "Auth", which just holds the state and doesn't use $http itself.
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push(function ($location, Auth) {
return {
'request': function (config) {
if (!Auth.isAuthenticated() && $location.path != '/login') {
console.log('user is not logged in.');
$location.path('/login');
}
return config;
}
}
})
}])
Auth Module:
angular
.module('app')
.factory('Auth', Auth)
function Auth() {
var $scope = {}
$scope.sessionId = localStorage.getItem('sessionId')
$scope.authorized = $scope.sessionId !== null
//... other auth relevant data
$scope.isAuthorized = function() {
return $scope.authorized
}
return $scope
}
(i used localStorage to store the sessionId on client side here, but you can also set this inside your AuthService after a $http call for example)

In an Angular, how would I get bootstrap data from the server before launch?

A bit of info. I'm working on a single page app, but am attempting to make it just an HTML file, rather than an actual dynamic page that contains all the bootstrap information in it. I'm also hoping to, when the app boots (or perhaps prior to), check to see if the current session is 'logged in', and if not then direct the hash to the 'login'.
I'm new to Angular, and am having a difficult time figuring out how to program out this flow. So, in essence..
HTML page loaded with 'deferred' bootstrap
Hit URL to get login status
If status is 'not logged in', direct to #/login
Start app
Any pointers on where #2 and #3 would live? In my 'easy world' I'd just use jquery to grab that data, and then call the angular.resumeBootstrap([appname]). But, as I'm trying to actually learn Angular rather than just hack around the parts I don't understand, I'd like to know what would be used in this place. I was looking at providers, but I'm not sure that's what I need.
Thanks!
EDIT
Based on #Mik378's answer, I've updated my code to the following as a test. It works to a point, but as the 'get' is async, it allows the application to continue loading whatever it was before then shooting off the status results..
var app = angular.module('ping', [
'ngRoute',
'ping.controllers'
]).provider('security', function() {
this.$get = ['$http', function($http) {
var service = {
getLoginStatus: function () {
if (service.isAuthenticated())
return $q.when(service.currentUser);
else
return $http.get('/login/status').then(function (response) {
console.log(response);
service.loggedIn = response.data.loggedIn;
console.log(service);
return service.currentUser;
});
},
isAuthenticated: function () {
return !!service.loggedIn;
}
};
return service;
}];
}).run(['security', function(security) {
return security.getLoginStatus().then(function () {
if(!security.isAuthenticated()) {
console.log("BADNESS");
} else {
console.log("GOODNESS");
}
});
}]);
My hope was that this could somehow be completed prior to the first controller booting up so that it wasn't loading (or attempting to load) things that weren't even cleared for access yet.
EDIT #2
I started looking into the 'resolve' property in the router, and #Mik378 verified what I was looking at. My final code that is (currently) working how I want it is as follows (appologies about the super long code block)
angular.module('ping.controllers', [])
.controller('Dashboard', ['$scope', function($scope) {
console.log('dashboard')
}])
.controller('Login', ['$scope', function($scope) {
console.log('login')
}]);
var app = angular.module('ping', [
'ngRoute',
'ping.controllers'
]).run(['$rootScope', '$location', function($root, $location) {
$root.$on("$routeChangeError", function (event, current, previous, rejection) {
switch(rejection) {
case "not logged in":
$location.path("/login"); //<-- NOTE #1
break;
}
});
}]);
app.provider('loginSecurity', function() {
this.$get = ['$http', '$q', function($http, $q) {
var service = {
defer: $q.defer, //<-- NOTE #2
requireAuth: function() { //<-- NOTE #3
var deferred = service.defer();
service.getLoginStatus().then(function() {
if (!service.isAuthenticated()) {
deferred.reject("not logged in")
} else {
deferred.resolve("Auth OK")
}
});
return deferred.promise;
},
getLoginStatus: function() {
if (service.isAuthenticated()) {
return $q.when(service.currentUser);
} else {
return $http.get('/login/status').then(function(response) {
console.log(response);
service.loggedIn = response.data.loggedIn;
console.log(service);
return service.currentUser;
});
}
},
isAuthenticated: function() {
return !!service.loggedIn;
}
};
return service;
}
];
});
app.config(['$routeProvider', function($routeProvider) {
console.log('Routing loading');
$routeProvider.when('/', {
templateUrl: 'static/scripts/dashboard/template.html',
controller: 'Dashboard',
resolve: {'loginSecurity': function (loginSecurity) {
return loginSecurity.requireAuth(); //<- NOTE #4
}}
});
$routeProvider.when('/login', {
templateUrl: 'static/scripts/login/template.html',
controller: 'Login'
});
$routeProvider.otherwise({redirectTo: '/404'});
}]);
Notes:
This section hooks into routing failures. In the case of a "no login", I wanted to catch the failure and pop the person over to the login page.
I can't get access to the $q inside of the requireAuth function, so I grabbed a reference to it. Perhaps a better way of doing this exists?
This function wraps up the other two - it uses the promise returned from getLoginStatus, but returns its own promise that will be rejected if the end result from the getLoginStatus winds up with the user not being logged in. Sort of a round-about way of doing it.
This returns #3's promise, which is used by the $routeProvider.. so if it fails, the routing fails and you end up catching it at #1.
Whew. I think that's enough for a day. Time for a beer.
No need to use a deferred bootstrap for your case:
angular.module('app').run(['security', '$location', function(security) {
// Get the current user when the application starts
// (in case they are still logged in from a previous session)
security.requestCurrentUser().then(function(){
if(!security.isAuthenticated())
$location.path('yourPathToLoginPage')
}; //service returning the current user, if already logged in
}]);
this method requestCurrentUser would be the following:
requestCurrentUser: function () {
if (service.isAuthenticated())
return $q.when(service.currentUser);
else
return $http.get('/api/current-user').then(function (response) {
service.currentUser = response.data.user;
return service.currentUser;
});
}
and inside security service again:
isAuthenticated: function () {
return !!service.currentUser;
}
Note the run method of the module => As soon as the application runs, this service is called.
-- UPDATE --
To prevent any controller to be initialized before the promise provided by requestCurrentUser is resolved, a better solution, as evoked in the comments below, is to use the resolve route property .

Categories