The applications that I've implemented in Angular 4+ with HttpClient
from @angular/common/http
utilize JSON Web Tokens (JWT) for authentication from our server, which are then stored in the Angular app and passed in an Authentication header to the web server for any API calls.
This works well until the JWT expires, in my case after two hours, at which point any HTTP requests start failing because of the expired token.
Rather than having to implement JWT refresh code in every HTTP call, I've instead written a drop-in replacement service for HttpClient
that performs the refresh authentication automatically. You call the .get()
and .post()
methods as you would normally for HttpClient
.
The full code can be found here, with a breakdown of what's going on below:
import {Injectable} from '@angular/core';
import {HttpClient, HttpErrorResponse} from '@angular/common/http';
import {Observable, Subscriber} from 'rxjs';
@Injectable()
export class CkHttpClientService {
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// Populate your own Refresh token value here
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
refreshTokenValue: string = null;
// constructor
constructor(private httpClient: HttpClient) { }
// is the specified error an auth error?
private _isAuthError(error: HttpErrorResponse): boolean {
// default that it's not?
let authError = false;
// if an error was included
if (error) {
// check the statuses for an auth error
authError = authError || (error.status === 401);
authError = authError || (error.status === 403);
}
return authError;
}
// refreshes the login token
private _refreshToken(): Observable<boolean> {
const observable = new Observable((subscriber: Subscriber<boolean>) => {
// if there is no refresh token value
if ((this.refreshTokenValue || '') !== '') {
subscriber.next(false);
subscriber.complete();
// if there is a refresh token value
} else {
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// Your Login Token refresh code goes here
// - Make sure to store the new refresh token for later use
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
this.refreshTokenValue = '';
subscriber.next(true);
subscriber.complete();
}
});
return observable;
}
// performs a GET operation
get(url: string, options?: any): Observable<any> {
return this._http('get', url, null, options);
}
// performs a POST operation
post(url: string, payload: any, options?: any): Observable<any> {
return this._http('post', url, payload, options);
}
// returns the appropriate HttpClient Observable
private _httpClient(method: string, url: string, payload: any, options: any): Observable<any> {
// where we're storing our HTTP Client observable
let httpClientObservable: Observable<any> = null;
// if this is a POST
if (method.toLowerCase() === 'post') {
httpClientObservable = this.httpClient.post(url, payload, options);
// default to a GET
} else {
httpClientObservable = this.httpClient.get(url, options);
}
return httpClientObservable;
}
// generic HTTP caller
private _http(method: string, url: string, payload: any, options: any): Observable<any> {
const observable = Observable.create((subscriber: Subscriber<any>) => {
// execute the HTTP observable
this._httpClient(method, url, payload, options).subscribe((result: any) => {
// on success
subscriber.next(result);
subscriber.complete();
// on error
}, (error: HttpErrorResponse) => {
// is this an auth error?
if (this._isAuthError(error)) {
// call the refresh token function
this._refreshToken().subscribe((refreshSuccessFlag: boolean) => {
// if the refresh was successful
if (refreshSuccessFlag) {
// re-attempt the observable
this._httpClient(method, url, payload, options).subscribe((retryResult: any) => {
// on success, send the result
subscriber.next(retryResult);
subscriber.complete();
// on error
}, (retryError: HttpErrorResponse) => {
subscriber.error(retryError);
subscriber.complete();
});
// if the refresh failed
} else {
subscriber.error(error);
subscriber.complete();
}
});
// if this is not an auth error
} else {
subscriber.error(error);
subscriber.complete();
}
});
});
return observable;
}
}
Here is an explanation of what's going on.
refreshTokenValue: string = null;
This is a string variable that stores the login refresh token, and will be used to refresh the token when it expires. You should populate this when the user initially logs in, or you can replace it with a function or getter that refers to another service.
constructor(private httpClient: HttpClient) { }
Here we're injecting httpClient
from @angular/common/http
.
private _isAuthError(error: HttpErrorResponse): boolean {
// default that it's not?
let authError = false;
// if an error was included
if (error) {
// check the statuses for an auth error
authError = authError || (error.status === 401);
authError = authError || (error.status === 403);
}
return authError;
}
This function determines if the HTTP error is an authentication error. We're using the HTTP responses statuses of 401 (Unauthorized) and 403 (Forbidden). We first check that error
is a defined object.
private _refreshToken(): Observable<boolean> {
const observable = Observable.create((subscriber: Subscriber<boolean>) => {
// if there is no refresh token value
if ((this.refreshTokenValue || '') !== '') {
subscriber.next(false);
subscriber.complete();
// if there is a refresh token value
} else {
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// Your Login Token refresh code goes here
// - Make sure to store the new refresh token for later use
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
this.refreshTokenValue = '';
subscriber.next(true);
subscriber.complete();
}
});
return observable;
}
If there is no refresh token stored, the function returns false
by default. You'll need to swap out the dummy refresh token code here with your own call to refresh the login token.
get(url: string, options?: any): Observable<any> {
return this._http('get', url, null, options);
}
post(url: string, payload: any, options?: any): Observable<any> {
return this._http('post', url, payload, options);
}
These functions mimic HttpClient
from @angular/common/http
, but internally refer to a common, private _http
function, since the code is mostly identical. I haven't implemented OPTIONS
or PUT
or any other methods, but you should be able to yourself.
private _httpClient(method: string, url: string, payload: any, options: any): Observable<any> {
// where we're storing our HTTP Client observable
let httpClientObservable: Observable<any> = null;
// if this is a POST
if (method.toLowerCase() === 'post') {
httpClientObservable = this.httpClient.post(url, payload, options);
// default to a GET
} else {
httpClientObservable = this.httpClient.get(url, options);
}
return httpClientObservable;
}
This function returns the appropriate GET or POST Observable, based on the method
value passed to it. We'll be using this function multiple times in _http()
.
private _http(method: string, url: string, payload: any, options: any): Observable<any> {
const observable = Observable.create((subscriber: Subscriber<any>) => {
The bulk of the heavy lifting is done in this function, performing the initial HTTP request, refreshing the token if we receive an authentication error, and performing the subsequent HTTP request after the token is refreshed.
this._httpClient(method, url, payload, options).subscribe((result: any) => {
// on success
subscriber.next(result);
subscriber.complete();
// on error
}, (error: HttpErrorResponse) => {
// is this an auth error?
if (this._isAuthError(error)) {
We first try performing the initial HTTP request. If it was successful, we pass through the result. Otherwise if it errors, we test to see if it was an authentication error.
// call the refresh token function
this._refreshToken().subscribe((refreshSuccessFlag: boolean) => {
If it was an authentication error, we call the _refreshToken()
function to perform the refresh and return a success indicator.
// if the refresh was successful
if (refreshSuccessFlag) {
// re-attempt the observable
this._httpClient(method, url, payload, options).subscribe((retryResult: any) => {
// on success, send the result
subscriber.next(retryResult);
subscriber.complete();
If the refresh was successful, we re-attempt the HTTP request that we performed earlier, passing through the result if it was successful.
// on error
}, (retryError: HttpErrorResponse) => {
subscriber.error(retryError);
subscriber.complete();
});
If the HTTP request was unsuccessful, we pass through the error.
// if the refresh failed
} else {
subscriber.error(error);
subscriber.complete();
}
});
If the token refresh was not successful, pass through that error.
// if this is not an auth error
} else {
subscriber.error(error);
subscriber.complete();
}
If the initial HTTP error wasn't an authentication error, we pass that error through.