Search code examples
angulartypescriptrxjsangular2-jwt

angular 2 refresh token before custom http request


I want to refresh the token before the custom http request if it is expired. I try my code when I'm sure that the token is expired but it gives the following console result:

Token refresh is required  app.js:1:92855
updateToken() method inside  app.js:1:93301
tokenNotExpired?: false  app.js:1:92674
Token refresh is required  app.js:1:92855
updateToken() method inside  app.js:1:93301
tokenNotExpired?: false  app.js:1:92674
Token refresh is required  app.js:1:92855
updateToken() method inside  app.js:1:93301
tokenNotExpired?: false  app.js:1:92674
Token refresh is required  app.js:1:92855
updateToken() method inside  app.js:1:93301
tokenNotExpired?: false  app.js:1:92674
............ lots of the same sentences and finally exception:

EXCEPTION: Uncaught (in promise): Error: Error in :0:0 caused by: too much recursion
k@http://localhost/xxx/node_modules/zone.js/dist/zone.min.js:1:11750
............

As I understand during refreshing the token it goes into an infinite loop. I've tested the updateToken() method somewhere else with a button and it works fine.

What am I doing wrong?

custom http service

import { Injectable } from '@angular/core';
import { Http, XHRBackend, RequestOptions, Request, RequestOptionsArgs, Response, Headers } from '@angular/http';
import { tokenNotExpired } from "angular2-jwt";
import { Observable } from "rxjs/Observable";

@Injectable()
export class HttpService extends Http {

    constructor (backend: XHRBackend, options: RequestOptions) {
        let token = localStorage.getItem('access_token'); // your custom token getter function here
        options.headers.set('Authorization', `Bearer ${token}`);
        super(backend, options);
    }

    request(url: string|Request, options?: RequestOptionsArgs): Observable<Response> {
        let token = localStorage.getItem('access_token');

        if (typeof url === 'string') { // meaning we have to add the token to the options, not in url
            if (!options) {
                // let's make option object
                options = {headers: new Headers()};
            }
            options.headers.set('Authorization', `Bearer ${token}`);
        } else { // we have to add the token to the url object
            url.headers.set('Authorization', `Bearer ${token}`);
        }

        console.log("tokenNotExpired?: " + tokenNotExpired('access_token'));

        if(tokenNotExpired('access_token')){ // if token is NOT expired

            return super.request(url, options).catch(this.catchAuthError(this));

        }else{  // if token is expired
            console.log("Token refresh is required");
            return this.updateToken()
                .flatMap((result: boolean) => {
                    console.log("updateToken result");
                    console.log(result);
                    if (result) {
                        return super.request(url, options).catch(this.catchAuthError(this));
                    } else {
                        return Observable.throw(new Error('Can\'t refresh the token'));
                    }

                });
        }
    }

    updateToken(): Observable<any> {

        console.log("updateToken() method inside");

        let body: string = 'refresh_token=' + localStorage.getItem('refresh_token') + '&client_id=' + localStorage.getItem('resource')  + '&grant_type=refresh_token';

        return super.post(
            localStorage.getItem("authUrl"),
            body,
            new RequestOptions({headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' })})
        )
            .map((response: Response) => {
                let returnedBody: any = response.json();
                console.log("post returnedBody");
                console.log(returnedBody);
                if (typeof returnedBody.access_token !== 'undefined'){
                    localStorage.setItem('access_token', returnedBody.access_token);
                    localStorage.setItem('refresh_token', returnedBody.refresh_token);

                    console.log("Token Refreshed: refreshed access_token");
                    console.log(localStorage.getItem('access_token'));

                    return true;
                }
                else {
                    return false;
                }
            });
    }

    private catchAuthError (self: HttpService) {
        return (res: Response) => {
            console.log(res);
            return Observable.throw(res);
        };
    }
}

app module

@NgModule({
  imports: [ .......... ],
  declarations: [ ....... ],
  providers: [
    {
      provide: HttpService,
      useFactory: (backend: XHRBackend, options: RequestOptions) => {
        return new HttpService(backend, options);
      },
      deps: [XHRBackend, RequestOptions]
    }

  ],
  bootstrap: [ Application ]
})

Solution

  • Inside your updateToken method you are calling super.post which would be equivalent to Http.prototype.post.apply(this...), and super.post will call internally this.request().

    The this context being your custom HttpService, it ends up in a recusrsive call of the HttpService request method. You should call super.request instead :

    return super.request(
        new Request({
          method: RequestMethod.Post,
          url: localStorage.getItem("authUrl"),
          body,
          headers: new Headers({
            'Content-Type': 'application/x-www-form-urlencoded'
          })
        })
    
      )
      .map((response: Response) => {
        let returnedBody: any = response.json();
        console.log("post returnedBody");
        console.log(returnedBody);
        if (typeof returnedBody.access_token !== 'undefined') {
          localStorage.setItem('access_token', returnedBody.access_token);
          localStorage.setItem('refresh_token', returnedBody.refresh_token);
    
          console.log("Token Refreshed: refreshed access_token");
          console.log(localStorage.getItem('access_token'));
    
          return true;
        } else {
          return false;
        }
      });
    

    Also note that it might not be the best idea to create a custom Http service.

    But maybe you could create a service that gets http injected, because you probably won't need to be authenticated just to fetch some simple static data from an ajax call.

    That would also avoid the problem you encountered with recursive call stack exceeded.

    @Injectable()
    export class MyAuhtHttpService{
    
      constructor(private http:Http){}
    
    }