0

I have a "Token" service that returns an access-token. Then I have many other services that use this service to get an access-token before they can make an API call to the backend with the token included in the request header.

The problem is that many of these services are calling the Token service almost simultaneously, so before the first call to the token service has returned and has been "cached" the next call is fired...

Resulting into multiple calls to the backend and getting multiple tokens.

Can somebody please tell me how to stop the multiple calls to the backend. Or how to run the Token service before everything else and cache the result and only then allow the application/services to run/bootstrap.

I'am using angular 2.4.4

Any help/suggestion is appreciated


import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
import 'rxjs/add/operator/toPromise';

export class Token{

    private cachedTokenObject = {
        'tokenKey':false,
        'userName':"johndoe@gmail.com",
        'password':"1234",
        'applicationId':"12344"
    };

    constructor(private _http: Http){}

    getAccessToken():Promise<Object>{

        if( this.cachedTokenObject.tokenKey ){
            console.log("returning chached token");
            return Promise.resolve( this.cachedTokenObject );
        }else{
            console.log("getting new token...");

            const tokenHeaders = "username=" + this.cachedTokenObject.userName + "&password=" + this.cachedTokenObject.password +"&grant_type=password";

            return this._http.post("https://someurl.com", tokenHeaders)
            .toPromise()
            .then( result => {
                if( result.json().hasOwnProperty('access_token') ){                 
                    this.cachedTokenObject.tokenKey = result.json()['access_token'];
                    return this.cachedTokenObject;
                }else{
                    console.log('"access_token" property not found in access object');
                    return {};
                }
            } )
            .catch(error =>{
                console.error('*** getAccessToken error ***', error);
                 Promise.reject(error);
            });
        }
    }
}

import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Headers, RequestOptions } from '@angular/http';
import {AccessTokenService} from './access-token.service';

@Injectable()
export class LanguageService{

    private cachedLanguages:any;

    constructor(private _http: Http, private _accessTokenService:AccessTokenService){}

    getLanguages(){

        return this._accessTokenService.getAccessToken().then( token => {

            if(this.cachedLanguages){
                console.log('returning cached languages', this.cachedLanguages);
                return Promise.resolve(this.cachedLanguages);
            }else{

                let headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token['tokenKey']});
                let options = new RequestOptions({ headers: headers });

                return this._http.get('https://someUrl/languages', options)
                .toPromise()
                .then(resp => {
                    this.cachedLanguages = JSON.parse( resp.json() );

                    return this.cachedLanguages;
                })
                .catch(error =>{
                    console.error('*** LanguageService error ***', error);
                     Promise.reject(error);
                });
            }
        });
    }
}
Khaled
  • 3
  • 2

2 Answers2

2

I had a similar issue and I solved it using RxJs operators. Here is my solution:

  private postRequest(): Observable<any> {
    let data: URLSearchParams = new URLSearchParams();
    let obs = this.http.postWithHeaders(
        'token', data, { 'Content-Type': 'application/x-www-form-urlencoded' })
        .map((response) => {
            ...
        })
        .catch((error) => {
           ...
        });
    return obs;
}

The postRequest is responsible for retrieving a refresh token. It's important to note that it returns an observable.

In the constructor I create an observable that will defer the postRequest and I share my observable:

 this.source = Observable.defer(() => {
        return this.postRequest();
    }).share();

The share operator will share the subscription between multiple subscribers. Here is a usefull to better understand it: RxJs - getting started

I then created a method that executes the observable:

refreshToken(): Observable<any> {
    return this.source
        .do((data) => {
            //extract the access token and save it to local storage
        }, error => {
           ...
        });
}

You can test the code by subscribing to the refreshToken method multiple times and then counting the number of requests the browser makes:

this.tokenRefreshService.refreshToken().subscribe(x=>{...})
this.tokenRefreshService.refreshToken().subscribe(x=>{...})
this.tokenRefreshService.refreshToken().subscribe(x=>{...})

Hope that makes sense.

Radu Cojocari
  • 1,759
  • 1
  • 22
  • 25
0

Is there anything preventing you from storing the token in Local Storage? ie window.localStorage.setItem('token_name', token); And then your Token Service could have a public function to retrieve the token, something like:

retrieveToken() { return window.localStorage.getItem(tokenName); }

If the token does not exist, then make a call to the backend. As for retrieving the token before application startup, there are numerous ways to handle this depending upon your architecture. For instance, upon Login, store the token in Local Storage prior to navigating to the next page. You could also setup Guards on your routes to verify a token exists in storage.

There is a nice library to handle auto-attaching your authentication header to Http requests called Angular-JWT. It also contains helper functions to check token expiration.

Hope that helps.

Tyler Jennings
  • 8,761
  • 2
  • 44
  • 39
  • I think that Khaled is concerned that multiple concurrent requests are made when there is no token. – Radu Cojocari Feb 22 '17 at 17:12
  • @stely000, yes exactly – Khaled Feb 23 '17 at 21:50
  • Just realised that you may come across a corner case where your token is valid when you check it, but expires before you get to make the request. I recommend implementing some retry logic to cater for this race condition. – Radu Cojocari Feb 25 '17 at 08:51
  • I have token refreshes handled. The question, as I understood it, was about caching the token prior to other requests needing it. Of course everything comes down to your use-cases of your application, and I may have misunderstood the original question. – Tyler Jennings Feb 26 '17 at 13:12