Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shopware/administration: auto token refresh fails when tab is heavily throttled by chrome, causing logout #3572

Open
rmobis opened this issue Feb 16, 2024 · 1 comment
Labels

Comments

@rmobis
Copy link

rmobis commented Feb 16, 2024

PHP Version

8.1.13

Shopware Version

6.5.8.4

Expected behaviour

hiding a tab for long periods of time should automatically refresh the authentication token every time, allowing the user to easily return to his work whenever he wishes

Actual behaviour

hiding a tab for long periods of time should eventually may lead to Chrome heavily throttling scheduled tasks for up to a minute, eventually delaying the refresh of a token until it's no longer valid and logging out the user from the admin dashboard and potentially causing loss of work

How to reproduce

  1. Make sure the frosh/tools package is installed¹
  2. Open a new incognito window in your browser and navigate to the admin dashboard
  3. Log in into the admin dashboard, selecting the Stay logged in option
  4. Open a new empty tab in the same window, focus it and minimize the window for 30 minutes to an hour
  5. Come back to see you've been logged out

This does involve a bit of chance, so might need a few tries to reproduce the error, but you'll easily notice through the network tab that the token is being renewed every 10 minutes instead of the expected 5 minutes.

¹ I suggest installing frosh/tools because it's the easiest and quickest way I found to reproduce, but this can surely be reproduced with any plugin or piece of code that regularly calls loginService.isLoggedIn() inside a scheduled call. See root cause below for why.

Root cause

This is two-fold:

  1. In version 88, Chrome introduced Heavy throttling of chained JS timers, which means under certain conditions it is possible that a scheduled task (setTimeout/setInterval) is delayed for up to a whole minute.
  2. Every time loginService.isLoggedIn() is called, the timeout for the auto token refresh is deleted and a new one is created, scheduled to happen in half the time left for the refresh token expiration. This can be seen in https://github.com/shopware/shopware/blob/trunk/src/Administration/Resources/app/administration/src/core/service/login.service.ts#L412.

Normally, this wouldn't be a problem, but means that if you had some plugin or other code repeatedly checking for loginService.isLoggedIn(), it would keep cancelling the refresh token task and delaying it, little by little, until the task is scheduled for less than a minute into the future. Put that repeating code inside a setInterval or chained setTimeout and you've met the criteria for heavy throttling, which might cause the refresh attempt to be delayed for up to a minute, until after the token is no longer valid. frosh/tools does just that inside its health status utility. Every 60s it checks if loginService.isLoggedIn() and performs some actions.

Please note though that this is not necessarily an issue with frosh/tools as this is a rather common pattern, but rather a bug in how Shopware core handles the auto renewal. The issue could occur with less frequent calls too as the core code also uses chained timers. It's just a lot more noticeable with it. Here's an example timeline:

  • 0m00s: token is generated, expires in 10 minutes; refresh is scheduled for 5m00s
  • 0m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 5m15s
  • 1m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 5m45s
  • 2m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 6m15s
  • 3m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 6m45s
  • 4m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 7m15s
  • 5m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 7m45s
  • 6m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 8m15s
  • 7m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 8m45s
  • 8m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 9m15s
  • 9m30s: loginService.isLoggedIn() is called; refresh is rescheduled for 9m45s
  • 10m30s: refresh happens only now because of throttling; refresh token is no longer valid so user is logged out

Possible solution

The easiest solution is to just avoid scheduling the renewal to less than a minute before the token expires. The following patch does just that. Let me know if that's an appropriate solution and if so I can open a PR.

diff --git a/src/Administration/Resources/app/administration/src/core/service/login.service.ts b/src/Administration/Resources/app/administration/src/core/service/login.service.ts
index 51cca3a7..ca67f2ea 100644
--- a/src/Administration/Resources/app/administration/src/core/service/login.service.ts
+++ b/src/Administration/Resources/app/administration/src/core/service/login.service.ts
@@ -267,9 +267,17 @@ export default function createLoginService(
 
         const timeUntilExpiry = expiryTimestamp * 1000 - Date.now();
 
+        // in Chrome, and possibly other browsers, timeouts may be delayed for up to 1 minute in
+        // inactive tabs, so we refresh it immediately if it's less than 1m15s away, to be safe
+        // https://developer.chrome.com/blog/timer-throttling-in-chrome-88
+        if (timeUntilExpiry < 75000) {
+            void refreshToken();
+            return;
+        }
+
         autoRefreshTokenTimeoutId = setTimeout(() => {
             void refreshToken();
-        }, timeUntilExpiry / 2);
+        }, timeUntilExpiry - 75000);
     }
 
     /**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant