Angular 4 Tutorial – Handling Refresh Token with New HttpInterceptor

by | Nov 9, 2017

One of the very cool new features that came out in Angular 4.3 was the HttpInterceptor.  Of course this isn’t new to Angular 1 developers who had it all along but now 4.3+ developers have it so that we can add header info, handle responses, catch errors, etc. to all new HttpClient (new in 4.3 also) calls.  All of this fun new stuff is available for import at ‘@angular/common/http’.  This post isn’t an in depth study of how to do an HttpInterceptor.  Instead, it will cover how to update an OAuth authorization token using the refresh token in the HttpInterceptor.

All of the code for this post is available at github.

First, an explanation of what is happening with OAuth and the refresh token.  When you login, you get an authorization token and a refresh token.  The refresh token is configured with a certain timeout.  So when that timeout is reached (regardless of activity) you receive a 401 (Unauthorized) error from your API call.  At that point, your code must attempt to refresh the token by calling the OAuth refreshToken endpoint (with the refresh token string).  It gives you back a new authorization token and a new refresh token.  From then on, you use the new authorization token to make your API calls.

Obviously, the new HttpInterceptor is perfect for this scenario.  It can add the Authorization header to all http calls and catch exceptions to listen for 401 errors.  When the 401 error happens, it can make the refreshToken call and try again with the new authorization token.

Simple UI

The sample project I created on github is just two buttons.  The first button calls one API method that will generate a 401 error.  The second button calls two API methods (run in parallel) that will both generate a 401 error.  It displays the ending status of the method calls on screen.  The hope is that the status will be 200 and not 401.

export class AppComponent {
  message: string;

  constructor(private testService: TestService,
              private authService: AuthService) { }

  test401() {
    this.testService.getData()
      .subscribe((response: any) => {
        this.message = `Worked with status = ${response.status}`;
      },
      error => this.message = `Failed with status = ${error.status}`);
  }

  test401Multiple() {
    let dataObservable$ = this.testService.getData();
    let lookupObservable$ = this.testService.getLookup();

    Observable.forkJoin(dataObservable$, lookupObservable$)
      .subscribe(([dataResult, lookupResult]) => {
        this.message = `Worked with getData status = ${dataResult.status} and getLookup status = ${lookupResult.status}`;
      },
      error => this.message = `Failed with status = ${error.status}`);
  }
}

AuthService

The AuthService will give us the tokens.  I’m making the assumption that you already know how to call your refreshToken method on the server and store the results in session storage or however you want to cache them.  Here is my example code:

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/delay';

export class AuthService {
    // Assuming this would be cached somehow from a login call.
    public authTokenStale: string = 'stale_auth_token';
    public authTokenNew: string = 'new_auth_token';
    public currentToken: string;

    constructor() {
        this.currentToken = this.authTokenStale;
    }

    getAuthToken() {
        return this.currentToken;
    }

    refreshToken(): Observable<string> {
        /*
            The call that goes in here will use the existing refresh token to call
            a method on the oAuth server (usually called refreshToken) to get a new
            authorization token for the API calls.
        */

        this.currentToken = this.authTokenNew;

        return Observable.of(this.authTokenNew).delay(200);
    }
}

TestService

The TestService will handle the calling of the API.  I created an API here:  http://docs.testerrorresponses.apiary.io/#

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class TestService {
    constructor(private http: HttpClient) { }

    getData() {
        return this.http.get<{status}>('http://private-4002d-testerrorresponses.apiary-mock.com/getDataError401');
    }

    getLookup() {
        return this.http.get<{status}>('http://private-4002d-testerrorresponses.apiary-mock.com/getLookupError401');
    }
}
  • The important thing to note here is that you must use HttpClient (which is new in Angular 4.3+) imported from @angular/common/http
    • The “classic” http client does not trigger the HttpInterceptor
  • The API call simply returns a 401 error

HttpInterceptor

The RequestInterceptorService will implement HttpInterceptor which has only one method:  intercept.  It will add a token to the header on each call and catch any errors that might occur.

@Injectable()
export class RequestInterceptorService implements HttpInterceptor {

    isRefreshingToken: boolean = false;
    tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);

    constructor(private authService: AuthService) {}

    addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
        return req.clone({ setHeaders: { Authorization: 'Bearer ' + token }})
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
        return next.handle(this.addToken(req, this.authService.getAuthToken()))
            .catch(error => {
                if (error instanceof HttpErrorResponse) {
                    switch ((<HttpErrorResponse>error).status) {
                        case 400:
                            return this.handle400Error(error);
                        case 401:
                            return this.handle401Error(req, next);
                    }
                } else {
                    return Observable.throw(error);
                }
            });
    }
  • We’ll use isRefreshingToken and tokenSubject later
  • addToken will add the Bearer token to the Authorization header
  • In the intercept method, we return next.handle and pass in the cloned request with a header added
    • Get the auth token from the AuthService
  • In the catch, we can handle any and all errors that occur, of primary interest to this example is the 401 Unauthorized error

Handling 401 Unauthorized

The code to handle the 401 error is the most important.

    handle401Error(req: HttpRequest<any>, next: HttpHandler) {
        if (!this.isRefreshingToken) {
            this.isRefreshingToken = true;

            // Reset here so that the following requests wait until the token
            // comes back from the refreshToken call.
            this.tokenSubject.next(null);

            return this.authService.refreshToken()
                .switchMap((newToken: string) => {
                    if (newToken) {
                        this.tokenSubject.next(newToken);
                        return next.handle(this.addToken(this.getNewRequest(req), newToken));
                    }

                    // If we don't get a new token, we are in trouble so logout.
                    return this.logoutUser();
                })
                .catch(error => {
                    // If there is an exception calling 'refreshToken', bad news so logout.
                    return this.logoutUser();
                })
                .finally(() => {
                    this.isRefreshingToken = false;
                });
        } else {
            return this.tokenSubject
                .filter(token => token != null)
                .take(1)
                .switchMap(token => {
                    return next.handle(this.addToken(this.getNewRequest(req), token));
                });
        }
    }

    /*
        This method is only here so the example works.
        Do not include in your code, just use 'req' instead of 'this.getNewRequest(req)'.
    */
    getNewRequest(req: HttpRequest<any>): HttpRequest<any> {
        if (req.url.indexOf('getData') > 0) {
            return new HttpRequest('GET', 'http://private-4002d-testerrorresponses.apiary-mock.com/getData');
        }

        return new HttpRequest('GET', 'http://private-4002d-testerrorresponses.apiary-mock.com/getLookup');
    }

    logoutUser() {
        // Route to the login page (implementation up to you)

        return Observable.throw("");
    }
  • If isRefreshingToken is false (which it is by default) we will enter the code section that calls authService.refreshToken
    • Immediately set isRefreshingToken to true so no more calls come in and call refreshToken again – which we don’t want of course
    • Set the tokenSubject to null so that subsequent API calls will wait until the new token has been retrieved
    • Call authService.refreshToken (this is an Observable that will be returned)
    • When successful, call tokenSubject.next on the new token, this will notify the API calls that came in after the refreshToken call that the new token is available and that they can now use it
    • Return next.handle using the new token
      • For the example, I had to change the call so that it doesn’t generate a 401 error…don’t do this in your code 🙂
  • If isRefreshingToken is true, we will wait until tokenSubject has a non-null value – which means the new token is ready
    • Only take 1 here to avoid returning two – which will cancel the request
    • When the token is available, return the next.handle of the new request
  • When the call to refreshToken completes, in the finally block, reset the isRefreshingToken to false for the next time the token needs to be refreshed
  • Note that no matter which path is taken, we must return an Observable that ends up resolving to a next.handle call so that the original call is matched with the altered call

Results

If I run the application and click the first button, this is what I see for output on screen:

Worked with status = 200

Then the second button, this is the output:

Worked with getData status = 200 and getLookup status = 200

The network tab will be the most interesting thing to see though because it shows a 401 error for all of the initial calls and then the refreshToken call, followed by all of the calls succeeding with 200.  This is the output for the multiple calls button:

  • The first two getLookup and getData calls that give a 200 status are the OPTION calls
  • The second two getLookup and getData calls are the actual successful API GET calls
  • The reason there is no “refreshToken” call here is that I’m simulating it and not making an actual call
    • But in the wild, you should see that right after the 401 error(s)

What if the Refresh Token Times Out?

Some may be wondering at this point what would happen if the refresh token times out.  Usually caused by not making any API calls for whatever the timeout is configured for.  Well, what happens is that you should see a 400 error with an ‘invalid_grant’ message.  So the handle400Error code should most likely log out the user and direct them to the login page.

Here is the code that checks for this situation:

    handle400Error(error) {
        if (error && error.status === 400 && error.error && error.error.error === 'invalid_grant') {
            // If we get a 400 and the error message is 'invalid_grant', the token is no longer valid so logout.
            return this.logoutUser();
        }

        return Observable.throw(error);
    }

HttpClient Caveat

After publishing this article, many of you ran into a circular dependency issue when you tried to inject AuthService into the HttpInterceptor constructor. This was because you added a dependency on HttpClient for the refreshToken call. It turns out that in the HttpInterceptor, you cannot inject anything that has a dependence on HttpClient into the constructor. So to get around it, you simply need to inject the Injector Angular class into the HttpInterceptor constructor and then use that in a JIT way within the injector.

For example, when you want to do the refreshToken call using authService, do the following before the call:

let authService = this.injector.get(AuthService);

…and then proceed to use authService instead of injecting AuthService into the constructor.

We found we had to do this with the Angular Router class as well. Hope this helps!

Conclusion

The new HttpInterceptor is a new feature in Angular 4.3+ that is very powerful and useful in all applications.  As you can see, it nicely handles the OAuth refresh token seamlessly “behind the scenes” so that to the user, everything works smoothly.