1

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

hxdef
  • 393
  • 2
  • 6
  • 15
  • try use switchMap, not flatMap (switchMap "switch" the response) – Eliseo Dec 11 '17 at 11:29
  • @Eliseo no difference – hxdef Dec 11 '17 at 14:27
  • You received error.error === 'token_expired' or only error.Status===401? what do you received from http.post(`${environment.apiBaseUrl}token/refresh`, {}, {headers: new HttpHeaders().set('Authorization', `Bearer ${token}`)}) – Eliseo Dec 11 '17 at 15:49
  • @Eliseo ok on first response I receive a new token with user object, which if fine, on second response I receive error: Token is blacklisted :( I assume that request to second route in chain should use the new token, but somehow it passes old token thats why its blacklisted, but where to find it I dont know, tried so many things already ( – hxdef Dec 11 '17 at 16:00

3 Answers3

0

It's some "strange" the way you inject the headers, try:

let httpHeaders = new HttpHeaders()
              .set('Authorization', `Bearer ${data.token}`)
            const cloneRequest = request.clone({ headers: httpHeaders });
            return next.handle(cloneRequest );
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • There is nothing strange, you can read more in documentation regarding httpClient, but anyway thanks. Haven't resolved ( – hxdef Dec 11 '17 at 19:21
  • sorry, I don't understand when you write: request.clone({setHeaders: {'Authorization': `Bearer ${data.token}`}}); -I must be read more :( - – Eliseo Dec 11 '17 at 19:26
0

My last attempt. Try check in a navigator if headers changes :(

if(errorResponse.status === 401 && error.error === 'token_expired') {
      const http = this.injector.get(HttpClient);
      let token = localStorage.getItem('token');
      return http.post(`${environment.apiBaseUrl}token/refresh`, {},
          {headers: new HttpHeaders().set('Authorization', `Bearer ${token}`)})
                 .switchMap(data => {
                        localStorage.setItem('currentUser', JSON.stringify(data));
                        localStorage.setItem('token', data.token);
                        const cloneRequest = request.clone(
                          {headers: new HttpHeaders()
                              .set('Authorization', `Bearer ${data.token}`)
                          });
                        return next.handle(cloneRequest);
                    });
            }
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • No bro ( the issue remains the same. I also haven’t found any guides on this. I don’t believe people not using interceptor with resolvers(((((( – hxdef Dec 12 '17 at 09:19
  • sometime ago I wrote this https://stackoverflow.com/questions/47417899/angular-4-and-oauth-intercept-401-responses-refresh-the-access-token-and-retr. as a general method (not using JWTAuth) Sure you have the new Rjxs and good luck. Sorry, I surrender :( – Eliseo Dec 12 '17 at 10:31
  • Thanks will look into it. – hxdef Dec 12 '17 at 10:37
0

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);
    }
hxdef
  • 393
  • 2
  • 6
  • 15