Fix refreshing access tokens when laptop suspends

This commit fixes an issue that refresh token requests were being sent
too late if a laptop was suspended, even if the laptop was resumed again
in time to do the refresh.

For example if a laptop was suspended for 10 mins, then the refresh
request would be sent 10 mins later than it should have been and the
access token and refresh token would have already expired before the
refresh request was attempted.

The bug was due to a misunderstanding about how `window.setTimeout()`
works:

If you, say, use `setTimeout` to call a function in 10 minutes time,
then wait 5 minutes, then put the laptop to sleep for an hour, then
wake it up, **it will wait another 5 minutes and then call the
function**.

That is, the time that the laptop spent asleep does not count towards
the timeout, it pauses the timer and then continues the timer again when
the laptop wakes. This will also happen if the browser or OS suspends
the tab or app.

I've verified this behaviour in Chrome and Firefox on Linux, and it also
agrees with [this StackOverflow
answer](http://stackoverflow.com/questions/6346849/what-happens-to-settimeout-when-the-computer-goes-to-sleep/6347336#6347336)
and [the HTML5 spec on
`setTimeout`](https://www.w3.org/TR/2010/WD-html5-20101019/timers.html#timers):
"wait until the Document associated with the method context has been
fully active for a further timeout milliseconds (not necessarily
consecutively)."

This means that if you want to definitely call a function at a given
time (say 55 mins in the future) you can't just use `setTimeout` to set
a single timer for 55 mins like our code was doing - if the laptop is
suspended during those 55 mins then the timer won't go off until 55 mins
+ however long the laptop was suspended for.

To fix this change the code to repeatedly poll, every few seconds,
whether the current time is later than the access token expiry time
minus 5 minutes. If it is, then try to refresh the access token.

I'm using repeated calls to `window.setTimeout()` to implement this,
rather than one call to `window.setInterval()`, because with
`setTimeout()` it's easier to do a couple of things that the previous
implementation was already doing:

1. Not make any more refresh token requests while one is still in flight
2. Stop making further refresh token requests when one fails
parent 7d041803
......@@ -22,6 +22,9 @@ function auth($http, settings) {
* @property {string} accessToken - The access token itself.
* @property {number} expiresIn - The lifetime of the access token,
* in seconds.
* @property {Date} refreshAfter - A time before the access token's expiry
* time, after which the code should
* attempt to refresh the access token.
* @property {string} refreshToken - The refresh token that can be used to
* get a new access token.
*/
......@@ -37,6 +40,12 @@ function auth($http, settings) {
return {
accessToken: data.access_token,
expiresIn: data.expires_in,
// We actually have to refresh the access token _before_ it expires.
// If the access token expires in one hour, this should refresh it in
// about 55 mins.
refreshAfter: new Date(Date.now() + (data.expires_in * 1000 * 0.91)),
refreshToken: data.refresh_token,
};
}
......@@ -79,14 +88,23 @@ function auth($http, settings) {
// Set a timeout to refresh the access token a few minutes before it expires.
function refreshAccessTokenBeforeItExpires(tokenInfo) {
var delay = tokenInfo.expiresIn * 1000;
// The delay, in milliseconds, before we will poll again to see if it's
// time to refresh the access token.
var delay = 30000;
// We actually have to refresh the access token _before_ it expires.
// If the access token expires in one hour, this should refresh it in
// about 55 mins.
delay = Math.floor(delay * 0.91);
// If the token info's refreshAfter time will have passed before the next
// time we poll, then refresh the token this time.
var refreshAfter = tokenInfo.refreshAfter.valueOf() - delay;
function refreshAccessTokenIfNearExpiry() {
if (Date.now() > refreshAfter) {
refreshAccessToken(tokenInfo.refreshToken);
} else {
refreshAccessTokenBeforeItExpires(tokenInfo);
}
}
window.setTimeout(refreshAccessToken, delay, tokenInfo.refreshToken);
window.setTimeout(refreshAccessTokenIfNearExpiry, delay);
}
function tokenGetter() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment