Search code examples
angularlaravelangular-http-interceptorsangular-httpclient

Angular 4 JWT Http Interceptor doesn't work on chain requests


I've implemented http interceptor for new HttpClient and everything works fine, token is refreshed for single request, but if I try to access the route which lazy load data from two api's I received an error and my JWT token is blacklisted.

Laravel Backend Token Refresh Method:

public function refreshToken() {

        $token = \JWTAuth::getToken();

        if (! $token) {
            return response()->json(["error" => 'Token is invalid'], 401);
        }

        try {

            $refreshedToken = \JWTAuth::refresh($token);
            $user = \JWTAuth::setToken($refreshedToken)->toUser();

        } catch (JWTException $e) {

            return response()->json(["error" => $e->getMessage()], 400);
        }

        return response()->json(["token" => $refreshedToken, "user" => $user], 200);
    }

Angular Http Interceptor:

@Injectable()
export class RefreshTokenInterceptor implements HttpInterceptor {

    constructor(private injector: Injector) { }

    intercept(request: HttpRequest<any>, next: HttpHandler) : Observable<HttpEvent<any>> {

        return next.handle(request).catch((errorResponse: HttpErrorResponse) => {

            const error = (typeof errorResponse.error !== 'object') ? JSON.parse(errorResponse.error) : errorResponse;

            if(errorResponse.status === 401 && error.error === 'token_expired') {

                const http = this.injector.get(HttpClient);

                let token = localStorage.getItem('token');

                return http.post<any>(`${environment.apiBaseUrl}token/refresh`, {},
                    {headers: new HttpHeaders().set('Authorization', `Bearer ${token}`)})
                    .flatMap(data => {

                        localStorage.setItem('currentUser', JSON.stringify(data));
                        localStorage.setItem('token', data.token);

                        const cloneRequest = request.clone({setHeaders: {'Authorization': `Bearer ${data.token}`}});
                        return next.handle(cloneRequest);
                    });
            }

            return Observable.throw(errorResponse);
        });
    }
}

My Route which use resolvers:

{
        path: '',
        children: [ {
            path: 'create',
            component: CreateCarComponent,
            resolve: {
                subcategories: SubCategoriesResolver,
                companies: CompaniesResolver
            }
        }]
    }

Companies Resolver: (Car resolver is simmilar to this)

@Injectable()
export class CompaniesResolver implements Resolve<any> {

    constructor(private _userService: UserService) {}

    resolve(route: ActivatedRouteSnapshot) {
        return this._userService.getCompaniesList();
    }
}

User Service Method Example:

getUserCardsApi: string = "user/cars/all";

    getCardsList() :  Observable<any[]> {

        return this._http.get(environment.apiBaseUrl + this.getUserCardsApi, this.jwtHeaders())
            .catch(error => {

                return Observable.throw(error);
            });
    }

Headers:

private jwtHeaders() {

        let currentUser = JSON.parse(localStorage.getItem('currentUser'));

        return {headers: new HttpHeaders().set('Authorization', 'Bearer ' + currentUser.token)}
        }
    }

Whenever I hit routes with more than 2 resolvers, first response I receive is correct and returns a refreshed token with user object and the next one after that right away returns token blacklisted. Could you please suggest what can be the issue, I've spent too much time on solving this (

Update 1:

What I noticed is that second refresh request is passing an old token rather than a new one thats why Laravel blacklisting a token


Solution

  • Solved and perfectly works with multiple resolvers:

    export class RefreshTokenInterceptor implements HttpInterceptor {
    
    isRefreshingToken: boolean = false;
        tokenSubject: BehaviorSubject<string> = new BehaviorSubject<string>(null);
    
        constructor(private router: Router, private injector: Injector, private _loadingBar: SlimLoadingBarService) {
    
        }
    
        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, localStorage.getItem('token')))
    
                .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);
                    }
                });
        }
    
        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);
                let token = localStorage.getItem('token');
                const http = this.injector.get(HttpClient);
    
                return http.post<any>(`${environment.apiBaseUrl}token/refresh`, {},
                    {headers: new HttpHeaders().set('Authorization', `Bearer ${token}`)})
                    .switchMap((data: string) => {
    
                    if (data["token"]) {
                            this.tokenSubject.next(data["token"]);
                            return next.handle(this.addToken(req, data["token"]));
                        }
    
                        // 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(req, token));
                    });
            }
        }
    
        logoutUser() {
            // Route to the login page (implementation up to you)
            localStorage.removeItem('currentUser');
            localStorage.removeItem('token');
    
            this.router.navigate(['./auth/login']);
    
            return Observable.throw("Error Logout");
        }
    
        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);
        }