Search code examples
authenticationangularangular2-observables

Angular 2 dealing with multiple failed requests requiring an auth token refresh


I am making an Angular 2 frontend for an API that uses access tokens. I am trying to use observables and ngrx/store with this.

Logging in and out works fine and as intended. I have also written some code for when requests fail due the token expiring. This code works okay ordinarily but I run into trouble when I have multiple requests in a short span of time. This happens, for example, when I refresh the page and the app tries to fill two or three stores required across the app.


My auth service has functions like these:

refreshLogin(): Observable<any> {
    const username = this.currentUser();
    let query = new URLSearchParams();
    query.set('refresh_token', this.refreshToken());
    query.set('grant_type', 'refresh_token');
    return this.getToken(username, query.toString());
}

private getToken(username: string, body: string): Observable<any> {
    const url = Config.AUTH_TOKEN_PATH;
    return this.http.post(url, body)
        .map((res: Response) => res.json())
        .do(
            (data) => {
                const token = {
                    username: username,
                    accessToken: data.access_token,
                    refreshToken: data.refresh_token,
                    tokenType: data.token_type,
                    expiresIn: data.expires_in
                };
                this.store.dispatch(AuthActions.loginSuccess(token));
            },
            error => {
                const description = error.json().error_description;
                this.store.dispatch(AuthActions.loginError(description));
                console.error(error);
            }
        )
    ;
}

My REST function has a function like this:

get(url: string): Observable<any> {
    return this.http.get(url, new RequestOptions({ headers: this.authHeader() }))
        .catch(err => {
            if (err.status === 401) {
                return this.auth.refreshLogin()
                    .switchMap(() => this.http.get(url, new RequestOptions({ headers: this.authHeader() })))
                    .catch(err2 => err2);
            } else {
                console.log(err);
            }                
        })
        .map((res: Response) => res.json())
    ;
}

I don't feel that the functions currentUser(), refreshToken(), and authHeader() are required to understand my question.


If I have one failed request and the error is a 401, my app calls refreshLogin(), gets a new access token and stores it, and the original request is tried again with the new access token.

If I have more that one failed request, and they are happening effectively at the same time, I run into issues. For example, say I have two GET requests. They both return 401 errors. They both fire off the refreshLogin() function. One refreshLogin() succeeds and stores a new access token; the other fails because the token being used to refresh is no longer valid. This stream of functions now fails, causing my app to stall.

One solution would be to stack my GET requests in series, but that doesn't seem like it should have to be the case.

I feel like there should be a solution where, on a failed GET (or other) request, the app triggers a call for the access token to be refreshed. The auth service would throttle these requests so there can only be one every couple of seconds or something. It fulfils this request, and the new access token is returned to all the requests to try again.

Do you think this is a sensible approach or am I just trying to patch up an approach that was badly thought out to start with? How would you recommend making these parts interact?


Solution

  • To solve this issue, I created a new @ngrx/store action for refreshing my token and consume it using @ngrx/effects. I understand that not everyone will want to use effects but I find it to be very useful it a number of scenarios, not just this one.

    So my REST get function now looks like this:

    get(url: string): Observable<any> {
        return this.http.get(url, new RequestOptions({ headers: this.authHeader() }))
            .catch(err => {
                if (err.status === 401) {
                    this.store.dispatch({type: 'REFRESH TOKEN'});
                }
                return Observable.of(err);
            })
            .map((res: Response) => res.json())
        ;
    }
    

    This action is picked up by my effects module ...

    @Effect() refreshToken$ = this.actions$
        .ofType('REFRESH TOKEN')
        .throttleTime(3000)
        .switchMap(() => this.authService.refreshLogin())
        .map((response) => {
            // Store token
        })
    

    Meanwhile, the function/action/whatever that receives the response from the REST get request can determine whether the request was successful or whether it failed because of failed authentication. If it was the latter, it can fire off the request another time (after waiting for the renewed token); otherwise, it can deal with another type of failure in a different way.