最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - JWT expired token handling with Angular Interceptor keeps refreshing - Stack Overflow

programmeradmin2浏览0评论

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/mon/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/mon/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;
    }

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/mon/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/mon/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;
    }
Share Improve this question edited Dec 22, 2017 at 9:16 lord_skootle asked Dec 20, 2017 at 8:03 lord_skootlelord_skootle 131 silver badge5 bronze badges 3
  • 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. stackoverflow./questions/47417899/… – Eliseo Commented Dec 20, 2017 at 8: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 Commented Dec 20, 2017 at 9: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 Commented Dec 20, 2017 at 10:08
Add a ment  | 

2 Answers 2

Reset to default 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;

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);
}
发布评论

评论列表(0)

  1. 暂无评论