witoldsz / angular-http-auth

MIT License
2.38k stars 417 forks source link

How to handle multiple 401 request #95

Open reggiepangilinan opened 9 years ago

reggiepangilinan commented 9 years ago

Hello! First of all good job on the interceptor :)

I have a scenario wherein i have multiple 401 response from my server and each 401 response is intercepted by angular-http-auth then the event:auth-loginRequired is triggered.

This will result into multiple request for a new token invalidating the previous ones.

Do you have a sample or can give some advice on how to handle this kind of scenario?

Cheers!

rolandocc commented 9 years ago

Hi @Reggieboy, I had the same issue, the best solution I found was first do just one request which I was sure would get a 401 and consecuently would fire the loginRequired event. This can be a dummy request just to be sure the next ones are sent with a valid token.

Cheers.

witoldsz commented 9 years ago

@rolandocc is right, that should solve your problem. Try to keep the bootstrap process of your application tight: send a probe to figure the response code and wait until it's all OK before firing up controllers.

wcyrek-comrise commented 8 years ago

I'm trying to do as @rolandocc suggests as well. I would like to put something in the route handler to prevent the application from changing location before probing for 401 on certain routes, or something similar. In such case I could preemptively stop the application from going there and having it return when login is canceled. Since right now I end up with partial page render in case of login cancel.

Mr-Anonymous commented 8 years ago

Hi @rolandocc, @witoldsz and @wcyrek-comrise

I love the idea of bootstrapping the process with one request first before firing the controller and going to a route. Unfortunately, I am not entirely sure on how to do it. This is what I have attempted so far and I am not sure if this is what you guys meant:

In the router state I added a resolve like this that does a dummy ping to my server route which requires auth or else throws a 401 or 400 bad request:

.state('dashboard', {
    abstract: true,
    url: '/dashboard',
    templateUrl: 'views/dashboard.html',
    resolve: {
        CheckAuth: ['$q','DummyPing', function($q, DummyPing) {
           var deferred = $q.defer();
           DummyPing.ping()
           .success(function (data, status, headers, config) {
               deferred.resolve();
            })
            .error(function (data, status, headers, config) {
               deferred.reject(); // Errors handled by http-auth-interceptor
            });
            return deferred.promise;
        }]
    }
})

Is this what you guys meant? I am not entirely happy with my method, since I am wondering if its better to perhaps have this under $rootScope.$on('$stateChangeStart) event. Is there a better way to do this? Can someone share a sample please cuz I really like your suggested approach of checking the auth-status first before firing all the other requests and controller.

witoldsz commented 8 years ago

Hello there, I know there are lot of developers who have their reasons to poke with router change events, but I still cannot get it. What is the reason you make your life so hard? I have written few apps using this module: from administration panels, local in-company apps to wide-open Internet services and I had never to deal with route change listeners or controllers' resolvers for authentication and authorization purposes.

What is your specific use case you cannot follow the simple solution from the project's README documentation?

Mr-Anonymous commented 8 years ago

Hi @witoldsz I have already followed the 'simple' solution in README which resolves 90% of my queries with no problem. However, there is 1 use case that the http-interceptor doesnt resolve directly for me. In my app, I have protected images where a token needs to be sent with the image url parameter for the server to check the authentication and permissions before sending its response. If I use the $http _GET to get the image binary directly in base64 then the http-auth-interceptor is able to intercept it fine with no problem. But that is not ideal when I have to display many images on the page. So in my use case, the Angular sends the img urls with token as the parameter that the browser can query (something like: example.com/image.jpg?token=xyz. But when that fails with 401, the http-auth-interceptor will not pick-up. Hence the only option I was thinking for this case was to perhaps add a dummyPing to the server using $http service first to check the token before sending out the view with the image urls with token parameter. Hence I was looking at this work-around of how you guys suggest to "keep the bootstrap process of your application tight".

Hope this makes sense :)

witoldsz commented 8 years ago

Hi, thanks for explanation of your case, but still I cannot get the problem at it's root. As far as I understand, at some point your application has to get some kind of token, so the images can be displayed. What does it have to deal with router and routes?

Let's just assume few things, so I can try to replay what is happening:

User navigates your application and at some point they summon the part, where a controller needs to fetch the token, so it can provide some directive with URLs.

Now, looking from the directive point of view we have two options: a) I am the directive and I ask for URLs, or b) the directive does not get created (i.e. concrete instance) until the URLs are available.

Option (a) is simple thanks to "promises". It asks for URLs, so controller just asks for token and return the promise. Using the "angular-http-auth" can help here, because if the process of fetching the token goes the 401 detour, the original promise returned to directive won't notice, assuming you won't spoil the process with some redirections or you won't destroy the current state in any other way.

Option (b) is also simple, you have a token and your directive is behind the "ng-if" until the controller says it's ready.

Question reminder: what does it all has to do with router? Why won't you leave it to "the" controller, or if you like, the directive with the logic built-in?

Mr-Anonymous commented 8 years ago

@witoldsz ,

First of all, I humbly thank you for taking the time to write back to me. Much obliged!

There are 2 cases I have and I will explain a bit more on exact user navigation and process scenario I have so far:

Scenario 1: Image Urls

1) User logs into the app and Satellizer sets up the JWT token and adds to localStorage.

2) After that, the user avatar url is set-up in controller or via directive like this: $rootScope.avatarUrl = 'api/avatar?token=' + localStorage.getItem(token); where the img src is like this in view <img ng-src="{{ avatarUrl }}" />

3) Now, the server api will read the token, identifies the userId and return the proper avatar image for that user. Uptill here, everything is fine since the url that was created after the token got created is still valid. But lets say this user goes to the next page after some time when the token had expired. If the next page doesnt have any server query from controller / service, then http-auth-interceptor will not pick up on anything. But since the token has become invalid, the img src that got created by controller before will get a 401 error when the browser queries it. Since the interceptor doesnt pick it up, a broken image will appear here.

4) Even when the directive is used like your suggestion, the img url doesnt get updated or changed if the token expired. Thats why I thought perhaps I might need to add preliminary check in router to check token validity for each route and fire the controller only after that which builds the $rootScope.avatarUrl based on the valid localStorage.getItem(token) so it doesnt have stale token if it had expired because http-auth-interceptor only picks it up when 401 is thrown within angular itself.

Scenario 2: Image Upload

This is a little similar to the above but when using the Nergh's Angular-File-Upload module.

1) User logs into the app and Satellizer sets up the JWT token and adds it to localStorage.

2) User goes to the Image Upload page. When user uploads, I need to add the token into the header like this for server auth check before upload is done:

// Add JWT Token to header
uploader.onBeforeUploadItem = function (item) {
  item.headers = {
    'Authorization': 'Bearer ' + localStorage.getItem(token)
  };
};

3) If the token had expired, the upload will fail and will throw a 401 error. Unfortunately, http-auth-interceptor doesnt pick up on this and 401 fails silently.

After your suggestion, I did gave more serious thought on your advise and it did make sense to keep everything in controller itself rather than poking the router. This is what I have come up with for now:

Attempted Solutions:

Scenario 1 Solution: Doing a 2 step solution for this:

(i) First added a dummy ping to the page controllers which doesnt have any server queries so a server auth-check is triggered. If 401 occurs, http-auth-interceptor will notice it.

// Just a Dummy Ping to check auth status
CheckAuth.ping();

(ii) Next in the login controllers, after the login is successful it calls a service which refreshes the $rootScope.avatarUrl again with the new token like this:

commonFunctions.refreshAvatar = function(){
            $rootScope.avatarUrl = 'api/avatar?token=' + localStorage.getItem(token) + '&time=' + new Date().getTime();
};

This method works fine, but the only drawback is I would need to remember to add other image urls to this service list in the future when I have to add images with similar scenario so all of them are refreshed.

Scenario 2 Solution: Instead of using the module's default uploadAll() method, I created a new method that checks the auth first using a dummy Ping and calls the uploadAll() method if its success or show a login box. Something like this:

$scope.uploadAvatarButton = function() {
        CheckAuth.ping()
        .success(function() {
            uploader.uploadAll();
        })
        .error(function(error) {
            LoginModalService()
                .then(function () {
                        uploader.uploadAll();
                })
                .catch(function () {
                        return $state.go('landingpage');
                });
        });
}

I am not sure if the above is the right approach or if there is a better way to do it. But it certainly does avoid doing anything from the router like you had advised. Do let me know if you feel it can be done differently.

Thanks again for taking the time to write to me.

Mr-Anonymous commented 8 years ago

updated Scenario 1 Solution in my last post. :)

ViniciusBabugia commented 7 years ago

I have the library updated, but I still have the same problem, my token is generated validated, but the 401 are still infinite, someone has some solution I've already seen topics 95, 120, 130, 131 and nothing solved