I am trying to intercept the 401
and 403
errors to refresh the user token, but I can't get it working well. All I have achieved is this interceptor:
app.config(function ($httpProvider) {
$httpProvider.interceptors.push(function ($q, $injector) {
return {
// On request success
request: function (config) {
var deferred = $q.defer();
if ((config.url.indexOf('API URL') !== -1)) {
// If any API resource call, get the token firstly
$injector.get('AuthenticationFactory').getToken().then(function (token) {
config.headers.Authorization = token;
deferred.resolve(config);
});
} else {
deferred.resolve(config);
}
return deferred.promise;
},
response: function (response) {
// Return the promise response.
return response || $q.when(response);
},
responseError: function (response) {
// Access token invalid or expired
if (response.status == 403 || response.status == 401) {
var $http = $injector.get('$http');
var deferred = $q.defer();
// Refresh token!
$injector.get('AuthenticationFactory').getToken().then(function (token) {
response.config.headers.Authorization = token;
$http(response.config).then(deferred.resolve, deferred.reject);
});
return deferred.promise;
}
return $q.reject(response);
}
}
});
});
The issue is that the responseError
does an infinite loop of 'refreshes' because by Authorization header with the updated token, that is not being received by $http(response.config)
call.
1.- App has an invalid token stored.
2.- App needs to do an API call
2.1 Interceptor catch the `request`.
2.2 Get the (invalid) stored token and set the Authorization header.
2.3 Interceptor does the API call with the (invalid) token setted.
3.- API respond that used token is invalid or expired (403 or 401 statuses)
3.1 Interceptor catch the `responseError`
3.2 Refresh the expired token, get a new VALID token and set it in the Authorization header.
3.3 Retry the point (2) with the valid refreshed token `$http(response.config)`
The loop is happening in point (3.3) because the Authorization header NEVER has the new refreshed valid token, it has the expired token instead. I don't know why because it supposed to be setted in the responseError
AuthenticationFactory
app.factory('AuthenticationFactory', function($rootScope, $q, $http, $location, $log, URI, SessionService) {
var deferred = $q.defer();
var cacheSession = function(tokens) {
SessionService.clear();
// Then, we set the tokens
$log.debug('Setting tokens...');
SessionService.set('authenticated', true);
SessionService.set('access_token', tokens.access_token);
SessionService.set('token_type', tokens.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', tokens.expires_in);
SessionService.set('refresh_token', tokens.refresh_token);
SessionService.set('user_id', tokens.user_id);
return true;
};
var uncacheSession = function() {
$log.debug('Logging out. Clearing all');
SessionService.clear();
};
return {
login: function(credentials) {
var login = $http.post(URI+'/login', credentials).then(function(response) {
cacheSession(response.data);
}, function(response) {
return response;
});
return login;
},
logout: function() {
uncacheSession();
},
isLoggedIn: function() {
if(SessionService.get('authenticated')) {
return true;
}
else {
return false;
}
},
isExpired: function() {
var unix = Math.round(+new Date()/1000);
if (unix < SessionService.get('expires')) {
// not expired
return false;
}
// If not authenticated or expired
return true;
},
refreshToken: function() {
var request_params = {
grant_type: "refresh_token",
refresh_token: SessionService.get('refresh_token')
};
return $http({
method: 'POST',
url: URI+'/refresh',
data: request_params
});
},
getToken: function() {
if( ! this.isExpired()) {
deferred.resolve(SessionService.get('access_token'));
} else {
this.refreshToken().then(function(response) {
$log.debug('Token refreshed!');
if(angular.isUndefined(response.data) || angular.isUndefined(response.data.access_token))
{
$log.debug('Error while trying to refresh token!');
uncacheSession();
}
else {
SessionService.set('access_token', response.data.access_token);
SessionService.set('token_type', response.data.token_type);
SessionService.set('expires', tokens.expires);
SessionService.set('expires_in', response.data.expires_in);
deferred.resolve(response.data.access_token);
}
}, function() {
// Error
$log.debug('Error while trying to refresh token!');
uncacheSession();
});
}
return deferred.promise;
}
};
});
PLUNKER
I made a plunker & backend to try to reproduce this issue.
$http
again results in 403 or 401?. And, btw, there a huge information gap for us inAuthenticationFactory
- even, conceptually, it's not clear happens between refresh and token cases. Does a call to refresh renews the token, such that next time.getToken
returns the new token? – Paphosresponse.config.headers.Authorization = token;
. Yes, the refresh renews and set the token for the following.getToken
calls – Aculeate$http(response.config)
or the one that refreshes the token? – Paphos$http(response.config)
– Aculeateconsole.log
call inrefreshToken().then
to confirm that a new token is being provided? – Chasse$http.defaults.headers.common.Authorization
in case settingresponse.config.headers.Authorization
isn't providing the necessary change to subsequent calls. – Chasse$http.defaults.headers.common.Authorization
will set the header for every $http call, not only API call. It's pretty insecure... Man-in-the-middle for example – AculeateAuthenticationFactory
code. – TrollyAuthenticationFactory
code. I omitted it for brevity – AculeateAuthorization
header. Just confirmed with Postman. Your Angular code appears to work as it should, but the server isn't. So long as the server gives that response the loop will continue. You might want to build in a counter to prevent an infinite loop. – Chasse