2

An Angular app using JWT for API authetication launchs a login dialog when a call to the API returns 401 "Unauthorized", to let the user input his credentials and get a new valid JWT. Then the app retries the failed unauthorized request and keeps the flow.

The code listed here is based in this solution by Chris Clarke.

.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push(['$q', '$location', '$injector', function ($q, $location, $injector) {
    return {
        'responseError': function(response) {
            // Keep the response waiting until we get new JWT
            var deferred = $q.defer();
            if (response.status === 401 && response.data.error && response.data.error.message.toLowerCase() === "unauthorized") {
                // JWT has expired
                // Open login dialog
                var cslAuth = $injector.get('cslAuth');
                if (cslAuth.isLoggedIn()) {
                    // Logout user, next pending request will not trigger auth dialog
                    cslAuth.logout();
                    $injector.get('ngDialog').openConfirm({
                        template: 'web_app/views/login.html',
                        className: 'ngdialog-theme-default',
                        showClose: false,
                        controller: 'LoginCtrl',
                        cache: false
                    })
                    .then(
                        function(value) {
                            // JWT has been refreshed. Try pending request again
                            var config = response.config;
                            // Inject the new token in the Auth header
                            config.headers.Authentication = cslAuth.getTokenHeader();
                            $injector.get("$http")(config).then(
                                function(response){
                                    deferred.resolve(response);
                                },
                                function(response) {
                                    deferred.reject();
                                }
                            );
                        },
                        function(value) {
                            deferred.reject();
                        }
                    );
                }
            } else {
                return $q.reject(response);
            }
            // Return a promise while waiting for auth refresh
            return deferred.promise;
        }
    }
}])
}])

The problem is when there are more than one request going with the expired token. The first one comming back should trigger the login dialog and get the new token. But how make the other pending requests wait until the new token is available? A flag could be set to tell all following incoming responses that a new token is being requested. A promise can be returned and all config objects can be stored in an array in a Service. When the new token is available all waiting requests could be retried. But what happen with unauthorized requests returning after the new token is available? They will trigger a new login dialog.

Some notes an extras:

  • This answer gives a solution to a related problem, but since there is a new login involved here I can not see how to adapt the solution to this case.

  • It's not an option to autorenew the token. Tokens will have 8 hours expiration (a working session) and new login is mandatory.

  • Is it safe to inject services (cslAuth and $http here) in a config object? My code is working but I have read they can not be fully ready at this point.
Community
  • 1
  • 1
David Casillas
  • 1,801
  • 1
  • 29
  • 57

1 Answers1

3

This code improves the one posted in the question in 2 ways:

  • Keeps an array of pendingRequests to retry then after succefull token refresh
  • On 401, it cheks if the auth token used is different to the current one to retry again the request without asking for login. This solves the problem of requests not included in the pendingRequests array.

Code:

/ HTTP Interceptors
.config(['$httpProvider', function($httpProvider) {
    $httpProvider.interceptors.push(['$q', '$location', '$injector', function ($q, $location, $injector) {
        var pendingRequests = [];
        function retryRequest(deferred, config, cslAuth) {
            config.headers.Authentication = cslAuth.getTokenHeader();
            $injector.get("$http")(config).then(
                function(response){
                    deferred.resolve(response);
                },
                function(response) {
                    deferred.reject();
                }
            );
        }
        return {
            'responseError': function(response) {
                switch (response.status) {
                    case 401: // JWT has expired
                        // To keep the response waiting until we get new JWT
                        var deferred = $q.defer();
                        var cslAuth = $injector.get('cslAuth');
                        // Check if a new token exists. Then retry the request with new token
                        if (response.config.headers.Authentication != cslAuth.getTokenHeader()) {
                            retryRequest(deferred, response.config, cslAuth);
                            // Return a promise while waiting
                            return deferred.promise;
                        }
                        // Open login dialog
                        if (cslAuth.isLoggedIn()) {
                            // Logout user, next pending request will not trigger auth dialog
                            cslAuth.logout();
                            $injector.get('ngDialog').openConfirm({
                                template: 'web_app/views/login-inner.html',
                                className: 'ngdialog-theme-default',
                                showClose: false,
                                controller: 'LoginCtrl'
                            })
                            .then(
                                function(value) {
                                    // JWT has been refreshed. Try pending requests again
                                    for (var i = 0; i < pendingRequests.length; i++) {
                                        retryRequest(pendingRequests[i].deferred, pendingRequests[i].config, cslAuth);
                                    }
                                },
                                function(value) {
                                    pendingRequests[i].deferred.reject();
                                }
                            );
                        }
                        // Return a promise while waiting for auth refresh
                        pendingRequests.push({'deferred': deferred, 'config': response.config});
                        return deferred.promise;
                        break;
                    default: // What happened?
                        return $q.reject(response);
                        break;
                }
            }
        }
    }])
}])
David Casillas
  • 1,801
  • 1
  • 29
  • 57