Intercept and refresh token in Angular for Drupal 8 backend

Photo by Praveesh Palakeel on Unsplash
Share this

The chapter is about an interceptor who wants a token to let you get in. Sadly your token has expired, and the only way to go through the interceptor is showing it a refresh token, something like the bracelet you get at the entrance of the nightclub.

Let’s go back to the code, I will try to explain step by step about all this, you can check the GitHub project, and also, the post where I got all the reference, here: How to build an HTTP Interceptor in Angular 5

I’ve been talking about the logic behind the project, how to configure the Drupal site with the simple OAuth module and how to test it with Postman. Also, for secure apps, this is not the only method, you can protect your route with guards like I explained. And last but not least I have an easier example of how to intercept a request from angular and add a header.

So, now, we will forget all that and jump right to the request.interceptor.ts, let's study it step by step.

Imports: 

import { Injectable } from '@angular/core';

import { HttpClient, HttpInterceptor, HttpRequest, HttpHandler, HttpSentEvent, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpUserEvent, HttpErrorResponse } from "@angular/common/http";

import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/finally';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/take';

import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from "rxjs/BehaviorSubject";

import { RefreshTokenService } from '../services/refresh-token.service';
import { AuthService } from '../services/auth.service';
import { LoginService } from '../services/login.service'

From our imports, we have some obvious parts, like the injectable or the observables, more surprising once like the behavior subject, but I talk about that before.

Now, let's focus on all the imports from the angular/common/http. I am calling an entire army for managing the HTTP request, intercepting it, sending events, getting responses, etc. 

Then, I call different operators from the RXJS library, they will help me a lot. 

Also, there are my services, One for refresh the token, another one to get the token for including it in the header, and the login.service for logout. 

// HttpInterceptor interface. 
export class RequestInterceptor implements HttpInterceptor {
    isRefreshingToken: boolean = false;
    tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);
    constructor(private authService: AuthService, public login: LoginService, private refreshToken: RefreshTokenService) {}
    // addToken will add the Bearer token to the Authorization header
    addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
        return req.clone({ setHeaders: { Authorization: 'Bearer ' + token }})
    }
    // 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.
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any>> {
        // 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
        console.log('here we go');
        return next.handle(this.addToken(req, this.authService.getToken()))        
          .catch(error => {
             if (error instanceof HttpErrorResponse) {
                     switch ((<HttpErrorResponse>error).status) {
                         case 401:
                              console.log('error 401'); 
                             return this.handle401Error(error);
                         case 403:
                             console.log('error 403');
                             return this.handle403Error(req, next);
                     }
             } else {
               return Observable.throw(error);
            }
          });
    }
isRefreshingToken: boolean = false;
The conditional we use later, now, it is set false, which means that the token is updated, see we don't need too much here. 
tokenSubject: BehaviorSubject<string> = 
      new BehaviorSubject<string>(null);
As behavior subject, This variable is connected, and know all the time the status of our token. 
constructor();
We declare our services here for use them later. 
addToken();
It needs an httprequest and a token, we will give all that in the return of the interceptor.
intercept(req:HttpRequest<any>, next: HttpHandler);
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.
return next.handle(
   this.addToken(
      req, this.authService.getToken()
   )
)  

Here we inject the token in the header after intercept it with the addToken() function. 
 
.catch(error => {});
With the operator catch, we send the 403 error to refresh token and the 401 to log out. 


Let's handle the 403 forbidden request 

// The code to handle the 403 error is the most important.
        handle403Error(req: HttpRequest<any>, next: HttpHandler) {
            // If isRefreshingToken is false (which it is by default) we will 
            // enter the code section that calls authService.refreshToken
            if (!this.isRefreshingToken) { 
                // Immediately set isRefreshingToken to true so no more calls 
                // come in and call refreshToken again – which we don’t want of course
                this.isRefreshingToken = true; 

                // Reset here so that the following requests wait until the token
                // comes back from the refreshToken call.
                this.tokenSubject.next(null);
                // Call authService.refreshToken (this is an Observable that will be returned)
                return this.refreshToken.refreshToken()
                    .switchMap((newToken: string) => {
                        if (newToken) {
                            // 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
                            this.tokenSubject.next(newToken);
                            // Return next.handle using the new token
                            return next.handle(this.addToken(req, newToken));
                        }

                        // If we don't get a new token, we are in trouble so logout.
                        return this.login.logout();
                    })
                    .catch(error => {
                        // If there is an exception calling 'refreshToken', bad news so logout.
                        return this.login.logout();
                    })
                    .finally(() => {
                        // 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
                        this.isRefreshingToken = false;
                    });
            // 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                
            }
            // If isRefreshingToken is true, we will wait until tokenSubject has a non-null value 
            // – which means the new token is ready 
            else {

                return this.tokenSubject
                    .filter(token => token != null)
                    // Only take 1 here to avoid returning two – which will cancel the request
                    .take(1)
                    .switchMap(token => {
                        // When the token is available, return the next.handle of the new request
                        return next.handle(this.addToken(req, token));
                    });
            }
        }

The first part of this function we already know, with the boolean from isRefreshingToken we know if we are doing the business, with the TokenSubject,  we can tell the token the actual value. 

The return this.refreshToken.refreshToken() is calling our refresh token service, that does this: 

 refreshToken(): Observable<string> {
    let refreshAuth = this.auth.getrefreshToken(); //get refresh token from storage
    let url: string = this.mainUrl + "oauth/token";
    console.log ('refresh ' + JSON.stringify(refreshAuth));

    let body= new FormData();  	  
    body.append("grant_type", "refresh_token");
    body.append("refresh_token", refreshAuth)
    body.append("client_id", this.client_id);
    body.append("client_secret", this.client_secret);
    
    return this.http.post(url, body)      
		.map((token: Token) => {      		
			localStorage.setItem('token', JSON.stringify(token.access_token));
      localStorage.setItem('refresh_token', JSON.stringify(token.refresh_token));
    	console.log (JSON.stringify ('new token from the service ' + token.refresh_token));
      	return token.access_token;
		});
  }

Here, we create a new Body as a formData with variables from our environment.ts file. Then, the important part here is our new items in the local storage, new tokens ready!. 

Back to the request interceptor, the switch map is making an amazing job, by returning this:

 return next.handle(this.addToken(req, newToken));

In that line, we are sending our previous request with a new authenticated token. 

Handle 401 error. 

handle401Error(error) {         
            if (error && error.status === 401 || 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.
                console.log('inside the conditional');
                return this.login.logout();
            }

            return Observable.throw(error);
        }

The 401 will happen if the period of time for our refresh token is over, remember that we can easily configure that On Drupal, by going to http://mysite.com/admin/config/people/simple_oauth. 

refresh authentication token

So, that is it!

Remember to test your development with Postman, one of the most common mistakes are understanding what the hell Drupal wants, and also, review the logic behind all the project, that is the main key for this. 

I will work in an other project, traying to storage cookies with an Angular Frontend, and save them in user fields in a Drupal site. Then, if the user with the same cookie comes again, I will try to show him specific content. Sounds interesting?. 

Any feedback is welcome! bye!

Add new comment

The content of this field is kept private and will not be shown publicly.

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.