I'm currently using the following code to rethrow a request that returns a 401 from my API:
responseError: function(rejection) {
var authData = localStorageService.get('authorizationData');
if (rejection.status === 401 && authData) {
var authService = $injector.get('authService');
var $http = $injector.get('$http');
̶v̶a̶r̶ ̶d̶e̶f̶e̶r̶r̶e̶d̶ ̶=̶ ̶$̶q̶.̶d̶e̶f̶e̶r̶(̶)̶;̶
var promise = authService.refreshToken();
return ̶d̶e̶f̶e̶r̶r̶e̶d̶.̶ promise.then(function () {
return $http(rejection.config);
});
}
return $q.reject(rejection);
}
This works great for 1 request, but it doesn't seem to work if I get two 401s back from a single page, such as when the page is loading with two api calls to populate different sections. How can I get my interceptor to rethrow multiple deferred calls?
Also, shouldn't the interceptor fire for each 401 individually? Not ideal, it would cause multiple refresh calls on a single page, but an improvement from missing data due to a call not being rethrown.
Screenshot:
One approach is to save the token promise and chain the second and subsequent retries until the token refresh is complete:
responseError: function(rejection) {
var authService = $injector.get('authService');
var $http = $injector.get('$http');
var tokenPromise = null;
var authData = localStorageService.get('authorizationData');
if (rejection.status === 401 && authData) {
if (!tokenPromise) {
tokenPromise = authService.refreshToken()
.finally(function() {
tokenPromise = null;
});
};
return tokenPromise.then(function () {
return $http(rejection.config);
});
} else {
throw rejection;
}
}
In the above example the rejection handler create a token refresh promise and subsequently nulls it when the token refresh settles (either fulfilled or rejected). If another rejection occurs while the token refresh is in progress, the retry in chained (and delayed) until the token refresh XHR completes.
quite similar georgeawg answer...
responseError: function(rejection) {
var authData = localStorageService.get('authorizationData');
if (rejection.status === 401 && authData && !isAuthRequest() /* If request for refresh token fails itself do not go into loop, i.e. check by url */) {
var authService = $injector.get('authService');
var $http = $injector.get('$http');
var promise = authService.refreshTokenExt(); // look below
return ̶promise.then(function () {
return $http(rejection.config);
});
}
return $q.reject(rejection);
}
AuthService:
...
var refreshAuthPromise;
service.refreshTokenExt = function() {
if (refreshAuthPromise == null) {
refreshAuthPromise = authService.refreshToken().catch(function() {
// Cant refresh - redirect to login, show error or whatever
}).finally(function() {
refreshAuthPromise = null;
});
}
return refreshAuthPromise;
}
Related
I'm facing issue redirecting user to login page if refresh (jwt) token gets unauthorized (after first token expires). There are 2 scenarios of un-authorization of tokens;
1st: When jwt token gets expire based on 401 response, than a new refresh service is called for generating new token via $http-interceptors (config).
2nd: When refresh token also gets unauthorized (401) response, this is when a user should redirect to login page.
I'm able to send refresh token on 1st scenario and its working fine as expected, but I'm not able to redirect user to login page if refresh token also get unauthorized (401) response.
Here is my code;
authInterceptor.service.js
angular.module('someApp').factory('AuthorizationTokenService', AuthorizationTokenService);
AuthorizationTokenService.$inject = ['$q', '$injector', '$cookies'];
function AuthorizationTokenService($q, $injector, $cookies) {
// Local storage for token
var tokenVM = {
accessToken: null
};
// Subscribed listeners which will get notified when new Access Token is available
var subscribers = [];
// Promise for getting new Access Token from backend
var deferedRefreshAccessToken = null;
var service = {
getLocalAccessToken: getLocalAccessToken,
refreshAccessToken: refreshAccessToken,
isAccessTokenExpired: isAccessTokenExpired,
subscribe: subscribe
};
return service;
////////////////////////////////////
// Get the new Access Token from backend
function refreshAccessToken() {
// If already waiting for the Promise, return it.
if( deferedRefreshAccessToken ) {
return deferedRefreshAccessToken.promise
} else {
deferedRefreshAccessToken = $q.defer();
// Get $http service with $injector to avoid circular dependency
var http = $injector.get('$http');
http({
method: 'POST',
url: 'api_url',
params: {
grant_type: 'refresh',
id_token: $cookies.get('access_token')
}
})
.then(function mySucces(response) {
var data = response.data;
if( data ){
// Save new Access Token
$cookies.put('access_token', data.access_token);
if( $cookies.get('access_token') ) {
// Resolve Promise
deferedRefreshAccessToken.resolve(data.access_token);
// Notify all subscribers
notifySubscribersNewAccessToken(data.access_token);
deferedRefreshAccessToken = null;
}
}
}, function myError(error) {
deferedRefreshAccessToken.reject(error);
deferedRefreshAccessToken = null;
});
return deferedRefreshAccessToken.promise;
}
}
function getLocalAccessToken() {
// get accesstoken from storage - $cookies
if ( $cookies.get('access_token') ) {
var access_token = $cookies.get('access_token')
return access_token;
}
}
function isAccessTokenExpired() {
// Check if expiresAt is older then current Date
}
function saveToken(accessToken) {
// get accesstoken from storage - $cookies
var access_token = $cookies.put('access_token');
console.log('access_token ' + access_token);
return access_token;
}
// This function will call all listeners (callbacks) and notify them that new access token is available
// This is used to notify the web socket that new access token is available
function notifySubscribersNewAccessToken(accessToken) {
angular.forEach(subscribers, function(subscriber) {
subscriber(accessToken);
});
}
// Subscribe to this service. Be notifyed when access token is renewed
function subscribe(callback) {
subscribers.push(callback);
}
}
And in Config (app.js)
config.$inject = ['$stateProvider', '$urlRouterProvider', '$httpProvider'];
function config($stateProvider, $urlRouterProvider, $httpProvider) {
// Push httpRequestInterceptor
// $httpProvider.interceptors.push('httpRequestInterceptor');
//Intercept all http requests
$httpProvider.interceptors.push(['$injector', '$q', "AuthorizationTokenService", "$cookies", function ($injector, $q, AuthorizationTokenService, $cookies) {
var cachedRequest = null;
return {
request: function (config) {
//If request if for API attach Authorization header with Access Token
if (config.url.indexOf("api") != -1) {
// var accessToken = AuthorizationTokenService.getLocalAccessToken();
console.log('cookie ' + $cookies.get('access_token'));
config.headers.Authorization = 'Bearer ' + $cookies.get('access_token');
}
return config;
},
responseError: function (response) {
switch (response.status) {
// Detect if reponse error is 401 (Unauthorized)
case 401:
// Cache this request
var deferred = $q.defer();
if(!cachedRequest) {
// Cache request for renewing Access Token and wait for Promise
cachedRequest = AuthorizationTokenService.refreshAccessToken();
}
// When Promise is resolved, new Access Token is returend
cachedRequest.then(function(accessToken) {
cachedRequest = null;
if (accessToken) {
// Resend this request when Access Token is renewed
$injector.get("$http")(response.config).then(function(resp) {
// Resolve this request (successfully this time)
deferred.resolve(resp);
},function(resp) {
deferred.reject();
console.log('success: refresh token has expired');
});
} else {
// If any error occurs reject the Promise
console.log('error: refresh token has expired');
deferred.reject();
}
}, function(response) {
// If any error occurs reject the Promise
cachedRequest = null;
deferred.reject();
return;
});
return deferred.promise;
}
// If any error occurs reject the Promise
return $q.reject(response);
}
};
}]);
}
Both in service & config, I've tried to implement that redirects user based on dual 401 (mean refresh token also gets expired and respond back with 401).
I also tried with multiple descendant 401 condition but that didn't work as well. (example below)
responseError: function (response) {
// Detect if reponse error is 401 (Unauthorized)
if (response.status === 401) {
// Cache this request
var deferred = $q.defer();
if(!cachedRequest) {
// Cache request for renewing Access Token and wait for Promise
cachedRequest = AuthorizationTokenService.refreshAccessToken();
}
// When Promise is resolved, new Access Token is returend
cachedRequest.then(function(accessToken) {
cachedRequest = null;
if (response.status === 401) {
console.log('refresh token also expired');
$location.path('/login');
} else {
// Resend this request when Access Token is renewed
$injector.get("$http")(response.config).then(function(resp) {
// Resolve this request (successfully this time)
deferred.resolve(resp);
},function(resp) {
deferred.reject();
console.log('success: refresh token has expired');
});
}
}, function(response) {
// If any error occurs reject the Promise
cachedRequest = null;
deferred.reject();
return;
});
return deferred.promise;
}
}
Based on above code, please guide me what I'm doing wrong or maybe there might be something wrong with login/implementation. Either case please do help me. Thanks
I managed to get this solved simply with below lines of code;
Config (app.js)
// Cache this request
var deferred = $q.defer();
if(!cachedRequest) {
// Cache request for renewing Access Token and wait for Promise
cachedRequest = AuthorizationTokenService.refreshAccessToken();
} else {
// this is where it checks for request token expiry
do_logout();
}
I'm facing issue on refreshing expired JWT token based on 401 (unauthorized) header response. What i want is when user get 401 (header) response, than a new (refresh) JWT should generated by calling specific service (api).
I'm sending XSRF-TOKEN & access_token (JWT) in header response and these are working fine. I even also can get refresh (expired) token by calling api manually. But can't get it worked with 401 (header) response.
I've a factory that take care of this promise and intercepts header requests. My (factory) code looks like this.
angular.module('myApp').factory('httpRequestInterceptor', httpRequestInterceptor);
function httpRequestInterceptor($cookies, $rootScope, $q, $location, $injector) {
var replays = [];
var refreshTokenPromise;
var factory = {
request: request,
responseError: responseError
};
return factory;
//////////
function requestTodoWhenDone() {
var token = store.get('token');
return $http({
method: 'POST',
url: ApiEndpoint.url,
params: {
grant_type: 'refresh',
id_token: $cookies.get('access_token')
}
})
.success(function(response) {
// Set the refreshed token.
$cookies.put('access_token', response.data.access_token);
})
.then(function(){
// Attempt to retry the request if request config is passed.
if( !angular.isUndefined(requestTodoWhenDone) && requestTodoWhenDone.length > 0 ) {
// Set the new token for the authorization header.
requestTodoWhenDone.headers = {
'Authorization': 'Bearer ' + $cookies.get('access_token')
};
// Run the request again.
return $http(requestTodoWhenDone);
}
});
}
//////////
// Add authorization token to headers
function request(config) {
config.headers = config.headers || {};
if ($cookies.get('access_token')) {
config.headers.Authorization = 'Bearer ' + $cookies.get('access_token');
}
return config;
}
// Intercept 401s and redirect you to login
function responseError(response, requestTodoWhenDone) {
if (response.status === 401 && $cookies.get('access_token')) {
return checkAuthorization(response);
}
return $q.reject(response);
/////////
function checkAuthorization(res) {
return $q(function(resolve, reject) {
var replay = {
success: function(){
$injector.get('$http')(res.config).then(resolve, reject);
},
cancel: function(){
reject(res);
}
};
replays.push(replay);
console.log(replays);
if (!refreshTokenPromise) {
refreshTokenPromise = $injector.get('requestTodoWhenDone') // REFRESH TOKEN HERE
.refreshToken()
.then(clearRefreshTokenPromise)
.then(replayRequests)
.catch(cancelRequestsAndRedirect);
}
});
////////////
function clearRefreshTokenPromise(auth) {
refreshTokenPromise = null;
return auth;
}
function replayRequests(auth) {
replays.forEach(function(replay) {
replay.success();
});
replays.length = 0;
return auth;
}
function cancelRequestsAndRedirect() {
refreshTokenPromise = null;
replays.forEach(function(replay) {
replay.cancel();
});
replays.length = 0;
$cookies.remove('token');
var $state = $injector.get('$state');
// SET YOUR LOGIN PAGE
$location.path('/login');
}
}
}
}
Based on above code I'm getting following error in console when token expires (401 response).
Console Error
Error: "[$injector:unpr] Unknown provider: requestTodoWhenDoneProvider <- requestTodoWhenDone
Any help on this would be highly appreciable.
Thanks.
Ok i ended up with different way that solves the issue. But i still can't be able to redirect user to login page when my token inactive time is also expires (this happens after jwt expires).
Here is the code.
authInterceptor.service.js
angular.module('someApp').factory('AuthorizationTokenService', AuthorizationTokenService);
AuthorizationTokenService.$inject = ['$q', '$injector', '$cookies'];
function AuthorizationTokenService($q, $injector, $cookies) {
// Local storage for token
var tokenVM = {
accessToken: null
};
// Subscribed listeners which will get notified when new Access Token is available
var subscribers = [];
// Promise for getting new Access Token from backend
var deferedRefreshAccessToken = null;
var service = {
getLocalAccessToken: getLocalAccessToken,
refreshAccessToken: refreshAccessToken,
isAccessTokenExpired: isAccessTokenExpired,
subscribe: subscribe
};
return service;
////////////////////////////////////
// Get the new Access Token from backend
function refreshAccessToken() {
// If already waiting for the Promise, return it.
if( deferedRefreshAccessToken ) {
return deferedRefreshAccessToken.promise
} else {
deferedRefreshAccessToken = $q.defer();
// Get $http service with $injector to avoid circular dependency
var http = $injector.get('$http');
http({
method: 'POST',
url: 'api_url',
params: {
grant_type: 'refresh',
id_token: $cookies.get('access_token')
}
})
.then(function mySucces(response) {
var data = response.data;
if( data ){
// Save new Access Token
$cookies.put('access_token', data.access_token);
if( $cookies.get('access_token') ) {
// Resolve Promise
deferedRefreshAccessToken.resolve(data.access_token);
// Notify all subscribers
notifySubscribersNewAccessToken(data.access_token);
deferedRefreshAccessToken = null;
}
}
}, function myError(error) {
deferedRefreshAccessToken.reject(error);
deferedRefreshAccessToken = null;
});
return deferedRefreshAccessToken.promise;
}
}
function getLocalAccessToken() {
// get accesstoken from storage - $cookies
if ( $cookies.get('access_token') ) {
var access_token = $cookies.get('access_token')
return access_token;
}
}
function isAccessTokenExpired() {
// Check if expiresAt is older then current Date
}
function saveToken(accessToken) {
// get accesstoken from storage - $cookies
var access_token = $cookies.put('access_token');
console.log('access_token ' + access_token);
return access_token;
}
// This function will call all listeners (callbacks) and notify them that new access token is available
// This is used to notify the web socket that new access token is available
function notifySubscribersNewAccessToken(accessToken) {
angular.forEach(subscribers, function(subscriber) {
subscriber(accessToken);
});
}
// Subscribe to this service. Be notifyed when access token is renewed
function subscribe(callback) {
subscribers.push(callback);
}
}
Than in config (app.js) I've following code which intercepts appropriate header(s) and refresh (request) api on 401 response.
Here is the config code
config.$inject = ['$stateProvider', '$urlRouterProvider', '$httpProvider'];
function config($stateProvider, $urlRouterProvider, $httpProvider) {
// Push httpRequestInterceptor
// $httpProvider.interceptors.push('httpRequestInterceptor');
//Intercept all http requests
$httpProvider.interceptors.push(['$injector', '$q', "AuthorizationTokenService", "$cookies", function ($injector, $q, AuthorizationTokenService, $cookies) {
var cachedRequest = null;
return {
request: function (config) {
//If request if for API attach Authorization header with Access Token
if (config.url.indexOf("api") != -1) {
// var accessToken = AuthorizationTokenService.getLocalAccessToken();
console.log('cookie ' + $cookies.get('access_token'));
config.headers.Authorization = 'Bearer ' + $cookies.get('access_token');
}
return config;
},
responseError: function (response) {
switch (response.status) {
// Detect if reponse error is 401 (Unauthorized)
case 401:
// Cache this request
var deferred = $q.defer();
if(!cachedRequest) {
// Cache request for renewing Access Token and wait for Promise
cachedRequest = AuthorizationTokenService.refreshAccessToken();
}
// When Promise is resolved, new Access Token is returend
cachedRequest.then(function(accessToken) {
cachedRequest = null;
if (accessToken) {
// Resend this request when Access Token is renewed
$injector.get("$http")(response.config).then(function(resp) {
// Resolve this request (successfully this time)
deferred.resolve(resp);
},function(resp) {
deferred.reject();
console.log('success: refresh token has expired');
});
} else {
// If any error occurs reject the Promise
console.log('error: refresh token has expired');
deferred.reject();
}
}, function(response) {
// If any error occurs reject the Promise
cachedRequest = null;
deferred.reject();
return;
});
return deferred.promise;
}
// If any error occurs reject the Promise
return $q.reject(response);
}
};
}]);
}
The code is working fine on 401 (response) case which happens when JWT expires. But its not redirecting me to login page (In this case I've added console in promise request in config instead of redirection code)
Please help on this, thanks...
We have an application that requires users to be logged in. Once logged in the user interacts with web api. Our issue is when the authentication expires we want to show a Login page, for the user to log back in without being redirected. This way they will not lose there work. Our issue is as follows:
Inside our Controller an request is made
$http.get("/api/Document/GetDocumentsManage").success(function (response) {
$scope.Documents = response;
});
On the server we identify that the user is no longer authenticated and we reject the call. We then use an interceptor to catch the error and handle it to show a popup modal.
$httpProvider.interceptors.push(function ($q, $injector) {
return {
'responseError': function (rejection) {
var response = rejection;
var defer = $q.defer();
if (rejection.status == 401 || rejection.status == 400) {
var modal = $injector.get("$mdDialog");
modal.show({
parent: angular.element("body"),
targetEvent: window.event,
templateUrl: "/directives/LoginPage/Login.html",
controller: "loginController",
onRemoving: function () {
var $http = $injector.get("$http");
$http(rejection.config);
}
});
}
}
};
});
With this code we can successfully re-authenticate without the user navigating away from the page, and then once authenticated again we can execute the original request. Our issue is the resubmitted request is not bound to the original .success callback of our request. Therefore in this example $scope.Documents does not get set to the response. Is there anyway we can rerun any request that failed and continue execution?
You are definitely on the right track! You just need a few minor changes to ensure that the result makes in back to your controller:
$httpProvider.interceptors.push(function ($q, $injector) {
return {
'responseError': function (rejection) {
var response = rejection;
var defer = $q.defer();
var $http = $injector.get("$http"); //moved this for readability
var modal = $injector.get("$mdDialog"); //moved this for readability
if (rejection.status == 401 || rejection.status == 400) {
modal.show({
parent: angular.element("body"),
targetEvent: window.event,
templateUrl: "/directives/LoginPage/Login.html",
controller: "loginController",
onRemoving: function () {
// resolve the deferred
deferred.resolve();
}
});
// return the promise object
return deferred.promise.then(function() {
// return the new request promise
return $http(rejection.config);
});
}
return $q.reject(rejection);
}
};
});
Take a look at the is blog example where they are doing the same thing you are trying to do: http://www.webdeveasy.com/interceptors-in-angularjs-and-useful-examples/#sessionrecovererresponseerrorinterceptor
I want to make a angularJS interceptor that when the client is offline returns instead of an error a cached response as if it wasn't any error.
What I've done so far was to make an interceptor that caches the api requests:
app.factory('httpCachingInterceptor', function ($q, apiCache) {
return {
'response' : function(response) {
// cache the api
if (response.config.url.indexOf('api/') > 0)
apiCache.addApiCache(response.config.url, response.data);
return response;
},
'responseError' : function(rejection) {
if (rejection.status == -1) {
// get the data from apiCache
//
apiCache.getApiCache(rejection.config.url, function(data) {
// build a new promise and return it as a valid response
})
}
return $q.reject(rejection);
}
}
})
I've noticed that when offline the rejection.status is -1 so that's when I check if a request was made while offline.
My question is how I build the response?
Should I make a new promise or can I update the rejection?
To convert a rejected promise to a fulfilled promise return a value in the onRejection handler.
Conversely, to convert a fulfilled promise to a rejected promise throw a value in the onFulfilled handler.
For more information, see Angular execution order with $q
I managed to make it work like this:
'responseError' : function(rejection) {
if (rejection.status == -1) {
// get the data from apiCache
var deferred = $q.defer();
apiCache.getApiCache(rejection.config.url, function(err, data) {
if (err || !data) {
return deferred.reject(rejection);
}
rejection.data = data;
rejection.status = 200;
deferred.resolve(rejection);
console.log("Resolve promise with ", rejection);
})
return deferred.promise;
}
return $q.reject(rejection);
}
So i made a new promise and modified the rejection I got with the new data from the cache.
In my application, I have a service called 'pendingRequests' that keeps track of my http requests. I configured my $httpProvider to use this service.
The purpose of this service is to give me the ability to cancel ALL pending http requests occurring in ANY controller.
Here is the code:
app.service('pendingRequests', function($rootScope, $q) {
var pending = [];
this.get = function() {
return pending;
};
this.add = function(request) {
pending.push(request);
//console.log("Pending Requests(before):" + pending);
};
this.remove = function(request) {
angular.forEach(pending, function(p , key) {
if(p.url == request.url) pending.splice(key, 1);
});
// console.log("Pending Requests(after):" + pending);
};
this.cancelAll = function() {
if(typeof pending !='undefined'){
angular.forEach(pending, function(p) {
p.canceller.resolve();
});
pending.length = 0;
}
};
});
app.config(function($httpProvider){
$httpProvider.interceptors.push(function($q, pendingRequests){
return {
'request': function (config){
var canceller = $q.defer();
pendingRequests.add({
url: config.url,
canceller: canceller
});
config.timeout = canceller.promise;
return config || $q.when(config);
},
'response': function (response){
pendingRequests.remove(response.config);
//pendingRequests remove request
return response;
}
}
});
});
The service is canceling the requests as intended. However, the next request submitted is delayed as if it is still waiting for another request to complete.
What is causing this delay?