6

I am trying to implement a catch for 401 responses and tried obtaining a refresh token based on Angular 4 Interceptor retry requests after token refresh. I was trying to implement the same thing, but I never was able to Retry that request, and I am really not sure if that is the best approach to apply the refresh token strategy. Here is my code:

@Injectable() export class AuthInterceptorService implements HttpInterceptor { public authService; refreshTokenInProgress = false; tokenRefreshedSource = new Subject(); tokenRefreshed$ = this.tokenRefreshedSource.asObservable(); constructor(private router: Router, private injector: Injector) { } authenticateRequest(req: HttpRequest<any>) { const token = this.authService.getToken(); if (token != null) { return req.clone({ headers: req.headers.set('Authorization', `Bearer ${token.access_token}`) }); } else { return null; } } refreshToken() { if (this.refreshTokenInProgress) { return new Observable(observer => { this.tokenRefreshed$.subscribe(() => { observer.next(); observer.complete(); }); }); } else { this.refreshTokenInProgress = true; return this.authService.refreshToken() .do(() => { this.refreshTokenInProgress = false; this.tokenRefreshedSource.next(); }).catch( (error) => { console.log(error); } ); } } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { this.authService = this.injector.get(AuthenticationService); request = this.authenticateRequest(request); return next.handle(request).do((event: HttpEvent<any>) => { if (event instanceof HttpResponse) { // do stuff with response if you want } }, (err: any) => { if (err instanceof HttpErrorResponse) { if (err.status === 401) { return this.refreshToken() .switchMap(() => { request = this.authenticateRequest(request); console.log('*Repeating httpRequest*', request); return next.handle(request); }) .catch(() => { return Observable.empty(); }); } } }); } } 

The issue is that SwitchMap is never reached in...

if (err.status === 401) { return this.refreshToken() .switchMap(() => { 

and the do operator as well...

return this.authService.refreshToken() .do(() => { 

so that took me to my authService refreshToken method...

refreshToken() { let refreshToken = this.getToken(); refreshToken.grant_type = 'refresh_token'; refreshToken.clientId = environment.appSettings.clientId; return this.apiHelper.httpPost(url, refreshToken, null) .map ( response => { this.setToken(response.data, refreshToken.email); return this.getToken(); } ).catch(error => { return Observable.throw('Please insert credentials'); }); } } 

It returns a mapped observable, and I know it needs a subscription if I replaced the do in...

return this.authService.refreshToken() .do(() => { 

With subscribe I'll break the observable chain I guess. I am lost and I've playing with this for a long time without a solution. :D

3
  • Can you try this: stackoverflow.com/questions/47797180/… Commented Dec 13, 2017 at 16:05
  • Thanks a lot that really worked.... I had to do some changes to fit my solution but in general it works like charm.. this is really great Commented Dec 17, 2017 at 9:39
  • can you post the answer so I can mark it as an answer?? Commented Dec 17, 2017 at 9:40

2 Answers 2

5

I'm glad that you like my solution. I'm going to put just the final solution here but if anybody wants to know the process that I fallowed go here: Refresh Token OAuth Authentication Angular 4+

Ok, First I created a Service to save the state of the refresh token request and Observable to know when the request is done.

This is my Service:

@Injectable() export class RefreshTokenService { public processing: boolean = false; public storage: Subject<any> = new Subject<any>(); public publish(value: any) { this.storage.next(value); } } 

I noticed that It was better if I have two Interceptors one to refresh the token and handle that and one to put the Authorization Header if exist.

This the Interceptor for Refresh the Token:

@Injectable() export class RefreshTokenInterceptor implements HttpInterceptor { constructor(private injector: Injector, private tokenService: RefreshTokenService) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const auth = this.injector.get(OAuthService); if (!auth.hasAuthorization() && auth.hasAuthorizationRefresh() && !this.tokenService.processing && request.url !== AUTHORIZE_URL) { this.tokenService.processing = true; return auth.refreshToken().flatMap( (res: any) => { auth.saveTokens(res); this.tokenService.publish(res); this.tokenService.processing = false; return next.handle(request); } ).catch(() => { this.tokenService.publish({}); this.tokenService.processing = false; return next.handle(request); }); } else if (request.url === AUTHORIZE_URL) { return next.handle(request); } if (this.tokenService.processing) { return this.tokenService.storage.flatMap( () => { return next.handle(request); } ); } else { return next.handle(request); } } } 

So here I'm waiting to the refresh token to be available or fails and then I release the request that needs the Authorization Header.

This is the Interceptor to put the Authorization Header:

@Injectable() export class TokenInterceptor implements HttpInterceptor { constructor(private injector: Injector) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const auth = this.injector.get(OAuthService); let req = request; if (auth.hasAuthorization()) { req = request.clone({ headers: request.headers.set('Authorization', auth.getHeaderAuthorization()) }); } return next.handle(req).do( () => {}, (error: any) => { if (error instanceof HttpErrorResponse) { if (error.status === 401) { auth.logOut(); } } }); } } 

And my main module is something like this:

@NgModule({ imports: [ ..., HttpClientModule ], declarations: [ ... ], providers: [ ... OAuthService, AuthService, RefreshTokenService, { provide: HTTP_INTERCEPTORS, useClass: RefreshTokenInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true } ], bootstrap: [AppComponent] }) export class AppModule { } 

Please any feedback will be welcome and if I'm doning something wrong tell me. I'm testing with Angular 4.4.6 but I don't know if it work on angular 5, I think should work.

Sign up to request clarification or add additional context in comments.

6 Comments

Won't your auth.logOut(); in the TokenInterceptor simply log you out if you get a 401?
Yes, That is the logic I'm using in my app at the moment.
What is this? this.tokenService.processing = true;
Hello @ValRob tokenService is the service RefreshTokenService , in that line of code I'm just changing the state thats indicate that a request is getting the token and I update the state to false when I have the token or when I get an error from the request responsable to get the token.
I've tried to implement this solution, but the only thing I cannot get my head around is the origin of OAuthService. Where is it imported from?
|
0

Below interceptors do this task for you

import { throwError as observableThrowError, Observable, Subject, EMPTY, } from 'rxjs'; import { catchError, switchMap, tap, finalize } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpSentEvent, HttpHeaderResponse, HttpProgressEvent, HttpResponse, HttpUserEvent, HttpErrorResponse, } from '@angular/common/http'; import { StoreService } from './store.service'; import { ApiService } from './api.service'; export const tokenURL = '/315cfb2a-3fdf-48c3-921f-1d5209cb7861'; //copied from api service @Injectable() export class SessionInterceptorService implements HttpInterceptor { isRefreshingToken: boolean = false; cachedRequests = []; tokenSubject: Subject<string> = new Subject<string>(); constructor( private readonly store: StoreService, private readonly ApiService: ApiService ) {} intercept( req: HttpRequest<any>, next: HttpHandler ): Observable< | HttpSentEvent | HttpHeaderResponse | HttpProgressEvent | HttpResponse<any> | HttpUserEvent<any> > { let urlPresentIndex = this.cachedRequests.findIndex( (httpRequest) => httpRequest.url == req.url ); if (this.isRefreshingToken && !req.url.endsWith(tokenURL)) { // check if unique url to be added in cachedRequest if (urlPresentIndex == -1) { this.cachedRequests.push(req); return this.tokenSubject.pipe( switchMap(() => next.handle(req)), tap((v) => { // delete request from catchedRequest if api gets called this.cachedRequests.splice( this.cachedRequests.findIndex( (httpRequest) => httpRequest.url == req.url ), 1 ); return EMPTY; }) ); } else { //already in cached request array return EMPTY; } } return next.handle(this.updateHeader(req)).pipe( catchError((error) => { console.log(error); if (error instanceof HttpErrorResponse) { switch ((<HttpErrorResponse>error).status) { case 400: return this.handle400Error(error); case 403 || 401: if (req.url.endsWith(tokenURL)) { return observableThrowError(error); } else { this.cachedRequests.push(req); return this.handle401Error(req, next); } default: return observableThrowError(error); } } else { return observableThrowError(error); } }) ); } handle400Error(error) { if ( error && error.status === 400 && error.error && error.error.error === 'invalid_grant' ) { // If we get a 400 and the error message is 'invalid_grant', the token is no longer valid so logout. return this.logout(); } return observableThrowError(error); } handle401Error(req: HttpRequest<any>, next: HttpHandler) { if (!this.isRefreshingToken) { this.isRefreshingToken = true; return this.ApiService.refreshToken().pipe( switchMap((newToken: string) => { if (newToken) { this.store.updateAccessToken(newToken); this.tokenSubject.next(newToken); return next.handle(this.updateHeader(this.cachedRequests[0])); } // If we don't get a new token, we are in trouble so logout. return this.logout(); }), catchError((error) => { // If there is an exception calling 'refreshToken', bad news so logout. return this.logout(); }), finalize(() => { this.isRefreshingToken = false; }) ); } } logout() { console.log('logging it out'); // Route to the login page (implementation up to you) return observableThrowError(''); } /* This method is append token in HTTP request'. */ updateHeader(req) { const authToken = this.store.getAccessToken(); console.log(authToken); req = req.clone({ headers: req.headers.set('X-RapidAPI-Key', `${authToken}`), }); return req; } } 

For more details you can read my medium article Token-Refresh-Interceptor-retry-failed-Requests

Check it out, how it works stackblitz

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.