5

I have built an error interceptor in my first Angular application which is all new for me. The interceptor tries to refresh a Firebase authorization token when a 401 response code occures. Therefore I have written the following code:

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private authService: AuthService, private alertService: AlertService) { }

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
        catchError(err => {
            if (err.status === 401) {
                let user = localStorage.getItem('currentUser');
                if (!user) {
                    this.logout(false);
                    return throwError(err.error);
                }
                let currentUser = JSON.parse(user);
                if (!currentUser || !currentUser.stsTokenManager || !currentUser.stsTokenManager.accessToken) {
                    this.logout(false);
                    return throwError(err.error);
                }
                const reference = this;
                this.authService.getToken(currentUser, true).then(t => {
                    // How do I await and return this properly?
                    return reference.updateTokenAndRetry(request, next, currentUser, t);
                }); // Get token and refresh
            }
            this.alertService.showAlert({
                text: 'Fout tijdens het verzenden van het verzoek',
            });
            return throwError(err.error);
        })
    );
}

updateTokenAndRetry(request: HttpRequest<any>, next: HttpHandler, currentUser: any, token: string): Observable<HttpEvent<any>> {
    // Update local stored user
    currentUser.stsTokenManager.accessToken = token;
    localStorage.setItem('currentUser', JSON.stringify(currentUser));

    // Add the new token to the request
    request = request.clone({
        setHeaders: {
            Authorization: token,
        },
    });

    return next.handle(request);
}

The token gets refreshed fine. However the network call is not being executed after the refresh, which reference.updateTokenAndRetry(request, next, currentUser, t); should do.

I assume the reason for this, is that this.authService.getToken(currentUser, true) returns a Promise (this is the Firebase plugin and can't be changed). I want to return return reference.updateTokenAndRetry(request, next, currentUser, t); but this is not possible since it's in an async function block.

How can I await or return the next network call? I can't make the intercept function async. I am pretty stuck at this point.

gi097
  • 7,313
  • 3
  • 27
  • 49

2 Answers2

7

Instead of trying to return an async promise you should convert your promise to an observable using the RxJS 'from' operator as described in this post: Convert promise to observable.

This will result in a correct return type of Observable> for your interceptor.

Your code would look like something like the following (assuming you only send one request at a time):

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
        catchError(err => {
            if (err.status === 401) {
                let user = localStorage.getItem('currentUser');
                if (!user) {
                    this.logout(false);
                    return throwError(err.error);
                }
                let currentUser = JSON.parse(user);
                if (!currentUser || !currentUser.stsTokenManager || !currentUser.stsTokenManager.accessToken) {
                    this.logout(false);
                    return throwError(err.error);
                }
                // Return a newly created function here
                return this.refreshToken(currentUser, request, next);
            }
            this.alertService.showAlert({
                text: 'Fout tijdens het verzenden van het verzoek',
            });
            return throwError(err.error);
        })
    );
}

refreshToken(currentUser: any, request: any, next: any) {
    // By making use of the from operator of RxJS convert the promise to an observable
    return from(this.authService.getToken(currentUser, true)).pipe(
        switchMap(t => this.updateTokenAndRetry(request, next, currentUser, t))
    )
}

updateTokenAndRetry(request: HttpRequest<any>, next: HttpHandler, currentUser: any, token: string): Observable<HttpEvent<any>> {
    // Update local stored user
    currentUser.stsTokenManager.accessToken = token;
    localStorage.setItem('currentUser', JSON.stringify(currentUser));

    // Add the new token to the request
    request = request.clone({
        setHeaders: {
            Authorization: token,
        },
    });

    return next.handle(request);
}

Hope this helps!

Arwin
  • 181
  • 8
  • @GiovanniTerlingen This will only work well with one request at the time. When handling multiple calls and authentication errors your `refreshToken` method will be called multiple times as well. – Mark Verkiel May 29 '19 at 12:22
  • @Pilatus, interesting. What can I do against that? Feel free to add your answer. – gi097 May 29 '19 at 12:23
  • You could put the token into a behaviorsubject (this way you can still return an observable and have the latest value). Next to that you should refresh your token automatically (after x minutes, but before it expires. see https://auth0.com/docs/quickstart/spa/angular2/05-token-renewal for an code example) – Mr.wiseguy May 29 '19 at 12:35
  • @Mr.wiseguy you are correct, the only problem is when the user returns after the token expires. The requests made in the `intercepter` still have to wait for the initial `refreshtoken` call. – Mark Verkiel May 29 '19 at 12:46
1

Arwin solution works well, but only in an environment where one request is send at the time.

In order to get this to work save the refreshToken method into an Observable with the pipe share. This will allow multiple subscribers but a single result.

Wrap the next.handle(request) method in another Subject<any> and return the subject. If the request fires an error call that isn't a 401 error call subject.error.

After refreshing the token call this.updateTokenAndRetry(request, next, currentUser, token).subscribe(result => subject.next(result); to make sure that the request is returned to the initial subscriber.

Below code is pseudo code and should work in your case.

refreshTokenObservable: Observable<any>;

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

    let subject = new Subject<any>();

    next.handle(request).pipe(
        catchError(err => {
            if (err.status === 401) {
                let user = localStorage.getItem('currentUser');
                if (!user) {
                    this.logout(false);
                    subject.error(err.error);
                    return;
                }
                let currentUser = JSON.parse(user);
                if (!currentUser || !currentUser.stsTokenManager || !currentUser.stsTokenManager.accessToken) {
                    this.logout(false);
                    subject.error(err.error);
                    return;
                }
                // Return a newly created function here
                this.refreshToken(currentUser).subscribe(token => {

                    this.updateTokenAndRetry(request, next, currentUser, token).subscribe(result => subject.next(result);
                    this.refreshTokenObservable = null; // clear observable for next failed login attempt
                });
            }
            this.alertService.showAlert({
                text: 'Fout tijdens het verzenden van het verzoek',
            });
            subject.error(err.error);
        })
    ).subscribe(result => subject.next(result));

    return subject.asObservable();
}

refreshToken(currentUser: any) {

    if(this.refreshTokenObservable == null)
    {
        // By making use of the from operator of RxJS convert the promise to an observable
        this.refreshTokenObservable = from(this.authService.getToken(currentUser, true)).pipe(share());
    }

    return this.refreshTokenObservable;
}

updateTokenAndRetry(request: HttpRequest<any>, next: HttpHandler, currentUser: any, token: string): Observable<HttpEvent<any>> {
    // Update local stored user
    currentUser.stsTokenManager.accessToken = token;
    localStorage.setItem('currentUser', JSON.stringify(currentUser));

    // Add the new token to the request
    request = request.clone({
        setHeaders: {
            Authorization: token,
        },
    });

    return next.handle(request);
}
Mark Verkiel
  • 1,229
  • 10
  • 22