0

I am trying to decorate the built-in request method in angular Http. I am triggering a multitude of chained and/or asynchronous requests at the same time which in turn results in that the 401 response is triggered a lot of times simultaneously. This crashes the authorization as the same refresh token is used multiple times.

So far, I've come this far:

request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options).catch(error=> {
        if (error.status === 401) {
            return Observable.create((observer: Observer<Response>) => {
                // Use promise to avoid OPTIONS request
                this.oauthService.refreshToken().then((tokenResponse: Response) => {
                    console.debug('promise resolved');

                    observer.next(tokenResponse);
                    observer.complete();
                });
            }).mergeMap((tokenResponse) => {
                options = this.updateAuthHeader(options);
                return super.request(url, options);
            });
        } else {
            return Observable.throw(error);
        }
    });
}

My issue seems to be that i need to que the pending requests when a 401 has been encountered, or that i just lack the knowledge of how to achieve that with observables. As far as i understand the mergeMap should to this for me, but sadly they all trigger the request before my token has been refreshed.

Any ideas of how to achieve this?

user93
  • 1,866
  • 5
  • 26
  • 45
Jari Thorup Palo
  • 519
  • 1
  • 6
  • 17

1 Answers1

2

With the help from @stely000, some colleagues, and other online communities I ended up this:

Since I am using the solution for intercepting the built-in Http service from angular2/angular4 found here https://scotch.io/@kashyapmukkamala/using-http-interceptor-with-angular2 my code differs a bit from other solutions i've found. The OAuthService i refer to in different places can be found here: https://github.com/manfredsteyer/angular-oauth2-oidc. Due to dependencies in that service to Http i had to inject that service after to avoid circular dependencies. If you have any questions about that, please just ask me and I'll answer that as well. :)

Basically this my solution for the refresh_token functionality to trigger once if the backend services respond with 401 is achieved in three steps:

  1. Create an observable that is shared to avoid this.postRequest() to trigger more than once at a time.

  2. Create request headers and add post to endpoint where refresh_token is handled.

  3. Listen to the shared observer from step 1. When the token has been refreshed, extract and update data in localstorage.

Now, in my constructor I create the shared observable:

constructor(
    backend: ConnectionBackend,
    defaultOptions: RequestOptions,
    private injector: Injector
) {
    super(backend, defaultOptions);
    // Step 1: Create an observable that is shared to avoid this.postRequest() to trigger more than once at a time
    this.refreshTokenObserver = Observable.defer(() => {
        return this.postRequest();
    }).share();
}

Then I create a method for posting the request to refresh the access_token (NOTE: this is basically a copy of the code that is held in the OAuthService. This is because the method for that is not public in that service):

// This method will only be triggered once at a time thanks to she shared observer above (Step 1).
private postRequest(): Observable<any> {
    // Step 2: Create request headers and add post to endpoint where refresh_token is handled.
    let search = new URLSearchParams();
    search.set('grant_type', 'refresh_token');
    search.set('client_id', this.oauthService.clientId);
    search.set('scope', '');
    search.set('refresh_token', localStorage.getItem('refresh_token'));

    let headers = new Headers();
    headers.set('Content-Type', 'application/x-www-form-urlencoded');

    let params = search.toString();

    return super.post(this.oauthService.tokenEndpoint, params, { headers }).map(r => r.json());
}

Then I have a method for extracting data and updating the local storage with the response from then endpoint:

// This method is triggered when the server responds with 401 due to expired access_token or other reasons
private refreshToken() {
    // Step 3: Listen to the shared observer from step 1. When the token has been refreshed, extract and update data in localstorage
    return this.refreshTokenObserver.do((tokenResponse) => {
        localStorage.setItem("access_token", tokenResponse.access_token);
        if (tokenResponse.expires_in) {
            var expiresInMilliSeconds = tokenResponse.expires_in * 1000;
            var now = new Date();
            var expiresAt = now.getTime() + expiresInMilliSeconds;
            localStorage.setItem("expires_at", "" + expiresAt);
        }

        if (tokenResponse.refresh_token) {
            localStorage.setItem("refresh_token", tokenResponse.refresh_token);
        }
    },
    (err) => {
        console.error('Error performing password flow', err);
        return Observable.throw(err);
    });
}

In order to initiate the steps above, the initial request needs to be triggered and respond with a 401:

request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options).catch(error=> {
        if (error.status === 401) {
            // It the token has been expired trigger a refresh and after that continue the original request again with updated authorization headers.
            return this.refreshToken().mergeMap(() => {
                options = this.updateAuthHeader(options);
                return super.request(url, options);
            });
        } else {
            return Observable.throw(error);
        }
    });
}

Bonus: The method i use for updating the Authorization header is basically using the functionality in the OAuthService mentioned above:

private updateAuthHeader(options: RequestOptionsArgs) {
    options.headers.set('Authorization', this.oauthService.authorizationHeader());

    return options;
}

Reflections/Thoughts: The original idea from my side was to use the OAuthService to refresh token. This was harder than i expected due to the mix of promises and observables. I can probably change the postRequest method to use the mentioned service methods. I don't really know what the better/cleaner solution might be.

Also, I think that this is something that should be available for everyone to find an easy solution for. This was hard to achieve by myself and I thank everyone who helped me (both here at SO, IRL, and other communities).

Jari Thorup Palo
  • 519
  • 1
  • 6
  • 17