Angular 8 Intercept call to refresh token
Asked Answered
A

2

16

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

Akerboom answered 24/8, 2019 at 12:23 Comment(3)
My guess is that the authService sends an http request to get a refreshed token. So this http request is intercepted by this interceptor. And since a refresh is in progress, the request used to get a refreshed token can't be sent until the token has been refreshed. You shouldn't filter the requests that try to refresh the token the same way as the other requests.Marmion
@JBNizet just excluded that request and seems that everything is working now, thanks a lot, you've made my day. Sometimes we need a fresh look at our code.Akerboom
You should answer your own question with an explanation and with the code used to fix it.Marmion
A
24

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

Akerboom answered 24/8, 2019 at 12:48 Comment(7)
can you please with accessExpired how you implemented in auth serviceMcnamee
I was also facing same issue , a lot of valuable time spent in it. Finally it is working fine now after excluding the "refresh" api. Thanks for your post.Clem
Thank you for this. It helped me a lot!Launalaunce
@Akerboom sir can you please share me isAccessTokenExpired function also?Karbala
@KapilSoni, sure, so basically I just save token's expiration time to localstorage (unix format) during login. and my isAccessTokenExpired function looks like return moment().utc().isAfter(moment.unix(parseInt( localStorage.getItem('EXP_TIME'))));Akerboom
@Akerboom ok sir and what is inside isRefreshTokenExpired?Karbala
@KapilSoni basically it contained the same thing, but that was applied to saved refresh token.Akerboom
S
1

@AntGrisha , thanks for posting the solution after correcting.Saved me lots of work. I have added error handling as well.Might help some beginner like me and so posting it here.I have used angular 9.1.5 version.

Should pipe in the request handler as below.

return next.handle(this.injectToken(req)).pipe(
            catchError(this.handleError)

Error handler function definition

injectToken(request: HttpRequest<any>) {
    const token = this.authService.getToken(AuthService.TOKEN_NAME);
    return request.clone({
        setHeaders: {
            Authorization: `Bearer ${token}`
        }
    });
}
//Error handling function
handleError(error: HttpErrorResponse){
console.log('Error Occurred: '+error);
if(error.status==401){
  return throwError("Unauthorized!");
}
else{
return throwError(error);
 }
Scornful answered 21/7, 2020 at 12:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.