I have an API with token based authentication mechanism. After successful signin I store two tokens in the browser's local storage - access and refresh token.
The access token contains all necessary information required to authorize a user on the server side and it has expiration date.
When the access token is expired the client could request a new access token using refresh token and in the response it will get a pair of new tokens.
In angular 1.x the implementation is pretty simple and straightforward. For instance we could use interceptors:
httpInterceptor.$inject = ['$httpProvider'];
function httpInterceptor($httpProvider) {
$httpProvider.interceptors.push(handleStaleAccessToken);
handleStaleAccessToken.$inject = ['$q', '$injector', 'session'];
function handleStaleAccessToken($q, $injector, session) {
function logoutAndRedirect() {
var authenticationRedirect = $injector.get('authenticationRedirect');
session.destroy();
authenticationRedirect.toLoginPage();
}
return {
responseError: function(rejection) {
// Do nothing for non 403 errors
if (rejection.status !== 403) {
return $q.reject(rejection);
}
var errorCode = rejection.data.error && rejection.data.error.code;
if (errorCode === 'access_token_expired') {
var $http = $injector.get('$http');
// Refresh token
var params = { refreshToken: session.getRefreshToken() };
return $http.post('/api/auth/refresh', params).then(function(response) {
session.setTokens(response.data);
// Re try failed http request
return $http(rejection.config);
}).catch(function(error) {
logoutAndRedirect();
return $q.reject(error);
});
} else {
logoutAndRedirect();
}
return $q.reject(rejection);
}
};
}
}
But how to implement similar logic in angular 2 / rxjs app?
This can be done transparently in Angular2 by extending the Http
class and leveraging observable operators like flatMap
.
Here is some sample code:
if (hasTokenExpired()) {
return this.authService
.refreshAuthenticationObservable()
.flatMap((authenticationResult:AuthenticationResult) => {
if (authenticationResult.IsAuthenticated == true) {
this.authService.setAuthorizationHeader(request.headers);
return this.http.request(url, request);
}
return Observable.throw(initialError);
});
}
This code must be integrated into a custom sub class of the Http
one:
An approach could be to extend the HTTP object to intercept errors:
@Injectable()
export class CustomHttp extends Http {
constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
super(backend, defaultOptions);
}
request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
console.log('request...');
return super.request(url, options).catch(res => {
// do something
});
}
get(url: string, options?: RequestOptionsArgs): Observable<Response> {
console.log('get...');
return super.get(url, options).catch(res => {
// do something
});
}
}
and register it as described below:
bootstrap(AppComponent, [HTTP_PROVIDERS,
new Provider(Http, {
useFactory: (backend: XHRBackend, defaultOptions: RequestOptions) => new CustomHttp(backend, defaultOptions),
deps: [XHRBackend, RequestOptions]
})
]);
For more details have a look at these questions:
- Handling refresh tokens using rxjs
- Angular 2 - How to get Observable.throw globally
I had to do something similar in my recent project shafihuzaib/cdp-ng-boilerplate and landed on this question for my answer. I couldn't go for the above suggested solution, as it felt complicated and something not desirable. So I came back to leave my solution after I implemented one. However, the difference being that in my case, I had two such tokens.
So, every request that needs to have tokens' validity checked, is called inside this function.
tokenValidatedRequest(func): Observable<any>{
let returnObservable = new Observable();
/**
* 1. check for auth token expiry - refresh it, if necessary
*/
if( parseInt(localStorage.getItem('AUTH_TOKEN_EXPIRY')) < (new Date()).valueOf() ){
//auth expired
this.refresh().subscribe(res => {
//refreshed
//this.postAuthSuccess(res);
returnObservable = func();
})
}
else{
//auth not expired
returnObservable = func();
}
return returnObservable;
}
The most important thing here is that func()
should return an Observable
, so that it can be consumed accordingly.
makeSomeHttpCall(){
this.tokenValidatedRequest(()=>{
return this.http.post(...);
}). subscribe();
}
It may seem a bit complicated for someone new, but I am sure it is a little more efficient.
In the following links, please ignore the details that are irrelevant to this question and focus on the usage of the suggested solution.
Actual implementation of tokenValidatedRequest()
in my project.
tokenValidatedRequest(func , tqlCheck = false): Observable<any>{
/**
* Delegate the actual task. However return an Observable, so as to execute
* the callback function only when subscribed to..
*/
//return Observable.create(obs => obs = (this.__tokenValidatedRequest(func, tqlCheck)));
return this.__tokenValidatedRequest(func, tqlCheck);
}
private __tokenValidatedRequest(func, tqlCheck = false): Observable<any>{
let returnObservable = new Observable();
/**
* 1. check for auth token expiry - refresh it, if necessary
* 2. after step 1 - check for TQL token expiry (if tqlCheck is true) - refresh it, if necessary
* 3.
*/
if( parseInt(localStorage.getItem('AUTH_TOKEN_EXPIRY')) < (new Date()).valueOf() ){
//auth expired
this.refresh().subscribe(res => {
//refreshed
this.postAuthSuccess(res);
if(tqlCheck && localStorage.getItem("TQL_TOKEN_EXPIRY") &&
parseInt(localStorage.getItem("TQL_TOKEN_EXPIRY")) < (new Date()).valueOf()
){
this.activateUser().subscribe(res => {
//TQL token subscribed
returnObservable = func();
})
}
else{
// Probably not a TQL request
returnObservable = func();
}
})
}
else{
//auth not expired
//check if tql token has expired
if(tqlCheck && localStorage.getItem("TQL_TOKEN_EXPIRY") &&
parseInt(localStorage.getItem("TQL_TOKEN_EXPIRY")) < (new Date()).valueOf()
){
this.activateUser().subscribe(res => {
//TQL token subscribed
returnObservable = func();
})
}
else{
// Probably not a TQL request or none of the tokens expired
returnObservable = func();
}
}
return returnObservable;
}
How it is used in other services!
getAllParkingSpaces() : Observable<any> {
let query = {
Query: {
....
}
};
return this.authService.tokenValidatedRequest(()=>{
return this.api.post( CONFIG.api.engineUrl + 'devices/parking', query);
}, true);
}
How I finally subscribe to it!
this.realTimeParkingService.getAllParkingSpaces().subscribe( r => {
this.parkingSpaces = r;
});