Search code examples
javascriptangularjsaccess-token

Angular Refresh Token Authorization Issue


I have an Angular client.I created a Bearer Token Authorization system using Asp.Net WebApi. It uses access tokens with a short-lived specified time and refresh tokens with a long-lived specified time. I have a problem when an access token expires and a request is made to a page with multiple $http requests. I have a service set up for retrieving data where, if the first response comes back 401 it goes and gets a new token from the refresh token server and then makes a second request for the data. What is happening is two secured requests are firing almost simultaneously, they both fail 401, they both got to get a new token and the first token retrieval is invalidated by the second.

Two fixes jumped out at me. 1.) Chain the calls to my data service. 2)Arrange my request in a way where the page makes one request for all the data. I am not going to go into detail why these two approaches are unacceptable, I am sure you know why.

I thought about "locking" requests to the token server while other requests are in progress to it or setting up a queue service for token request, I am obviously trying to avoid bottlenecks while also maintaining the integrity of my promise chain.

I am sure other's have experienced this, just wondering what approached others have taken.

Thanks in advance!


Solution

  • Are you overriding the $httpProvider in the angular app.config method?

    By using $httpProvider.interceptors.push(<Your auth interceptor>);

    If not, the interceptor will monitor all the requests your angular app makes and processes them using the "optional functions" below.

    You would setup a factory to handle everything such as the bare bones one below. This will basically take it one request at a time. So for the simulatanious request problem, the first request will pass through the factories request function and set the headers. The request will then be made and the response function will be hit. If the response returns an error the factories response error function will be called and if will evaluate the status code for 401 and you can implement a way to request a new bearer token. After that is done the second request will send and the new bearer token will already be set and you should be good to go.

    angular.module('authInterceptor', []).factory('Interceptor', Interceptor);
    
    function Interceptor($q, $cookie){
        var factory = {
            response: response,
            responseError: responseError,
            request: request,
            };
    
        //////////////////
    
        //This will setup the http request
        //Since your using a bearer token and refresh token you will want to add the bearer token the the header of all the requests
        function request(config) {
            //Set config.headers to existing headers or if none exists set it to an empty object
            config.headers = config.headers || {};
    
            //Grab you auth data from storage
            //I usually store the object from the /token request in $cookie
            var authData = $cookies.getObject("authData");
    
            //If authData exists set it as a Authorization header
            if (authData) {
                config.headers.Authorization = "Bearer " + authData.access_token;
            }
        }
    
        function response(response) {
            return response || $q.when(response);
        }
    
        //Where the 401 magic happens
        //If your rejection status is a 401(UnAuthorized)
        //You want to request a new token from the authorization service
        function responseError(rejection) {
            var deferred = $q.defer();
    
            if (rejection.status === 401) {
                //You need to manually inject services or factories since your in the .config method
                var Authentication = $injector.get("Authentication");
    
                //I usually set up a Authentication Service to do the dirty work so the interceptor doesnt get bloated with code
    
                //See below for reissue token
                Authentication.reissueToken()
                    .then(function () {
    
                    //Valid grant
    
                }, function (err) {
    
                    //Invalid grant
                });
    
                //When this get his the original $http request that failed will get resent to the server
                return deferred.promise;
            }
    
            return $q.reject(rejection);
        }
    }
    
    
    /*Reissue token 
             * Grabs the authData from storage, using the bearer token make a request to the token endpoint
             * If the refresh token hasnt expired the success will send back a new bearer token.
             * Update the auth cookie with the new token object
             * If the refresh token has expired the request will result in an invalid_grant request will
             * be rejected, user must log in again*/
     function reissueToken() {
            var deferred = $q.defer();
            var authData = $cookies.getObject("authData");
    
            if (authData) {
                var token = authData.refresh_token;
                var data = "grant_type=refresh_token&refresh_token=" + token + "&client_id=" + clientId;
    
                $http.post("/token", data)
                    .success(function (newToken) {
                        //Update wherever you store the authorization data
                    }).error(function (err, status) {
                        deferred.reject();
                });
            } else {
                deferred.reject();
            }
    
            return deferred.promise;
        }
    

    You can read more here ctrl + f for interceptor