Reputation: 778
TLDR: My task is to complete 3 requests instead of 1 and return the last response as a response to the first request without any additional modifications of the request initiator.
I have extended the Angular Http
class to automatically append authorization headers to all of my requests and implement my own authorization error handling.
It looks like this:
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
// ... append some headers
super.request(url, options).catch((error: Response) => {
if (error.status === 401 || error.status === 403 ) {
// todo: Send refreshToken request to get new credentials
// todo: Send current request again with new credentials
// todo: If request is completed properly pretend everything was fine and return response
}
});
}
I want to catch authorization errors, fix them by sending token refresh request and return proper response to the initial request.
There's a lot of code using http
now and I don't want to change it so the fixed response has to be returned as the initial would have been without anybody knowing about it.
One of the approaches was to use synchronous requests but it's not a good idea I think.
Could you tell please if the solution is possible and how can I achieve it?
PS. There may be a problem when another request is executed while the token is being refreshed and crash into an authorization causing one more token refresh. But this is not that important now.
Upvotes: 3
Views: 3028
Reputation: 778
The aim was achieved mostly by using flatMap
to compose requests.
Key functions:
It's designed to work with the REST authentication model which includes:
gToken
) aToken
)refresh_token
)Most likely you will need to rewrite requests to fit your backend but here's a well-commented Services to be provided instead of default Http
:
import {Injectable} from '@angular/core';
import {
Http, XHRBackend, RequestOptions, RequestOptionsArgs, Request, Response, RequestMethod,
Headers
} from "@angular/http";
import { Observable } from "rxjs";
import { StorageService } from "../storage.service";
import { AppService } from "./app.service";
@Injectable()
export class HttpClientService extends Http {
private autoAppendHeadersDefault = true;
constructor(
backend: XHRBackend,
defaultOptions: RequestOptions,
private storageService: StorageService,
private appState: AppService,
) {
super(backend, defaultOptions);
this.autoAppendHeadersDefault = this.appState.hoodConfig.HTTP_AUTO_APPEND_HEADERS;
}
request(url: string | Request, options?: RequestOptionsArgs, disableTryFix = false): Observable<Response> {
// Checking if the request needs headers to be appended
let assetRequest = false;
if(url instanceof Request) {
if(url.url.startsWith("/assets")) {
assetRequest = true;
}
}
// Appending headers
if(!assetRequest && this.appState.hoodConfig.HTTP_AUTO_APPEND_HEADERS && url instanceof Request) {
// append aToken || gToken
let token = this.storageService.get('aToken');
if('undefined' === typeof token || !token) {
token = this.storageService.get('gToken');
}
if('undefined' !== typeof token && token) {
url.headers.set('Authorization', `Bearer ${token}`);
} else {
// neither aToken nor gToken are set
if(disableTryFix) {
this.removeAllTokens();
return Observable.throw({error: "Can't reauth: 01"});
}
return this.tryFixAuth().flatMap(
(res:any) => {
res = res.json();
this.storageService.set('gToken', res.access_token);
return this.request(url, options, true);
}
);
}
// headers appended to every request
if(!url.headers.get('Content-Type')) {
url.headers.append('Content-Type', 'application/json');
}
}
this.appState.hoodConfig.HTTP_AUTO_APPEND_HEADERS = this.autoAppendHeadersDefault;
return super.request(url, options).catch((error: Response) => {
if (error.status === 401 /* || error.status === 403 */ ) {
if(disableTryFix) {
this.removeAllTokens();
this.navigateOnAuthFail();
return Observable.throw({error: "Can't reauth: 02"});
}
return this.tryFixAuth().flatMap(
(res: any) => {
res = res.json();
if('undefined' !== typeof res.refresh_token)
{
// got aToken & refresh_token
this.storageService.set('aToken', res.access_token);
this.storageService.set('refresh_token', res.refresh_token);
}
else if('undefined' !== typeof res.access_token)
{
// got only gToken
this.storageService.set('gToken', res.access_token);
}
else
{
console.log('tryFix: nothing useful returned')
// got no aToken, no gToken, no refresh_token
}
// retry request
return this.request(url, options, true);
}
);
}
// handle invalid refresh_token
if(disableTryFix && error.status === 400) {
console.log('Wrong refresh token (400)');
this.storageService.remove('refresh_token');
this.storageService.remove('aToken');
this.navigateOnAuthFail();
// handle invalid refresh token
}
return Observable.throw(error);
});
}
private tryFixAuth(): Observable<Response> {
console.log('Trying to fix auth');
if(this.storageService.get('refresh_token'))
{
return this.refreshToken();
}
else if(this.storageService.get('aToken'))
{
// no refresh_token, but aToken
// since aToken is dead it's not useful
this.storageService.remove('aToken');
}
else
{
// no aToken, no refresh_token
// possibly there's a gToken
// since the request is trying to fix itself (is failed) the gToken is most likely not valid
return this.guestToken();
}
}
// sends request with refresh_token to get new aToken
// the request returns only aToken and refresh_token, no gToken
private refreshToken(): Observable<Response> {
// is called only when refresh_token is set
let refreshToken = this.storageService.get('refresh_token');
// check refresh_token in case it's not checked before
if('undefined' === typeof refreshToken || !refreshToken || refreshToken == 'undefined') {
this.storageService.remove('refresh_token');
// there's no refresh_token saved
return Observable.throw({error: "Refresh token is not set"});
}
// form refresh_token request
const headers = new Headers();
headers.append('Authorization', `Bearer ${this.storageService.get('gToken')}`);
headers.append('Content-Type', 'application/json');
const url = `${this.appState.config.WEBSITE_ENDPOINT}/oauth/v2/token`;
const localData = JSON.stringify({
"client_id": this.appState.config.CLIENT_ID,
"client_secret": this.appState.config.CLIENT_SECRET,
"grant_type": 'refresh_token',
"refresh_token": refreshToken
});
this.appState.hoodConfig.HTTP_AUTO_APPEND_HEADERS = false;
// refresh_token request
return this.request(
new Request({
method: RequestMethod.Post,
url: url,
headers: headers,
body: localData
}),
null, true);
}
// sends request to get new gToken
private guestToken(): Observable<Response> {
const url = `${
this.appState.config.WEBSITE_ENDPOINT}/oauth/v2/token?client_id=${
this.appState.config.CLIENT_ID}&client_secret=${
this.appState.config.CLIENT_SECRET}&grant_type=client_credentials`;
this.appState.hoodConfig.HTTP_AUTO_APPEND_HEADERS = false;
return super.get(url);
}
// Aux methods
private navigateOnAuthFail() {
console.warn('Page is going to be refreshed');
// redirect to auth is performed after reload by authGuard
// it's possible to add some warning before reload
window.location.reload();
}
private removeAllTokens() {
this.storageService.remove('aToken');
this.storageService.remove('gToken');
this.storageService.remove('refresh_token');
}
}
Upvotes: 2