Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJs - doesn't skip request when waiting for new token

I have implemented authentication system and after upgrading from angular 1.0.8 to 1.2.x, system doesn't work as it used to. When user logs in it gets a token. When token is expired, a refresh function for new token is called. New token is successfully created on a server and it is stored to database. But client doesn't get this new token, so it requests a new token again, and again and again until it logs out. Server side (MVC Web Api) is working fine, so problem must be on client side. The problem must be on a retry queue. Below I pasted relevant code and a console trace for both versions of applications (1.0.8 and 1.2.x). I am struggling with this for days now and I can't figure it out.

In the link below, there are 5 relevant code blocks:

  • interceptor.js (for intercepting requests, both versions)
  • retryQueue.js (manages queue of retry requests)
  • security.js (manages handler for retry queue item and gets a new token from api)
  • httpHeaders.js (sets headers)
  • tokenHandler.js (handles tokens in a cookies)

Code: http://pastebin.com/Jy2mzLgj

Console traces for app in angular 1.0.8: http://pastebin.com/aL0VkwdN

and angular 1.2.x: http://pastebin.com/WFEuC6WB

interceptor.js (angular 1.2.x version)

angular.module('security.interceptor', ['security.retryQueue'])
.factory('securityInterceptor', ['$injector', 'securityRetryQueue', '$q',
     function ($injector, queue, $q) {
        return {
            response: function(originalResponse) {
                return originalResponse;
            },
            responseError: function (originalResponse) {
                var exception;
                if (originalResponse.headers){
                    exception = originalResponse.headers('x-eva-api-exception');
                }
                if (originalResponse.status === 401 && 
                   (exception === 'token_not_found' || 
                    exception === 'token_expired')){
                    queue.pushRetryFn(exception, function retryRequest() {
                        return $injector.get('$http')(originalResponse.config);
                    });
                }
                return $q.reject(originalResponse);
            }
        };
     }])
     .config(['$httpProvider', function($httpProvider) {
         $httpProvider.interceptors.push('securityInterceptor');
     }]);

retryQueue.js

angular.module('security.retryQueue', [])
.factory('securityRetryQueue', ['$q', '$log', function($q, $log) {
    var retryQueue = [];
var service = {
        onItemAddedCallbacks: [],
        hasMore: function(){
            return retryQueue.length > 0;
        },
        push: function(retryItem){
            retryQueue.push(retryItem);
            angular.forEach(service.onItemAddedCallbacks, function(cb) {
                try {
                    cb(retryItem);
                } 
                catch(e){
                     $log.error('callback threw an error' + e);
                }
            });
        },
        pushRetryFn: function(reason, retryFn){
            if ( arguments.length === 1) {
                retryFn = reason;
                reason = undefined;
            }
            var deferred = $q.defer();
            var retryItem = {
                reason: reason,
                retry: function() {
                    $q.when(retryFn()).then(function(value) {
                        deferred.resolve(value);
                    }, function(value){
                        deferred.reject(value);
                    });
                },
                cancel: function() {
                    deferred.reject();
                }
            };
            service.push(retryItem);
            return deferred.promise;
        },
        retryAll: function() {
            while(service.hasMore()) {
                retryQueue.shift().retry();
            }
        }
    };
    return service;
}]);

security.js

angular.module('security.service', [
'session.service',
'security.signin',
'security.retryQueue',
'security.tokens',
'ngCookies'
])
.factory('security', ['$location', 'securityRetryQueue', '$q', /* etc. */ function(){
     var skipRequests = false;      
     queue.onItemAddedCallbacks.push(function(retryItem) {
         if (queue.hasMore()) {
             if(skipRequests) {return;}
             skipRequests = true;
             if(retryItem.reason === 'token_expired') {
                 service.refreshToken().then(function(result) {
                     if(result) { queue.retryAll(); }
                     else {service.signout(); }
                     skipRequests = false;
                 });
             } else {
                 skipRequests = false;
                 service.signout();
             }
         }
     });

     var service = {
         showSignin: function() {
             queue.cancelAll();
             redirect('/signin');
         },
         signout: function() {
             if(service.isAuthenticated()){
                 service.currentUser = null;
                 TokenHandler.clear();
                 $cookieStore.remove('current-user');
                 service.showSignin();
             }
         },
         refreshToken: function() {
             var d = $q.defer();
             var token = TokenHandler.getRefreshToken();
             if(!token) { d.resolve(false); }
             var session = new Session({ refreshToken: token });
             session.tokenRefresh(function(result){
                 if(result) { 
                     d.resolve(true); 
                     TokenHandler.set(result);
                 } else {
                     d.resolve(false);
                 }
             });
             return d.promise;
         }
     };

    return service;
}]);

session.service.js

angular.module('session.service', ['ngResource'])

    .factory('Session', ['$resource', '$rootScope', function($resource, $rootScope) {
     var Session = $resource('../api/tokens', {}, {
        create: {method: 'POST'}
    });

    Session.prototype.passwordSignIn = function(ob) {
        return Session.create(angular.extend({
                       grantType: 'password', 
                       clientId: $rootScope.clientId
                }, this), ob);
    };

    Session.prototype.tokenRefresh = function(ob) {
        return Session.create(angular.extend({
                       grantType: 'refresh_token', 
                       clientId: $rootScope.clientId
                    }, this), ob);
    };

    return Session;
}]);

Thanks to @Zerot for suggestions and code samples, I had to change part of the interceptor like this:

if (originalResponse.status === 401 && 
    (exception === 'token_not_found' || exception === 'token_expired')){
    var defer = $q.defer();

    queue.pushRetryFn(exception, function retryRequest() {
            var activeToken = $cookieStore.get('authorization-token').accessToken;
            var config = originalResponse.config;
            config.headers.Authorization = 'Bearer ' + activeToken;
            return $injector.get('$http')(config)
                    .then(function(res) {
                            defer.resolve(res);
                    }, function(err)
                    {
                            defer.reject(err);
                    });
    });

    return defer.promise;
}

Many thanks, Jani

like image 202
h3ndr1ks Avatar asked Sep 30 '22 22:09

h3ndr1ks


2 Answers

Have you tried to fix the error you have in the 1.2 log?

Error: [ngRepeat:dupes] Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: project in client.projects, Duplicate key: string:e

That error is at the exact point where you would need to see the $httpHeaders set line. It looks like your session.tokenrefresh is not working(and that code is also missing from the pastebin so I can't check.)

like image 192
Zerot Avatar answered Oct 03 '22 11:10

Zerot


Interceptors should always return a promise.

So in responseError, you should better return $q.reject(originalResponse); instead of just return originalResponse.

Hope this helps

like image 29
jujule Avatar answered Oct 03 '22 10:10

jujule