Token is not being refreshed if user is authenticated on app start up

After logging in the scheduleRefresh() function is called and it will keep refreshing the token.

The problem occurs when a user is authenticated and they come back to the webpage. I can see that is goes to the scheduleRefresh() function but it never actually refreshes the token. If I refresh the page after I know the token has been expired I get a token_not_provided error because an HTTP request is being made on that page to my API… BUT the token is refreshed a couple of seconds later (I can see in the network console). If I refresh the page again after I see thats its been refreshed the token is refreshed ONCE after it has been expired but not again.

app.component.ts (on app start up)

platform.ready().then(() => {

  storage.ready().then(() => storage.get('token'))
    .then(token => {
      storage.set('token', token);
      authService.token = token;
      authService.authNotifier.next(true);
      authService.checkToken();
      authService.startupTokenRefresh();
    });

  authService.authenticationNotifier().subscribe((authed) => {
    if (authed) {
      this.rootPage = TabsPage;
    } else {
      authService.logout();
      this.rootPage = LoginPage;
    }
  });
}

auth.service.ts


jwtHelper: JwtHelper = new JwtHelper();
token;
refreshSubscription: any;
authNotifier: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);

authenticationNotifier(): Observable<boolean> {
	return this.authNotifier;
}

refresh(): Observable<any> {
	let URL = `${myApi}/refresh?token=${this.token}`;

	return this.authHttp.get(URL)
	  .map((rsp) => {
	      this.token = rsp.json().token;
	      this.storage.ready().then(() => this.storage.set('token', this.token));
	      this.authNotifier.next(true);
	      return rsp.json().token;
	    },
	    err => {
	      this.authNotifier.next(false);
	      this.logout();
	      console.log(err);
	    })
	  .share();
}

checkToken() {
	if (this.token === '' || this.token === null || this.token === undefined) {
  	this.authNotifier.next(false);
  	this.logout();
	}
}

public startupTokenRefresh() {
	if (this.token === '' || this.token === null || this.token === undefined) {
	console.log("instartup if this.tok====");
	this.authNotifier.next(false);
	this.logout();
	}
	else {
	  // Get the expiry time to generate a delay in milliseconds
	  let now: number = new Date().valueOf() / 1000;
	  let jwtExp: number = this.jwtHelper.decodeToken(this.token).exp;
	  let iat: number = this.jwtHelper.decodeToken(this.token).iat;

	  let refreshTokenThreshold = 10; //seconds

	  let delay: number = jwtExp - now;
	  let totalLife: number = (jwtExp - iat);
	  (delay < refreshTokenThreshold ) ? delay = 1 : delay = delay - refreshTokenThreshold;

	  // Use the delay in a timer to // run the refresh at the proper time
	  return Observable.timer(delay * 1000);
	  });

	// Once the delay time from above is reached, get a new JWT and schedule additional refreshes
	source.subscribe(() => {
	  this.refresh().subscribe(
	    (res) => {
	      console.log('-> Refreshed on startup');
	      this.scheduleRefresh();
	    },
	    (error) => console.log('-> Refresh error:' + JSON.stringify(error)))

		});
	}
}

public scheduleRefresh() {
  if (this.token === '' || this.token === null || this.token === undefined) {
    console.log("in schedrefresh if this.tok====");
    this.authNotifier.next(false);
    this.logout();
  } else {
    // If the user is authenticated, use the token stream provided by angular2-jwt and flatMap the token
    let source = this.authHttp.tokenStream.flatMap(
      token => {
        let jwtIat = this.jwtHelper.decodeToken(this.token).iat;
        let jwtExp = this.jwtHelper.decodeToken(this.token).exp;
        let iat = new Date(0);
        let exp = new Date(0);

        let delay = (exp.setUTCSeconds(jwtExp) - iat.setUTCSeconds(jwtIat));

        return Observable.interval(delay);
      });

    this.refreshSubscription = source.subscribe(() => {
      this.refresh().subscribe((res) => console.log('-> Refreshed...'), 
      (error) => console.log('Refresh error: ' + JSON.stringify(error)))
    });
  }
}

public unscheduleRefresh() {
  console.log("unsched");
  if (this.refreshSubscription) {
    this.refreshSubscription.unsubscribe();
  }
}

login.ts

onLogin() {
  this.authService.login(this.loginForm.value.username, this.loginForm.value.password)
    .subscribe(
    (response) => {
        this.storage.ready().then(() => this.storage.set('token', response.token));
        this.authService.token = response.token;
        this.authService.authNotifier.next(true);
      },
      error => {
        console.log(error);
        this.loginError = true;
        this.authService.authNotifier.next(false);
      },
      () => {
        console.log("login success");
        this.authService.scheduleRefresh();
        this.navCtrl.push(TabsPage);
      },
    );
	}
}

If you don’t get any better answers, I would get rid of all the complicated expiration calculating and refresh scheduling. Due to potential clock skew between client and server, it’s not completely reliable to try to do this client-side in the first place.

What I would instead do is just let the server decide whether the token is valid. On every authenticated HTTP request, your authenticated http service can handle 401 responses by refreshing the token and then reattempting the original request.

How could I make the call again?

service.ts

getSites(): Observable<any> {

  let URL = `${this.serverAPI}/jobsites`;
  return this.authHttp.get(URL)
    .map((response: Response) => { return response.json().jobSites },
      (error: Response) => { console.log(error) })
    .share();
}

component.ts

ionViewDidLoad() {
  this.myService.Sites()
    .subscribe(
      (sites: SitesInterface[]) => this.sites = sites,
      (error: Response) => { console.log(error); });
}

I’m not certain where exactly you would want to put this logic (in case you’re trying to share it amongst get/post/put/&c), but for simplicity let’s assume we’re only dealing with get. I’m thinking of something like this:

get(url: string, options?: RequestOptionsArgs): Observable<Response> {
  let allopts = this._addJwtToOptions(options);
  return this._http.get(url, allopts).catch((err) => {
    if (err.status === 401) {
      return this.refreshToken().mergeMap(() => {
        allopts = this._addJwtToOptions(options);
        return this._http.get(url,allopts));
      });
    } else {
      return Observable.throw(err);
    }
  });
}

I would be sharing the logic across several services. And I would need to share it with get/post/put. I only have two requests which dont need to be authenticated.

Also I’m using auth0’s angular2-jwt library so I dont know if this would work for me

I wonder if it would be easier to catch errors like this but check if the error is 401 and retry

private handleError(error: Response | any) {
    console.log(error);
    return Observable.throw(error.json().error || "Server Error.");
}

Shouldn’t have to. You should be able to have a single AuthenticatedHttp service that all other services and pages call, and only have this retry logic in there.

Then you could do it in a single request() method, and have your AuthenticatedHttp's get(), post(), and put() all call request().

Shouldn’t matter. You’re going to have to write a wrapper around whatever its Http equivalent is anyway, but I think it would be far simpler and more predictable than what you have at the moment.

I can’t see how you would know what to retry in that situation.

Then you could do it in a single request() method, and have your AuthenticatedHttp’s get(), post(), and put() all call request().

I’m not sure what you mean. How would I do something like this?

Internally this is what the stock Angular Http service does.

So could I take that code and just copy and paste the http get method you posted earlier?

lol i have exact the same problem a hour before look at slack :stuck_out_tongue: i discuss with mike about that problem

is it possible to disable the ionic 2 lazy loading deeplinker/router?

Were you able to solve it?