In my SPA I catch every 401 response from REST requests. From there I don't redirect to login page immediatly, but first I check to the backend if the problem is that the token has expired or not. If not (the user is not known) I redirect to login, but if it was an expired problem, I generate a new token then I run again the request that previously failed into a 401. Here is the code for my interceptor:
var $http, loginService;
return function (promise) {
return promise.then(function (response) {
return response;
}, function (response) {
if (response.status === 401) {
$http = $http || $injector.get('$http');
loginService = loginService || $injector.get('loginService');
var defer = $q.defer();
var promiseToken = defer.promise;
var configPreviousRequest = response.config;
console.log(configPreviousRequest);
var url = configurationService.serverUrl + "mobile" + configurationService.apiVersion + "/verify";
var request = $http.post(url, {'code': loginService.getVmmToken()});
// Get the token. If success, we try to login again
return request.then(
function (responseVerify) {
loginService.setVmmsToken(responseVerify.data);
loginService.setAuthentificationToken();
configPreviousRequest.headers.vmmsToken = responseVerify.data;
return $http(configPreviousRequest);
},
function () {
$location.path('/login');
});
}
return $q.reject(response);
});
};
But here is the result in Chrome Network tool. All methods are not in the correct number (called too many times for /verify and /blocks)
So I logged (with console.log(configPreviousRequest);) to see what happens. Here are logs:
We clearly observe that for one 401 error, I intercept it many times.
And I have no clue why :)
Has someone any idea?
Thanks
Related
I am using the javascript MEAN stack for my single page app.
I have an Angular factory making a call to my Api.
app.factory('authorizing', function($resource){
return $resource(
'/api/authorizing/:username',
{'username':'#username'},
// retreiving only one user and all their roles
{'singleUser' : {
method:'GET'
,isArray: false
}}
);
});
I call the factory in my controller like this. My goal is to update the web page data based on the response I get back from the Api. I expect a true or false value to be returned. I have tried other things but I want to keep my authorization on the server side.
app.controller('usrPageController', function ($scope, usrServiceById, $route, authorizing, $rootScope) {
$scope.updateUser = function (data,field){
var vCheckUserRoles;
vCheckUserRoles = authorizing.singleUser({'username': $rootScope.current_user});
if (vCheckUserRoles == true){
usrServiceById.update({'username':username},{field:data, 'fieldName':field});
};
};
});
The database returns the result data using a res.send.
.get(function (req, res) {
RoleUser.find({ 'aclUserName': req.params.username }, function (err, aclUser) {
if (err) { return res.sendStatus(500) };
// return an error if no user is found
if (aclUser == null) { return res.sendStatus(401) };
//check for proper role in list of users
for (var i = 0; i < aclUser.length; i++) {
if (aclUser[i].aclUserResource == req.params.username + '_PROFILE') {
//return a successful status
return res.send('true');
};
};
//return a failed status
return res.send('false');
});
});
I don't get any errors from the code but the return object is empty when it hits the controller after the res.send. I have tried different types of data to return but nothing seems to work for me. Any help is appreciated. I have othe res.send calls in my Api. They work but I take the data directly from my database wiht the callback and feed it to the res.send. This is the only time in my code that I am trying to return something besides the successful callback variable.
UPDATED CODE:
I removed the var vCheckUserRoles. Now the value is passed to a success callback
app.controller('usrPageController', function ($scope, usrServiceById, $route, authorizing, $rootScope) {
authorizing.singleUser({'username': $rootScope.current_user},function(response){
console.log('response = ' + response.data)
if (response.data == 'true'){
usrServiceById.update({'username':usrSingle.username},{field:data, 'fieldName':field});
};
});
});
You can use res.json instead of res.send to send status of your query.
Example ////
res.json({status:true})
And on client side you can access that status value in data.status field.
On the server you send data as follows:
res.status(200).send({message: "Hello!"});
In the front-end you receive the data, then resolve it to get data as follows:
fetch(url)
.then(response => {
if(response.ok) {
return response.json();
}
}).then(data => {
if(data) {
console.log(data);
}
}).catch(err => console.error(err));
I have login tied to Facebook authentication, and this is all handled by Firebase.
However I need to make an API call to Facebook 'me/friends/'
Since I am already logged in, how would I use OAuth object to make a call without making another request.
I am using following wrapper for Angular for connection to Facebook.
https://github.com/ccoenraets/OpenFB
You don't need a wrapper. $firebaseAuth() + $http() = easy Graph API requests.
The Graph API is pretty easy to use and will work easily with Firebase.
Make sure you have the Facebook Friends permission enabled or you won't get any data back.
You can use $firebaseAuth() to login and get the Facebook access_token. That token can be used against the Graph API to get data via HTTP requests. Angular has a good $http library for making these calls.
Don't mind the way I structure the code, I prefer to use the Angular styleguide.
angular.module('app', ['firebase'])
.constant('FirebaseUrl', 'https://<my-firebase-app>.firebaseio.com/')
.constant('FacebookAppId', '<app-id>')
.service('RootRef', ['FirebaseUrl', Firebase])
.factory('Auth', Auth)
.factory('Friends', Friends)
.controller('MainCtrl', MainCtrl);
function Friends($http, RootRef, $q, FacebookAppId) {
function getFriends() {
// get the currently logged in user (may be null)
var user = RootRef.getAuth();
var deferred = $q.defer();
var token = null;
var endpoint = "https://graph.facebook.com/me/friends?access_token="
// if there is no logged in user, call reject
// with the error and return the promise
if (!user) {
deferred.reject('error');
return deferred.promise;
} else {
// There is a user, get the token
token = user.facebook.accessToken;
// append the token onto the endpoint
endpoint = endpoint + token;
}
// Make the http call
$http.get(endpoint)
.then(function(data) {
deferred.resolve(data);
})
.catch(function(error) {
deferred.reject(error);
});
// return the promise
return deferred.promise;
}
return {
get: getFriends
};
}
Friends.$inject = ['$http', 'RootRef', '$q', 'FacebookAppId'];
function Auth($firebaseAuth, RootRef) {
return $firebaseAuth(RootRef);
}
Auth.$inject = ['FirebaseAuth', 'RootRef'];
function MainCtrl($scope, Friends) {
$scope.login = function login() {
Auth.$authWithOAuthPopup('facebook').then(function(authData) {
console.log(authData, 'logged in!');
});
};
$scope.getFriends = function getFriends() {
Friends.get()
.then(function(result) {
console.log(result.data);
});
};
}
MainCtrl.$inject = ['$scope', 'Friends'];
I copied the following code from the web to work with JWT authorisation, but it does not work. In particular the $location.path command has no impact - the redirect does not take place. I also tried with $state.go, but that led to bigger errors. I don't fully understand what $q is referring to here, not what is waiting for the promise to unwind, but the issue is $location.path not taking the user back to the login screen (nor are the proposals below with respect to $state changes).
$httpProvider.interceptors.push(function($q, $location, $localStorage) {
return {
'request': function (config) {
config.headers = config.headers || {};
if ($localStorage.token) {
config.headers.Authorization = 'Bearer ' + $localStorage.token;
}
return config;
},
'responseError': function(response) {
if(response.status === 401 || response.status === 403) {
console.log("app.js: httpInterceptor caught authorisation status response");
delete $localStorage.token;
$location.path('/'); // to login page
// $state.go('login');
}
return $q.reject(response);
}
};
});
}
To test, I send a message that creates an server error, catch that in devtools on the way back, and then manually set response.status = 403. The redirect is clearly parsed but does not lead to the redirect. I can see that the login screen is put back on the screen, but is then immediately overwritten by a different view.
This is the factory $http ajax request. Is it possible that the deferred $q I use here is interfering with that in the interceptor?
$http(httpObj)
.success(function (response) { // returns 0: mongo resto data, 1: wpserver report
console.log("%s succesful, response: %s", method, response);
if (!updateNotAdd) {
Restos.data.restos.push(response[0]); // add to local copy of data
} else {
// replace existing entry with new information
var idxToReplace = _.findIndex(Restos.data.restos, function(r) {
return r.qname === resto.qname;
});
// copy over all data from editor model to database
_.assign(Restos.data.restos[idxToReplace], resto);
}
var response = {
success: true,
id: response[0]._id,
message: response[1]
};
$rootScope.$broadcast("ajaxresponse", response);
deferred.resolve(response);
})
.error(function (msg) {
// console.log(msg);
var response = {
success: false,
message: msg
};
$rootScope.$broadcast("ajaxresponse", response);
deferred.resolve(response);
});
return deferred.promise;
You shouldn't have an issue leveraging ui-router here. You need to use the $injector service to get a reference on the $state service. Observe the following change...
$httpProvider.interceptors.push(function($q, $location, $localStorage, $injector) {
return {
'request': function (config) {
config.headers = config.headers || {};
if ($localStorage.token) {
config.headers.Authorization = 'Bearer ' + $localStorage.token;
}
return config;
},
'responseError': function(response) {
if(response.status === 401 || response.status === 403) {
console.log("app.js: httpInterceptor caught authorisation status response");
delete $localStorage.token;
$injector.get('$state').go('login');
}
return $q.reject(response);
}
};
});
}
The bigger issues you're experiencing are caused by a circular dependency due to ui-router injecting $http into $TemplateFactory - leading to a circular reference to $http inside $httpProvider when you attempt to inject $state (which doesn't yet appear to be in your interceptors injection signature anyways)
Not sure whether you want to use, but probably this would redirect:
$window.location.href = url;
Currently I'm trying to cache a user's information. I give you guys a scenario of what is happening.
a user is trying to log in with an account A. Account A's name appears on the navbar. After that user log out and tries to log in with an account B, on the navbar itself the name is still belongs to account A's name.
The code
service.js
.factory('Auth', function($http, $q, AuthToken) {
// create auth factory object
var authFactory = {};
// log a user in
authFactory.login = function(username, password) {
// return the promise object and its data
return $http.post('/api/login', {
username: username,
password: password
})
.success(function(data) {
AuthToken.setToken(data.token);
return data;
});
};
// log a user out by clearing the token
authFactory.logout = function() {
// clear the token
AuthToken.setToken();
};
// check if a user is logged in
// checks if there is a local token
authFactory.isLoggedIn = function() {
if (AuthToken.getToken())
return true;
else
return false;
};
// get the logged in user
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me', {cache: true});
else
return $q.reject({ message: 'User has no token.' });
};
// return auth factory object
return authFactory;
})
// ===================================================
// factory for handling tokens
// inject $window to store token client-side
// ===================================================
.factory('AuthToken', function($window) {
var authTokenFactory = {};
// get the token out of local storage
authTokenFactory.getToken = function() {
return $window.localStorage.getItem('token');
};
// function to set token or clear token
// if a token is passed, set the token
// if there is no token, clear it from local storage
authTokenFactory.setToken = function(token) {
if (token)
$window.localStorage.setItem('token', token);
else
$window.localStorage.removeItem('token');
};
return authTokenFactory;
})
// ===================================================
// application configuration to integrate token into requests
// ===================================================
.factory('AuthInterceptor', function($q, $location, AuthToken) {
var interceptorFactory = {};
// this will happen on all HTTP requests
interceptorFactory.request = function(config) {
// grab the token
var token = AuthToken.getToken();
// if the token exists, add it to the header as x-access-token
if (token)
config.headers['x-access-token'] = token;
return config;
};
// happens on response errors
interceptorFactory.responseError = function(response) {
// if our server returns a 403 forbidden response
if (response.status == 403)
$location.path('/login');
// return the errors from the server as a promise
return $q.reject(response);
};
return interceptorFactory;
});
controller.js
angular.module('mainCtrl', [])
.controller('MainController', function($rootScope, $location, Auth) {
var vm = this;
// get info if a person is logged in
vm.loggedIn = Auth.isLoggedIn();
// check to see if a user is logged in on every request
$rootScope.$on('$routeChangeStart', function() {
vm.loggedIn = Auth.isLoggedIn();
// get user information on page load
Auth.getUser()
.then(function(data) {
vm.user = data.data;
});
});
// function to handle login form
vm.doLogin = function() {
vm.processing = true;
// clear the error
vm.error = '';
Auth.login(vm.loginData.username, vm.loginData.password)
.success(function(data) {
vm.processing = false;
// get user information on page load
Auth.getUser()
.then(function(data) {
vm.user = data.data;
});
// if a user successfully logs in, redirect to users page
if (data.success)
$location.path('/');
else
vm.error = data.message;
});
};
// function to handle logging out
vm.doLogout = function() {
Auth.logout();
$location.path('/logout');
};
});
index.html
<ul class="nav navbar-nav navbar-right">
<li ng-if="!main.loggedIn">Login</li>
<li ng-if="main.loggedIn">Hello {{ main.user.username }}</li>
<li ng-if="main.loggedIn">Logout</li>
<li><button class="btn btn-primary">Write</button></li>
</ul>
So basically my assumption of the problem lies in the service.js where i added cache: true. Do i need to add some logic to it?
There are two different caches that may be involved in your app. First it's the angular cache which you have set below {cache: true}
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me', {cache: true});
else
return $q.reject({ message: 'User has no token.' });
};
This cache is only there for the duration of the app, once you leave or reload the page, it's gone!
The other cache which is the browser cache is a little more complex to deal with. Note this has no relationship with the Angular cache, so if this is your problem simply turning off {cache: false} wont help. To prevent cache you will need to send a list of different caching headers in your restful API and it may not always work.
The easiest way to prevent cache is to add a version to your url which doesnt actually affect your results but tricks your browser into thinking that it's a different url. This is referred to as Cache Busting.
The easiest way to cache bust is to add a Math.Random() to append to the url. The chances of Math.Random to be the same is probably in the billions.
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me?rand=' + Math.random(), {cache: true});
else
return $q.reject({ message: 'User has no token.' });
};
However, if you want a better way to do it specific for your app, you could append the username to your url. This way it will cache for the same users which means you are actually taking advantage of the caching mechanism and not getting tied down by it!
authFactory.getUser = function() {
if (AuthToken.getToken())
return $http.get('/api/me?user=' + <username>, {cache: true});
else
return $q.reject({ message: 'User has no token.' });
};
I'm setting up authorization with AngularJS and angular ui router.
If a user tries to access a route which requires authorization, the server returns a 401 status code.
I have created an HTTP response interceptor which checks for the 401 status code.
If the code is 401, it is supposed to redirect to the log in page, but this does not work.
It catches the 401, but does not redirect. Here is the code for the interceptor:
app.config(['$httpProvider', function ($httpProvider) {
// This is just for to check for XHR requests on the server
$httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
$httpProvider.responseInterceptors.push(['$q', '$location', function ($q, $location) {
return function (promise) {
return promise.then(
// Success: just return the response
function (response) {
return response;
},
// Error: check the error status to get only the 401
function (response) {
if (response.status === 401)
$location.url('/users/login');
return $q.reject(response);
}
);
}
}]);
}]);
EDIT: It seems to be working if I do it like this:
$timeout(function() { $location.url('/users/login'); }, 0);
I guess that puts it last in the execution stack and changes the execution context. Does anyone know more about this, or if this indeed does work or only seems so?
I add the same issue. So I changed my code to use the $location.path (not url)
$location.path("/users/login");
Can you try that
angular.module('yourApp').config(function ($httpProvider) {
var logsOutUserOn401 = ['$q', '$location', function ($q, $location) {
var success = function (response) {
return response;
};
var error = function (response) {
if (response.status === 401) {
//redirect them back to login page
$location.path('/login');
return $q.reject(response);
}
else {
return $q.reject(response);
}
};
return function (promise) {
return promise.then(success, error);
};
}];
$httpProvider.responseInterceptors.push(logsOutUserOn401);
});
(source : http://arthur.gonigberg.com/2013/06/29/angularjs-role-based-auth/)
I have it working with
$window.location.href='...'
I finally solved it, but I still don't know why it didn't work. Anyway, I had to use this to redirect: (state transition)
$injector.get('$state').transitionTo('users.login');
this is how we added a response interceptor in our project. its working for us.
$httpProvider.interceptors.push(function($q, $rootScope, $location,
$cookieStore) {
return {
'responseError' : function(
rejection) {
var status = rejection.status;
var config = rejection.config;
var method = config.method;
var url = config.url;
if (status == 401) {
$rootScope.shouldRedirect = true;
$rootScope.urlAttempted = url;
$location
.path("/login");
} else if (status == 403) {
$location
.path("/accessForbidden");
} else {
}
return $q.reject(rejection);
}
};
});