Angular 8 Intercept call to refresh token

AntGrisha picture AntGrisha · Aug 24, 2019 · Viewed 11.5k times · Source

I am trying to refresh access token if current access token is expired.

I am sending multiple requests at one time and I want to make a kind of a queue, so other requests won't request refreshing token route.

I've googled some best practises and examples and found out the following solution for Angular 6 and rxjs v6, which is using BehaviourSubject and switchMaps. (please see attached code)

However I am using Angular 8 (8.1) and rxjs v6.4 and this solution does not work for me.

It simply does not reach switchMap in this.authService.requestAccessToken().pipe. (Tested using console.log)

However if I comment return this.refreshTokenSubject.pipe and return next.handle(request) it reaches that switchMap, but my other requests are failed.

Do you know if anything has been changed or should I try doing this in another way?

  • TokenInterceptor
    import { Injectable } from '@angular/core';
    import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
    import { AuthService } from './auth.service';
    import { Observable, BehaviorSubject, Subject } from 'rxjs';
    import { switchMap, take, filter } from 'rxjs/operators';
    @Injectable()
    export class TokenInterceptor implements HttpInterceptor {
        private refreshTokenInProgress = false;
        private refreshTokenSubject: Subject<any> = new BehaviorSubject<any>(null);

        constructor(public authService: AuthService) { }
        intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

            const accessExpired = this.authService.isAccessTokenExpired();
            const refreshExpired = this.authService.isRefreshTokenExpired();

            if (accessExpired && refreshExpired) {
                return next.handle(request);
            }
            if (accessExpired && !refreshExpired) {
                if (!this.refreshTokenInProgress) {
                    this.refreshTokenInProgress = true;
                    this.refreshTokenSubject.next(null);
                    return this.authService.requestAccessToken().pipe(
                        switchMap((authResponse) => {
                            this.authService.saveToken(AuthService.TOKEN_NAME, authResponse.accessToken);
                            this.authService.saveToken(AuthService.REFRESH_TOKEN_NAME, authResponse.refreshToken);
                            this.refreshTokenInProgress = false;
                            this.refreshTokenSubject.next(authResponse.refreshToken);
                            return next.handle(this.injectToken(request));
                        }),
                    );
                } else {
                    return this.refreshTokenSubject.pipe(
                        filter(result => result !== null),
                        take(1),
                        switchMap((res) => {
                            return next.handle(this.injectToken(request))
                        })
                    );
                }
            }

            if (!accessExpired) {
                return next.handle(this.injectToken(request));
            }
        }

        injectToken(request: HttpRequest<any>) {
            const token = this.authService.getToken(AuthService.TOKEN_NAME);
            return request.clone({
                setHeaders: {
                    Authorization: `Bearer ${token}`
                }
            });
        }
    }
  • requestAccessToken
    requestAccessToken(): Observable<any> {
        const refreshToken = this.getToken(AuthService.REFRESH_TOKEN_NAME);
        return this.http.post(`${this.basePath}/auth/refresh`, { refreshToken });
    }

UPD 1

So I used these sources to write my interceptor:

UPD 2

I've excluded refresh request from interceptor scope and now it's working Thanks to @JBNizet

Answer

AntGrisha picture AntGrisha · Aug 24, 2019

I've excluded refresh request from interceptor scope and now it's working. I've made a temporary fix in order to see it's working in the fastest way.

Now my TokenInterceptor looks like:

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { AuthService } from './auth.service';
import { Observable, BehaviorSubject, Subject } from 'rxjs';
import { switchMap, take, filter } from 'rxjs/operators';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
    private refreshTokenInProgress = false;
    private refreshTokenSubject: Subject<any> = new BehaviorSubject<any>(null);

    constructor(public authService: AuthService) { }
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (request.url.indexOf('refresh') !== -1) {
            return next.handle(request);
        }

        const accessExpired = this.authService.isAccessTokenExpired();
        const refreshExpired = this.authService.isRefreshTokenExpired();

        if (accessExpired && refreshExpired) {
            return next.handle(request);
        }
        if (accessExpired && !refreshExpired) {
            if (!this.refreshTokenInProgress) {
                this.refreshTokenInProgress = true;
                this.refreshTokenSubject.next(null);
                return this.authService.requestAccessToken().pipe(
                    switchMap((authResponse) => {
                        this.authService.saveToken(AuthService.TOKEN_NAME, authResponse.accessToken);
                        this.authService.saveToken(AuthService.REFRESH_TOKEN_NAME, authResponse.refreshToken);
                        this.refreshTokenInProgress = false;
                        this.refreshTokenSubject.next(authResponse.refreshToken);
                        return next.handle(this.injectToken(request));
                    }),
                );
            } else {
                return this.refreshTokenSubject.pipe(
                    filter(result => result !== null),
                    take(1),
                    switchMap((res) => {
                        return next.handle(this.injectToken(request))
                    })
                );
            }
        }

        if (!accessExpired) {
            return next.handle(this.injectToken(request));
        }
    }

    injectToken(request: HttpRequest<any>) {
        const token = this.authService.getToken(AuthService.TOKEN_NAME);
        return request.clone({
            setHeaders: {
                Authorization: `Bearer ${token}`
            }
        });
    }
}

Thanks to @JBNizet