I was working with the Http
clase from Angular but I decide to make the migration and work with the new HttpClient
, and I was trying to create a so
For anyone else looking for a solution to this in Angular 4 (maybe slight changes needed for Angular 5+), I came up with the following solution:
@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
private _refreshRequest: Observable> | null = null;
constructor(
private _router: Router,
private _tokenStorage: TokenStorageService,
private _injector: Injector) {
}
private _addTokenHeader(request: HttpRequest) {
const authToken = this._tokenStorage.authToken;
if (!authToken) {
return request;
}
return request.clone({setHeaders: {'Authorization': 'Bearer ' + authToken.value}});
}
private _fail() {
this._tokenStorage.clearTokens();
this._router.navigate(['/login']);
return throwError(new HttpErrorResponse({status: 401}));
}
private _refreshAuthToken(request: HttpRequest, next: HttpHandler) {
// AuthService has the following dependency chain:
// ApiService -> HttpClient -> HTTP_INTERCEPTORS
// If injected at the constructor this causes a circular dependency error.
const authService = this._injector.get(AuthService);
if (this._refreshRequest === null) {
// Send the auth token refresh request
this._refreshRequest = authService.refreshAuthToken();
this._refreshRequest.subscribe(() => this._refreshRequest = null);
}
// Wait for the auth token refresh request to finish before sending the pending request
return this._refreshRequest
.flatMap(result => {
if (result.success) {
// Auth token was refreshed, continue with pending request
return this._sendRequest(this._addTokenHeader(request), next);
}
// Refreshing the auth token failed, fail the pending request
return this._fail();
});
}
private _sendRequest(request: HttpRequest, next: HttpHandler) {
return next.handle(request).catch((err: HttpErrorResponse, caught) => {
// Send the user to the login page If there are any 'Unauthorized' responses
if (err.status === 401) {
this._router.navigate(['/login']);
}
return Observable.throw(err);
});
}
intercept(request: HttpRequest, next: HttpHandler): Observable> {
if (request.url.indexOf('/api/auth') !== -1) {
// This interceptor should not be applied to auth requests
return this._sendRequest(request, next);
}
const authToken = this._tokenStorage.authToken;
const refreshToken = this._tokenStorage.refreshToken;
// Attempt to refresh the auth token if it is expired or about to expire
if (authToken && authToken.expiresWithinSeconds(60)) {
if (refreshToken && !refreshToken.isExpired) {
return this._refreshAuthToken(request, next);
}
else {
// Auth token has expired and cannot be refreshed
return this._fail();
}
}
return this._sendRequest(this._addTokenHeader(request), next);
}
}
This will issue an auth token refresh request to the server if the current auth token is expired, but there is a valid refresh token. Further requests are buffered until the pending refresh request completes.
Not shown is the source to:
- TokenStorageService
which just uses localStorage
- Jwt
class that wraps a token and makes the token claims like expiry date easy to access
- ApiResult
which is just a simple wrapper around HttpResponse
for my application, and not particularly relevant to anything here
Edit: Angular 6/7
import { Injectable, Inject, Injector } from '@angular/core';
import { Router } from '@angular/router';
import {
HttpEvent,
HttpInterceptor,
HttpHandler,
HttpRequest,
HttpErrorResponse,
} from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { catchError, flatMap } from 'rxjs/operators';
import { ApiResult } from '../../api';
import { TokenStorageService } from './token-storage.service';
import { AuthService } from './auth.service';
@Injectable()
export class AuthInterceptorService implements HttpInterceptor {
private _refreshRequest: Observable | null = null;
constructor(
private _router: Router,
private _tokenStorage: TokenStorageService,
@Inject('BASE_URL') private _baseUrl: string,
private _injector: Injector) {
}
private _addTokenHeader(request: HttpRequest) {
const authToken = this._tokenStorage.authToken;
if (!authToken) {
return request;
}
return request.clone({setHeaders: {'Authorization': 'Bearer ' + authToken.value}});
}
private _forceLogin() {
this._tokenStorage.clearTokens();
this._router.navigate(['/account/login'], { queryParams: {
message: 'Your session has expired. Please re-enter your credentials.'
}});
}
private _fail() {
this._forceLogin();
return throwError(new HttpErrorResponse({status: 401}));
}
private _refreshAuthToken(request: HttpRequest, next: HttpHandler) {
// AuthService has the following dependency chain:
// ApiService -> HttpClient -> HTTP_INTERCEPTORS
// If injected at the constructor this causes a circular dependency error.
const authService = this._injector.get(AuthService);
if (this._refreshRequest === null) {
// Send the auth token refresh request
this._refreshRequest = authService.refreshAuthToken();
this._refreshRequest.subscribe(() => this._refreshRequest = null);
}
// Wait for the auth token refresh request to finish before sending the pending request
return this._refreshRequest.pipe(flatMap(result => {
if (result.success) {
// Auth token was refreshed, continue with pending request
return this._sendRequest(this._addTokenHeader(request), next);
}
// Refreshing the auth token failed, fail the pending request
return this._fail();
}));
}
private _sendRequest(request: HttpRequest, next: HttpHandler) {
return next.handle(request).pipe(catchError(err => {
// Send the user to the login page If there are any 'Unauthorized' responses
if (err.status === 401) {
this._forceLogin();
}
return throwError(err);
}));
}
intercept(request: HttpRequest, next: HttpHandler): Observable> {
if (request.url.indexOf(this._baseUrl) === -1 || request.url.indexOf('/api/auth') !== -1) {
// This interceptor should not be applied to non-api requests or auth requests
return this._sendRequest(request, next);
}
const authToken = this._tokenStorage.authToken;
const refreshToken = this._tokenStorage.refreshToken;
// Attempt to refresh the auth token if it is expired or about to expire
if (authToken && authToken.expiresWithinSeconds(60)) {
if (refreshToken && !refreshToken.isExpired) {
return this._refreshAuthToken(request, next);
}
else {
// Auth token has expired and cannot be refreshed
return this._fail();
}
}
return this._sendRequest(this._addTokenHeader(request), next);
}
}