Skip to content

Instantly share code, notes, and snippets.

@drackp2m
Last active August 11, 2023 19:22
Show Gist options
  • Save drackp2m/ea63ce577bea9b1b0652d054fda89ea6 to your computer and use it in GitHub Desktop.
Save drackp2m/ea63ce577bea9b1b0652d054fda89ea6 to your computer and use it in GitHub Desktop.
Angular HTTP Interceptor for Pausing and Resuming Unauthorized Requests with JWT
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