Last active
August 11, 2023 19:22
-
-
Save drackp2m/ea63ce577bea9b1b0652d054fda89ea6 to your computer and use it in GitHub Desktop.
Angular HTTP Interceptor for Pausing and Resuming Unauthorized Requests with JWT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
HttpClient, | |
HttpErrorResponse, | |
HttpEvent, | |
HttpHandler, | |
HttpInterceptor, | |
HttpRequest, | |
HttpResponse, | |
} from '@angular/common/http'; | |
import { Injectable } from '@angular/core'; | |
import { Router } from '@angular/router'; | |
import { Observable, Subject, of } from 'rxjs'; | |
import { catchError, switchMap } from 'rxjs/operators'; | |
/** | |
* This interceptor handles the refreshing of JWT tokens in response to an | |
* 'Unauthorized' error from the server. | |
* It ensures that the token refresh occurs only once (although this sometimes fails | |
* if the initial requests are launched too often), and if the refresh fails, | |
* the user is redirected to the login page. | |
* Additionally, the interceptor waits for the tokens to be refreshed | |
* before resending the original request, | |
* while excluding refresh and login requests from the token refresh process | |
*/ | |
@Injectable() | |
export class AuthInterceptor implements HttpInterceptor { | |
private readonly API_REFRESH_SESSION_URL = '/api/auth/refresh-session'; | |
private readonly API_LOGIN_URL = '/api/auth/login'; | |
private tokenIsValid = true; | |
private jwtTokensRefreshed$: Subject<void> = new Subject<void>(); | |
constructor( | |
private readonly httpClient: HttpClient, | |
private readonly router: Router, | |
) {} | |
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { | |
const urlsToIgnore = [this.API_REFRESH_SESSION_URL, this.API_LOGIN_URL]; | |
// Skip any checks if the received URL is our token refresh endpoint. | |
if (urlsToIgnore.includes(req.url)) return next.handle(req); | |
if (!this.tokenIsValid) { | |
// Await for the token refresh request to finish and release original request. | |
return this.jwtTokensRefreshed$.pipe(switchMap(() => next.handle(req))); | |
} | |
return next.handle(req).pipe( | |
switchMap((event) => { | |
// Skip any checks if the received event is not an HTTP response. | |
if (!(event instanceof HttpResponse)) return of(event); | |
const isUnauthorizedError = | |
event.body?.errors && event.body.errors[0].message === 'Unauthorized'; | |
// Skip any checks if the received event is not an unauthorized error. | |
if (!isUnauthorizedError) return of(event); | |
// Raise the flag to prevent multiple token refresh requests. | |
this.tokenIsValid = false; | |
return this.tryToRefreshToken<HttpResponse<unknown>>(req, next, event); | |
}), | |
catchError((error) => { | |
// Skip any checks if the received event is not an HTTP error response. | |
if (!(error instanceof HttpErrorResponse)) return of(error); | |
const isUnauthorizedError = 401 !== error.status; | |
// Skip any checks if the received event is not an unauthorized error. | |
if (isUnauthorizedError) return of(error); | |
// Raise the flag to prevent multiple token refresh requests. | |
this.tokenIsValid = false; | |
// Refresh the JWT tokens and retry the original request. | |
return this.tryToRefreshToken(req, next, error); | |
}), | |
); | |
} | |
private tryToRefreshToken<T>( | |
req: HttpRequest<unknown>, | |
next: HttpHandler, | |
event: unknown, | |
): Observable<T> { | |
return this.refreshJwtTokens().pipe( | |
switchMap(() => { | |
// Lower the flag to prevent multiple token refresh requests. | |
this.tokenIsValid = true; | |
this.jwtTokensRefreshed$.next(); | |
return next.handle(req) as Observable<T>; | |
}), | |
catchError(() => { | |
// If the token refresh request fails, redirect the user to the login page. | |
this.tokenIsValid = true; | |
this.router.navigate(['/login']); | |
return of(event) as Observable<T>; | |
}), | |
); | |
} | |
private refreshJwtTokens(): Observable<void> { | |
return this.httpClient.get<void>(this.API_REFRESH_SESSION_URL); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment