With the help from @stely000, some colleagues, and other online communities I ended up this:
Since I am using the solution for intercepting the built-in Http service from angular2/angular4 found here https://scotch.io/@kashyapmukkamala/using-http-interceptor-with-angular2 my code differs a bit from other solutions i've found. The OAuthService
i refer to in different places can be found here: https://github.com/manfredsteyer/angular-oauth2-oidc. Due to dependencies in that service to Http
i had to inject that service after to avoid circular dependencies. If you have any questions about that, please just ask me and I'll answer that as well. :)
Basically this my solution for the refresh_token
functionality to trigger once if the backend services respond with 401 is achieved in three steps:
Create an observable that is shared to avoid
this.postRequest() to trigger more than once at a time.
Create request headers and add post to endpoint where
refresh_token is handled.
Listen to the shared observer from step 1. When the token
has been refreshed, extract and update data in localstorage.
Now, in my constructor I create the shared observable:
constructor(
backend: ConnectionBackend,
defaultOptions: RequestOptions,
private injector: Injector
) {
super(backend, defaultOptions);
// Step 1: Create an observable that is shared to avoid this.postRequest() to trigger more than once at a time
this.refreshTokenObserver = Observable.defer(() => {
return this.postRequest();
}).share();
}
Then I create a method for posting the request to refresh the access_token
(NOTE: this is basically a copy of the code that is held in the OAuthService
. This is because the method for that is not public in that service):
// This method will only be triggered once at a time thanks to she shared observer above (Step 1).
private postRequest(): Observable<any> {
// Step 2: Create request headers and add post to endpoint where refresh_token is handled.
let search = new URLSearchParams();
search.set('grant_type', 'refresh_token');
search.set('client_id', this.oauthService.clientId);
search.set('scope', '');
search.set('refresh_token', localStorage.getItem('refresh_token'));
let headers = new Headers();
headers.set('Content-Type', 'application/x-www-form-urlencoded');
let params = search.toString();
return super.post(this.oauthService.tokenEndpoint, params, { headers }).map(r => r.json());
}
Then I have a method for extracting data and updating the local storage with the response from then endpoint:
// This method is triggered when the server responds with 401 due to expired access_token or other reasons
private refreshToken() {
// Step 3: Listen to the shared observer from step 1. When the token has been refreshed, extract and update data in localstorage
return this.refreshTokenObserver.do((tokenResponse) => {
localStorage.setItem("access_token", tokenResponse.access_token);
if (tokenResponse.expires_in) {
var expiresInMilliSeconds = tokenResponse.expires_in * 1000;
var now = new Date();
var expiresAt = now.getTime() + expiresInMilliSeconds;
localStorage.setItem("expires_at", "" + expiresAt);
}
if (tokenResponse.refresh_token) {
localStorage.setItem("refresh_token", tokenResponse.refresh_token);
}
},
(err) => {
console.error('Error performing password flow', err);
return Observable.throw(err);
});
}
In order to initiate the steps above, the initial request needs to be triggered and respond with a 401
:
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
return super.request(url, options).catch(error=> {
if (error.status === 401) {
// It the token has been expired trigger a refresh and after that continue the original request again with updated authorization headers.
return this.refreshToken().mergeMap(() => {
options = this.updateAuthHeader(options);
return super.request(url, options);
});
} else {
return Observable.throw(error);
}
});
}
Bonus: The method i use for updating the Authorization header is basically using the functionality in the OAuthService
mentioned above:
private updateAuthHeader(options: RequestOptionsArgs) {
options.headers.set('Authorization', this.oauthService.authorizationHeader());
return options;
}
Reflections/Thoughts:
The original idea from my side was to use the OAuthService
to refresh token. This was harder than i expected due to the mix of promises and observables. I can probably change the postRequest
method to use the mentioned service methods. I don't really know what the better/cleaner solution might be.
Also, I think that this is something that should be available for everyone to find an easy solution for. This was hard to achieve by myself and I thank everyone who helped me (both here at SO, IRL, and other communities).