Refreshing Authorization Tokens – Angular 6

In this post, we are going to build a http interceptor for refreshing authorization tokens once expired. The idea here is to be able to intercept http requests, attach an authorization header to the request. And to intercept http response, check for authentication errors and refresh tokens when necessary, otherwise redirect to the login page.

Once the token has been refreshed successfully, you should resend all intercepted HTTP responses back to their origin, and only return a non auth related error back to the end user. This whole process should occur smoothly without breaking the UX if successful. We should only interrupt the user when action is needed from them – such as log in in this case.

Getting Started

Let’s first start by creating a HTTP Interceptor class, then adding it to our module (app.module.ts).

ng generate class http-auth-interceptor

Then, open the http interceptor and make the following modifications to it:

@Injectable()
export class HttpAuthInterceptor implements HttpInterceptor {
   intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {}
}

First, we made the class injectable. Then, we implemented the HTTP Interceptor Interface, by one, adding implements HttpInterceptor. And two, by adding the intercept method – the method that will intercept and return the modified requests. Most of the action will take place inside this method.

Next, we need to add the newly created http interceptor class to the list of providers, in our module:

@NgModule({
  imports: [CommonModule, MatDialogModule],
  declarations: [],
  providers: [
    AuthService,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpAuthInterceptor,
      multi: true
    }
  ]
})

Intercepting HTTP Requests (Adding Authorization Header)

Here, will are going to take the intercepted request, clone it, modify the copied http requests and return it as the new request.

// Clone the request and authorization header
const authReq = req.clone({
  headers: req.headers.set('authorization', Authorization ? Authorization : '')
});

NB: You can attach the header based on the authorization method you are using on your server. Example: Bearer Authorization.

After that, you can return the modified request which replaces the original request:

return next.handle(authReq)

Refreshing Authorization Tokens

Intercepting Expired Tokens Request

To refresh tokens, we need to monitor the responses looking for http status code 401 for unauthorized request. So, we are going to pipe our modified http request, and catch all errors. Next, we are going to check for http status code 401. Whenever we catch an authentication error, we are going to attempt and refresh our token.

return next.handle(authReq).pipe(
   catchError(error => {
       // checks if a url is to an admin api or not
       if (error.status === 401) {
          // attempting to refresh our token
       }
   }
});

We also need to avoid sending multiple refresh requests to our endpoint. This happens when you send multiple http requests simultaneously, and all of them return an authentication error. So, we are going to share (using Share Operator) the existing refresh request across all intercepted http responses that find a token refresh request already inflight.

Basically, when we catch a response with http status code 401, we are going to check if there is an inflight request to refresh our token, then hitch on it. If none exists, we are going to send a new refresh request.

In our http interceptor class, we need to and inflightAuthRequest property and set it to null. Then, when we get a http response error 401, we are going to check whether it is null.

if (!this.inflightAuthRequest) {
  this.inflightAuthRequest = authService.refreshToken();

  if (!this.inflightAuthRequest) {
     // remove existing tokens
     localStorage.clear();
     this.router.navigate(['/sign-page']);
     return throwError(error);
   }
}

If not, we are going to pipe the existing request and attach our request using SwitchMap operator. After the token has been refreshed successfully, we are going to resend each of our http request (that responded with 401) back to the server, hoping to get another response not related to authentication.

return this.inflightAuthRequest.pipe(
  switchMap((newToken: string) => {
    // unset inflight request
    this.inflightAuthRequest = null;

    // clone the original request
    const authReqRepeat = req.clone({
      headers: req.headers.set('', newToken)
    });

    // resend the request
    return next.handle(authReqRepeat);
  })
);

Our Refresh Method

In our refresh method, we are just going to be making a http request to our refresh token endpoint. This is also where we will add the RXJS Share Operator, ensuring that only one request is sent at a time:

refreshToken(): Observable<string> {

    const url = 'url to refresh token here';

    // append refresh token if you have one
    const refreshToken = localStorage.getItem('refreshToken');

    return this.http
      .get(url, {
        headers: new HttpHeaders().set('refreshToken', refreshToken),
        observe: 'response'
      })
      .pipe(
        share(), // <========== YOU HAVE TO SHARE THIS OBSERVABLE TO AVOID MULTIPLE REQUEST BEING SENT SIMULTANEOUSLY
        map(res => {
          const token = res.headers.get('token');
          const newRefreshToken = res.headers.get('refreshToken');

          // store the new tokens
          localStorage.setItem('refreshToken', newRefreshToken);
          localStorage.setItem('token', token);
          return token;
       })
    );
}

Whitelist and Blacklist Requests

What we would like to achieve in this section is a simple way to exempt some request from going through a http interceptor. There are several ways used to achieve this. One common way is to attach a header to requests which you do not want to be modified by the interceptor. Then, check if the header is present at the beginning of the interceptor method. And if present, return an unmodified http request:

if (req.headers.get('authExempt') === 'true') {
  return next.handle(req);
}

This is functional but requires you to manually add the header on the requests you are blacklisting. The other method which I consider to be much better, is to have a blacklist containing URLs or URL regex patterns. The idea being that if a URL is on the list, then you can exempt it from being modified by the http interceptor. For instance, you can simply exempt an entire sub directory using a simple regex:

(((https?):\/\/|www\.)theinfogrid.com\/auth\/)

The above regex exempts all subdirectories with auth directory at the root. So, you can check if your URL is in the blacklist or not:

blacklistCheckup($url: string): boolean {

   let returnValue = false;

   for (const i of Object.keys(this.blacklist)) {
      if (this.blacklist[i].exec($url) !== null) {
        returnValue = true;
        break;
      }
   }

   return returnValue;
}

 

This gives you the freedom to blacklist or whitelist an entire domain/subdomain, without needing to attach a header to the request manually. You can then package it as an object and iterate through it. I am assuming, this should be a small list, because if it becomes too large, it will impact the performance of your app.

Tips

  1. You might want to consider attaching a header to the response originating from the refresh token endpoint. Then, check if the response contains the header before sending another refresh request. And if it does, and has status code 401, it means that the token was not refreshed successfully. Thus, you should redirect the user to the login page instead of sending another refresh token, which could lead into a loop of some sort.
if (error.status === 401) {
  // check if the response is from the token refresh end point
  const isFromRefreshTokenEndpoint = !!error.headers.get(
    'unableToRefreshToken'
  );

  if (isFromRefreshTokenEndpoint) {
    localStorage.clear();
    this.router.navigate(['/sign-page']);
    return throwError(error);
}
  1. To work around the Cyclic Dependency Error, do not inject your AuthService inside the constructor. Instead, inject the injector class and then use it inside the interceptor method to inject AuthService into a variable. The constructor for our http interceptor
constructor(private injector: Injector, ...) {}

And then inside our method:

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

Get this Code

You can get the complete code here.

18 Replies to “Refreshing Authorization Tokens – Angular 6”

  1. This post is helpful to me very much.
    I have a question to ask about Tip2 (To work around the Cyclic Dependency Error)
    Just seeing your project, in fact, I don’t know why this issue would happen in your code?
    Could you explain this to me? thanks

    1. Cyclic Dependency error occurs when you inject let’s Service A in to Service B and Service B in to Service A. While not common, it sometimes occurs especially when implementing a HTTP Interceptor. This is why I suggested Tip 2 incase someone came across it, then they would know how to handle it using the Injector to get the dependency instead of directly injecting the dependency through the constructor. If you haven’t come across it, then it’s common, but if you do that’s the quickest solution.

  2. HI, thanks for getting back to me!. I found the error just minutes ago… or I think at least I did. In the blacklist I was skipping the call to my token endpoint, so the call was never done. I’ve managed to skip that check for the login endpont and the token endpoint and it works. And indeed Im getting a correct token, when it’s expired it refreshes the token, other calls wait for the regresh operation and then continue to use the fresh token!. Seems all is good. I do want to confirm if I’m right. What to yo think? does this sound right to you?
    blacklist: object = [
    /\/assets/, << some i18n files are coming trhu xhr so I figured this would be ok
    /\/login/, << seems reasonable since you don’t have any tokens before logging in
    /\/token/ << seems weird to bypass this endpoint to get the token, but maybe my 10-hour streak is getting to me
    ];
    I’m sorry if I’m making a silly mistake, I’m extermely new to this.
    Thanks again.

    1. Assets login and token, since you can attach the authorizations token and monitor their response right at the originating method. if you are loading files using XHR, then assets is also necessary.

      1. Thanks!
        I got it working. However, I noticed that after I get kicked out by an expired refreshToken, I’m unable to login again.
        I had to the the following to get it working:
        if (isFromRefreshTokenEndpoint) {
        localStorage.clear();
        this.router.navigate([‘/login’]);
        this.inflightAuthRequest = null; <<<< seems that I need to reset this to null, otherwise its values lingers
        return throwError(error);
        }

        if (!this.inflightAuthRequest) {
        this.inflightAuthRequest = authService.refreshToken();

        if (!this.inflightAuthRequest) {
        // remove existing tokens
        localStorage.clear();
        this.router.navigate(['/login']);
        this.inflightAuthRequest = null; <<<< here as well
        ....

        is this correct? am I misssing something? did I configure the the interceptor the wrong way?

        Thanks again for your help. I really appreciate it.

        1. Hi, to make it easier to distinguish between refresh request and login required, I like to use two https code, 401 indicates the user needs to login and then a custom 419 to indicate, token has expired. Then when you receive error code 401, just clear the token and show login form otherwise send a refresh request. Would it be possible for you to share the whole class, so I can have a look, a gist may be?

          1. Hi, thanks for getting back to me. Unfortunately I don’t have access to the api so I can’t make the response code different. However, what I’ve noticed is that somehow the inflightAuthRequest var stays set, so

            
            if (!this.inflightAuthRequest) {
              this.inflightAuthRequest = authService.refreshToken();
            
              if (!this.inflightAuthRequest) {
                 // remove existing tokens
                 localStorage.clear();
                 ....... 
              }
            }
            

            never ran again, what I’ve done to make it work is to un-set it in:

            
            if (isFromRefreshTokenEndpoint) {
                ....
                this.inflightAuthRequest = null;
                ....
            }
            
            

            I’ve talked to the owner of the app and the api and we arranged that we can assume, for this particular domain, that a token is expired and the refresh token is expired looking at the expiration timestamp on the jwt itself – they won’t accept changes in the api – . That way I can determine the value of ‘isFromRefreshTokenEndpoint’

            How does it sound? seems like unsetting that value did the trick for me, it’s been working solid for almost two weeks

            Thanks again.-

          2. Yes, you have to unset the inflightAuthRequest otherwise the value stays around even after refresh request is done. Which is why it is set to null once refresh request is completed successfully.

            return this.inflightAuthRequest.pipe(
              switchMap((newToken: string) => {
                // unset inflight request
                this.inflightAuthRequest = null;
            
                // clone the original request
                const authReqRepeat = req.clone({
                  headers: req.headers.set('', newToken)
                });
            
                // resend the request
                return next.handle(authReqRepeat);
              })
            );
            

            Secondly, you have isFromRefreshTokenEndpoint, is it a method variable or a class property? If you have no access to the API, then you should exempt the refresh url from being intercepted, using a blacklist. Then handle all errors related to refresh token at the service that you initiated and only resend the request once it has successfully refreshed.

          3. Hi,
            isFromRefreshTokenEndpoint is local to the catchError block. I took that from your “tips” section. I’ve already defined the urls which should bypass auth. You’ve already clarified that to me. I’m so sorry, the threads I think I made a mess with the message threads.

            Thanks again for all the help!

  3. Hi, this is an enlightening tutorial. Thanks a million.
    However, once I get a 401, my app doesn’t make any subsequent calls, even the one to refresh the token, what I’m doing wrong? I’m super-green on reactive programming and Angular.
    Thanks in advance

  4. why you have two if statement for the same var ?

    if (!this.inflightAuthRequest) {
    this.inflightAuthRequest = authService.refreshToken();

    if (!this.inflightAuthRequest) {
    // remove existing tokens
    localStorage.clear();
    this.router.navigate([‘/sign-page’]);
    return throwError(error);
    }
    }

    1. Just checking if the inflightAuthRequest was set successfully, after calling refreshToken() method. Let’s say, when you call refreshToken() method and fail, you return either false or null, you have to check for that before subscribing to it.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.