1

I have an interceptor in Angular that I am using to refresh a token if it is expired, but the application seems to get caught in an endless call of 401 errors to the API when the token is successfully refreshed. When I step through the code, the token does indeed refresh if expired but then tries to refresh repeatedly.

I should also note that upon clicking the button again and calling the API again, the app picks up the new token and works properly afterward. Would love to get this working without so many console errors in the first place though.

Here is the interceptor (old)

import { Injectable, Injector } from "@angular/core";
import { Router } from "@angular/router";
import {
    HttpClient,
    HttpHandler, HttpEvent, HttpInterceptor,
    HttpRequest, HttpResponse, HttpErrorResponse
} from "@angular/common/http";
import { AuthService } from "./auth.service";
import { Observable } from "rxjs/Observable";

@Injectable()
export class AuthResponseInterceptor implements HttpInterceptor {

    currentRequest: HttpRequest<any>;
    auth: AuthService;

    constructor(
        private injector: Injector,
        private router: Router
    ) { }

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

        this.auth = this.injector.get(AuthService);
        var token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null;

        if (token) {
            // save current request
            this.currentRequest = request;

            return next.handle(request)
                .do((event: HttpEvent<any>) => {
                    if (event instanceof HttpResponse) {
                        // do nothing
                    }
                })
                .catch(error => {
                    return this.handleError(error)
                });
        }
        else {
            return next.handle(request);
        }
    }

    handleError(err: any) {
        if (err instanceof HttpErrorResponse) {
            if (err.status === 401) {
                // JWT token might be expired:
                // try to get a new one using refresh token
                console.log("Token expired. Attempting refresh...");
                this.auth.refreshToken()
                    .subscribe(res => {
                        if (res) {
                            // refresh token successful
                            console.log("refresh token successful");

                            // re-submit the failed request
                            var http = this.injector.get(HttpClient);
                            http.request(this.currentRequest).subscribe(
                                (result: any) => {
                                    console.log(this.currentRequest);
                                }, (error: any) => console.error(error)
                            );
                        }
                        else {
                            // refresh token failed
                            console.log("refresh token failed");

                            // erase current token
                            this.auth.logout();

                            // redirect to login page
                            this.router.navigate(["login"]);
                        }
                    }, error => console.log(error));
            }
        }
        return Observable.throw(err);
    }
}

EDIT: Updated code to working solution

import { Injectable, Injector } from "@angular/core";
import { Router } from "@angular/router";
import {
    HttpClient,
    HttpHandler, HttpEvent, HttpInterceptor,
    HttpRequest, HttpResponse, HttpErrorResponse, HttpHeaders
} from "@angular/common/http";
import { AuthService } from "./auth.service";
import { Observable, Subject } from "rxjs";

@Injectable()
export class AuthResponseInterceptor implements HttpInterceptor {


    auth: AuthService;
    currentRequest: HttpRequest<any>;


    constructor(
        private injector: Injector,
        private router: Router
    ) { }


    logout() {
        this.auth.logout();
        this.router.navigate(["login"]);
    }


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


        this.auth = this.injector.get(AuthService);
        let token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null;




        this.currentRequest = request;



        return next.handle(request).
            catch((error) => {
                if (error instanceof HttpErrorResponse && error.status === 401) {
                    return this.auth.refreshToken()

                        .switchMap(() => {
                            let token = (Response) ? this.auth.getAuth() : null;
                            console.log(token);

                            if (token) {
                                this.currentRequest = request.clone({
                                    setHeaders: {
                                        Authorization: `Bearer ${token.token}`
                                    }
                                });
                            }

                            return next.handle(this.currentRequest);

                        }).
                        catch((e) => {
                            this.logout();
                            console.error(e);
                            return Observable.empty();
                        });
                }

                return Observable.throw(error);

            });
    }


}

Auth.service

constructor(private http: HttpClient,
        @Inject(PLATFORM_ID) private platformId: any) {
    }

    // performs the login
    login(username: string, password: string): Observable<boolean> {
        var url = "api/token/auth";
        var data = {
            username: username,
            password: password,
            client_id: this.clientId,
            // required when signing up with username/password
            grant_type: "password",
            // space-separated list of scopes for which the token is issued
            scope: "offline_access profile email"
        };

        return this.getAuthFromServer(url, data);
    }

    // try to refresh token
    refreshToken(): Observable<boolean> {
        var url = "api/token/auth";
        var data = {
            client_id: this.clientId,
            // required when signing up with username/password
            grant_type: "refresh_token",
            refresh_token: this.getAuth()!.refresh_token,
            // space-separated list of scopes for which the token is issued
            scope: "offline_access profile email"
        };

        return this.getAuthFromServer(url, data);
    }

    // retrieve the access & refresh tokens from the server
    getAuthFromServer(url: string, data: any): Observable<boolean> {
        return this.http.post<TokenResponse>(url, data)
            .map((res) => {
                let token = res && res.token;
                // if the token is there, login has been successful
                if (token) {
                    // store username and jwt token
                    this.setAuth(res);
                    // successful login
                    return true;
                }

                // failed login
                return Observable.throw('Unauthorized');
            })
            .catch(error => {
                return new Observable<any>(error);
            });
    }

    // performs the logout
    logout(): boolean {
        this.setAuth(null);
        return true;
    }

    // Persist auth into localStorage or removes it if a NULL argument is given
    setAuth(auth: TokenResponse | null): boolean {
        if (isPlatformBrowser(this.platformId)) {
            if (auth) {
                localStorage.setItem(
                    this.authKey,
                    JSON.stringify(auth));
            }
            else {
                localStorage.removeItem(this.authKey);
            }
        }
        return true;
    }

    // Retrieves the auth JSON object (or NULL if none)
    getAuth(): TokenResponse | null {
        if (isPlatformBrowser(this.platformId)) {
            var i = localStorage.getItem(this.authKey);
            if (i) {
                return JSON.parse(i);
            }
        }
        return null;
    }

    // Returns TRUE if the user is logged in, FALSE otherwise.
    isLoggedIn(): boolean {
        if (isPlatformBrowser(this.platformId)) {
            return localStorage.getItem(this.authKey) != null;
        }
        return false;
    }
lord_skootle
  • 13
  • 1
  • 5
  • In your interceptor you must return always a next.handle, not an observable. So you can not subscribe to refreshToken. Use a switchMap instead. You can see a generic interceptor, e.g. https://stackoverflow.com/questions/47417899/angular-4-and-oauth-intercept-401-responses-refresh-the-access-token-and-retr/47420967#47420967 – Eliseo Dec 20 '17 at 08:13
  • Thanks - I checked out your example but I am not quite sure how to move over my logic in the handleError to the main interceptor block. Would switchMap be used instead of "do..."? Apologies - would be grateful for anymore clarification. – lord_skootle Dec 20 '17 at 09:47
  • do NOT change the request. do is used generally when we want to register a log,e.g. or save the data in a variable to make a "cache". switchMap change a request to another request. – Eliseo Dec 20 '17 at 10:08

2 Answers2

3
return this.auth.refreshToken(response:any)
        //response can be true or null
        let token=(response)?this.auth.getAuth():null;
        //In token we have an object of type TokenResponse
        console.log(token)
        .switchMap(() => {
            if (token) {
               this.currentRequest = request.clone({
                    setHeaders: {  //I think it's toke.token
                         Authorization: `Bearer ${token.token}`
                      }
               });
....

NOTE: Try to change "var" for "let" NOTE2: At first you have

var token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null;
//    May be  remove "!"?
let  token = (this.auth.isLoggedIn()) ? this.auth.getAuth().token : null;
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • Awesome, it worked! Thank you so much for your patience and help! Marking as answer - I will also update my code with the working solution. – lord_skootle Dec 22 '17 at 09:14
  • I glad to help you, but check the "!" at first your interceptor when you write let token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null; – Eliseo Dec 22 '17 at 09:39
  • Actually Typescript compiler will throw an error if I do that. Any reason why I should remove it? https://stackoverflow.com/questions/38874928/operator-in-typescript-after-object-method – lord_skootle Dec 23 '17 at 05:06
  • Great! I didn't know about "Non-null assertion operator". I like learn some new :) – Eliseo Dec 23 '17 at 10:56
1

If you want separate the error handle you can do some like

handleError(err: any) { 
    if (err instanceof HttpErrorResponse) {
        if (err.status === 401) {
            this.auth.refreshToken()
                .switchMap(res=>{  //<--switchMap, not susbcribe
                    if (res) {
                        console.log("refresh token successful");

                        // re-submit the failed request
                        var http = this.injector.get(HttpClient);
                        //Modify there this.currentRequest if was neccesary

                        return next.handle(this.currentRequest).catch(error:any=>
                        {
                            console.error(error);
                            return Observable.throw(error);
                        });
                    }
                    else {
                        console.log("refresh token failed");
                        this.auth.logout();
                        this.router.navigate(["login"]);
                    }
                })
        }
    }
    return Observable.throw(err);
}
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • Thank you, @Eliseo - I have changed my code but am still running into an issue as I explain above. It is almost as though the token gets refreshed but then when it re-runs the request it comes back unauthorized the second time around. The third time (when I click the button), it works fine. – lord_skootle Dec 21 '17 at 10:14
  • try make the header in two steps:1.-let httpHeaders = new HttpHeaders() .set('Authorization', 'Bearer '+token) 2.-const authReq = req.clone({ headers: httpHeaders }); – Eliseo Dec 21 '17 at 11:59
  • Yikes - this is getting puzzling! I went ahead and took your advice as well as changed SwitchMap(token) to SwitchMap(res) because it was passing the boolean to where the string should be and while it says the token refreshes successfully, it still returns a 401 after the last return.handle(). I've updated the edited code in my original post – lord_skootle Dec 21 '17 at 23:02
  • @lord_skootle, after you refresh the token, you go on sending the SAME token (the variable not change). you should send this.auth.token or whatever you get the token – Eliseo Dec 21 '17 at 23:47
  • I see. I've updated the code again - do you mind pointing out where that needs to happen? I have a refreshToken method that gets the new token in my auth service so I'm not sure what I'm missing. @Eliseo – lord_skootle Dec 22 '17 at 00:21
  • Can you show your auth.refreshToken function? What this function do? Where save the new token? How we reach this new token? – Eliseo Dec 22 '17 at 00:25
  • Sure thing. I've posted it above. – lord_skootle Dec 22 '17 at 00:40