In a previous post here, we looked at how to intercept HTTP unauthorized responses, and refresh authorization tokens and resend the original request, with the new authorization token. This had me thinking, what if you wanted to be more proactive. In this post, we want to get the authorization token, check if it’s expired and refresh it just before attaching it to the HTTP request. We also need to avoid sending multiple authorization token refresh requests, but rather queue the request behind a single request response and use the new authorization token when it is received.
How it Works
For our authorization token, we will be using JWT (JSON Web Tokens) standard. We will need to check before every request whether it is expired (or close to expire) and send a request to refresh it. So, we need a library to read JWT Tokens, we will use angular2-jwt by Auth0. We will also require a service – Auth Service, that will fetch and return the token to us as an observable.
The service will also be checking whether the token is expired, and then send a refresh request. The observable returned by the service will be shared across multiple requests. On top of that, we of course need a HTTP Interceptor, to attach an authorization header to every outgoing request. To make things much easier, we will not start a new project this time. We are going to build on top of the previous post, which you can find here. It already has an Authentication Service and a HTTP Interceptor. So, without further ado:
Getting Started
We already have a project, so in this case, we just need to add angular2-jwt library. You can use your favorite package manager to add it: Using NPM:
$ npm install @auth0/angular-jwt
Using Yarn Package Manager:
$ yarn add @auth0/angular-jwt
Feel free to use any other library you are comfortable with. Since, we are only interested with decoding JSON, and not the rest of functionality offered by the library, we are going to use only the JwtHelperService
. So, let’s register the service, to our list of providers in our app module.
// ...
import { JwtHelperService } from '@auth0/angular-jwt';
// ...
@NgModule({
declarations: [ ... ],
imports: [...],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: HttpAuthInterceptor,
multi: true
},
JwtHelperService
],
bootstrap: [AppComponent]
})
export class AppModule {}
Next, we need to add the functionality to fetch the token and check if it’s expired. If it’s expired, we are going to refresh it and return the new token.
Fetching and Refreshing Tokens
This task will be completed by our authentication service. We need to add a new method – getToken()
inside our service. On top of that, we also need to inject JwtHelperService
into our service, so we can use it within the method:
private decoder: JwtHelperService
Then create our new method:
getToken() : Observable<string> {}
NB: Our method returns only an observable string, which is the token. Next, let’s add a method to fetch a token and check if it is expired. We will be storing our tokens in local-storage. Please note, you can also use state management libraries like NGXS/NGRX/Akita to store tokens. First, let’s get the token from local storage:
const token = localStorage.getItem('token');
Then, check if its expired using JwtHelperService service:
const isTokenExpired = this.decoder.isTokenExpired(token);
Now, let’s return an observable of the authorization token string, if it is not expired:
if (!isTokenExpired) {
return of(token);
}
And if it is expired, we need to send a refresh request to get a new authorization token. In the previous post, we had already built a method to refresh an expired token. It basically sends the expired token and a refresh token to a refresh token endpoint and gets back new once. On top of that, we shared the observable and saved the authorization tokens to our local storage inside the same method. Here is what the method looks like:
refreshToken(): Observable<string> {
const url = 'url to refresh token here';
// append refresh token if you have one
const refreshToken = localStorage.getItem('refreshToken');
const expiredToken = localStorage.getItem('token');
return this.http
.get(url, {
headers: new HttpHeaders()
.set('refreshToken', refreshToken)
.set('token', expiredToken),
observe: 'response'
})
.pipe(
share(), // <========== YOU HAVE TO SHARE THIS OBSERVABLE TO AVOID MULTIPLE REQUEST BEING SENT SIMULTANEOUSLY
map(res => {
const token = res.headers.get('token');
const newRefreshToken = res.headers.get('refreshToken');
// store the new tokens
localStorage.setItem('refreshToken', newRefreshToken);
localStorage.setItem('token', token);
return token;
})
);
}
We are going to utilize the same method to renew our authorization token. So, let’s call the refreshToken()
method and return it inside our getToken()
method:
return this.refreshToken();
So, our complete getToken() method will look like this:
getToken(): Observable<string> {
const token = localStorage.getItem('token');
const isTokenExpired = this.decoder.isTokenExpired(token);
if (!isTokenExpired) {
return of(token);
}
return this.refreshToken();
}
Attaching Authorization Header
Our HTTP Interceptor already intercepts response with 401 and refreshes the token. We don’t want to remove that, but rather add our new functionality on top of it. This is because, due to network latency, the request might take longer than anticipated and the authorization token expires before getting to the server. So, we still need to intercept expired token responses from the server and refresh them. So, first I want to bring your attention to inflightAuthRequest
property of the http interceptor. This holds the refresh requests in progress, so that other subsequent requests requiring to refresh their token can hitch onto it instead of sending their own refresh request. This is made possible by the share operator of RXJS. So, foreach intercepted request, we need to check if the inflightAuthRequest
is set, and subscribe to it. If not set, we need to initiate a new one.
if (this.inflightAuthRequest == null) {
this.inflightAuthRequest = authService.getToken();
}
Next, we will use switchMap, to send the initial request we intercepted after we got the token from the getToken()
method.
return this.inflightAuthRequest.pipe(
switchMap((newToken: string) => {
// unset request inflight
this.inflightAuthRequest = null;
// use the newly returned token
const authReq = req.clone({
headers: req.headers.set('token', newToken)
});
return next.handle(authReq);
}),
catchError(error => {
// the code to catch 401 response here
})
);
NB: Once the getToken method is resolved successfully, remember to unset the
inflightAuthRequest
property, so that you don’t end up attaching an expired token to request that come after that.
The complete code for the above post can be found here.