Skip to content

🔒 Improve token handling in TokenActor: retry on failure and clear pr…#87

Merged
bitbacchus merged 1 commit intomainfrom
feature/token-timing
Jun 6, 2025
Merged

🔒 Improve token handling in TokenActor: retry on failure and clear pr…#87
bitbacchus merged 1 commit intomainfrom
feature/token-timing

Conversation

@bitbacchus
Copy link
Member

Problem

In the previous implementation, we were polling the refresh endpoint at a fixed 1-minute interval regardless of when the access token actually expired. Concretely:

  • After login, we scheduled a recurring job (e.g. via scheduleRecurring(unit(minutes(1)), …)) to hit /auth/refresh every 60 seconds.

  • The access token itself had a 5-minute lifetime, but because our refresh job ran on a rigid 1-minute cadence, there was a risk that a network hiccup or timer drift would push the next /auth/refresh call out past the actual expiry time (e.g. calling at 14:01:27 when the token expired at 14:01:26).

  • If that happened, the token would already be invalid by the time the client called refresh, leading to a gap in which the frontend held a stale (expired) access token, causing 401 errors and unexpected logouts.

Users reported intermittent “timeout” or forced re-login errors. Examining the logs showed lines like:

Setting expiry to 2025-06-03T11:01:26.000Z 2025-06-03T11:26:26.000Z
… (minutes of pings, but no refresh)
GET /auth/refresh? 401 (because it was already past 11:01:26)

Polling every 60 s failed to guarantee “always before expiry,” especially under throttled conditions or slight timer drift.

Solution

We switch from “fixed-interval polling” to “dynamic scheduling based on the actual expiresAt timestamp.” The main changes are:

  1. Capture expiresAt exactly from the backend’s login/refresh response.

    • Instead of using a hardcoded 5 minutes, we now read the expiresAt value that the backend logs (or returns) when it issues a new access token.
    • That timestamp is parsed into a JavaScript Date (via itu-utils) so we know precisely how many milliseconds remain until expiry.
  2. Use a one-time setTimeout() (rather than setInterval()) that fires just before real expiration.

    • As soon as we get a new access token with exp = <tokenExp>, we compute:

      const now = Date.now();
      const msUntilExpiry = tokenExp.getTime() - now;
      const safetyMarginMs = unit(seconds(30));  // e.g. 30 s before actual expiry
      const timeoutDuration = Math.max(msUntilExpiry - safetyMarginMs, 0);
    • Then we call:

      setTimeout(() => this.triggerRefresh(), timeoutDuration);
    • In other words, instead of issuing /auth/refresh every 60 s, we schedule exactly one callback at (expiresAt − 30 s).

  3. On refresh, repeat the above logic with the newly returned expiresAt.

    • Once triggerRefresh() hits /auth/refresh and receives a new access token, we again read its expiresAt timestamp and schedule the next setTimeout() relative to that.
    • This guarantees that—even if /auth/refresh takes extra time (network hiccup) or the frontend’s event loop was delayed—the call still happens before the real token expiry (thanks to the 30 s safety margin).
  4. Cancel any leftover timers when refreshing or logging out.

    • If for any reason the user logs out or the refresh token expires, we call clearTimeout() on the pending timer handle. This prevents stale callbacks from firing after we no longer hold a valid token.

Summary of Changes

Removed

this.refreshIntervalHandle = setInterval(
  () => this.callRefreshEndpoint(),
  unit(minutes(1))
);

Added

  1. A function scheduleRefresh(expiryDate: Date) that computes a one-time timeout:
const timeoutDuration = Math.max(
  expiryDate.getTime() - Date.now() - safetyMarginMs,
  0
);
this.refreshTimeoutHandle = setTimeout(() => this.triggerRefresh(), timeoutDuration);

  1. In the refresh callback (triggerRefresh()), parsing the new expiresAt from refreshResponse and calling scheduleRefresh(newExpiry) again—ensuring a continuous “refresh-just-before-expiry” loop instead of a blind 60 s interval.

  2. Logic to clear any pending refreshTimeoutHandle on logout or when scheduling a new refresh.

Why it works:

  • We compute timeoutDuration = (expiresAt − now − 30 s).

  • Even if the browser is throttled or the network is slow, that 30 s window guarantees we still call /auth/refresh before the token truly expires.

  • Once the backend issues a new token, it sends back its own expiresAt, which we immediately use to schedule the next refresh.

@bitbacchus bitbacchus self-assigned this Jun 6, 2025
@bitbacchus bitbacchus requested a review from katrinmeyer June 6, 2025 17:18
@bitbacchus bitbacchus merged commit fe753d5 into main Jun 6, 2025
4 checks passed
@bitbacchus bitbacchus deleted the feature/token-timing branch June 6, 2025 21:37
bitbacchus added a commit that referenced this pull request Jun 6, 2025
* Hotfix Can only click on stats detaiils when availible (#85)

* update dependencies

* adds loading spinner to dashboard

* Hotfix: Stats details can only be clicked when stats avail.

* Revert "Hotfix Can only click on stats detaiils when availible (#85)" (#86)

This reverts commit 80a01a1.

* Delete .github/workflows/deploy-check.yml

Updated for Docker deployment

Logrotate function explicitly returns 0 now.

* 🔒 Improve token handling in TokenActor: retry on failure and clear previous timeout (#87)

* Update deploy-test.yml

* Version 1.6.2
bitbacchus added a commit that referenced this pull request Jun 10, 2025
…ization (#89)

* PR version 1.6.2 (#88)

* Hotfix Can only click on stats detaiils when availible (#85)

* update dependencies

* adds loading spinner to dashboard

* Hotfix: Stats details can only be clicked when stats avail.

* Revert "Hotfix Can only click on stats detaiils when availible (#85)" (#86)

This reverts commit 80a01a1.

* Delete .github/workflows/deploy-check.yml

Updated for Docker deployment

Logrotate function explicitly returns 0 now.

* 🔒 Improve token handling in TokenActor: retry on failure and clear previous timeout (#87)

* Update deploy-test.yml

* Version 1.6.2

* 🩹 fix(Root): defer auth cookie check to useEffect after actor initialization

- Move the inline  →  redirect out of the render path
- Perform the login‐redirect in a  that runs once after
- Prevents spurious redirects on re‐renders (e.g. when using React DevTools “pause”)
bitbacchus added a commit that referenced this pull request Jun 11, 2025
* Hotfix Can only click on stats detaiils when availible (#85)

* update dependencies

* adds loading spinner to dashboard

* Hotfix: Stats details can only be clicked when stats avail.

* Revert "Hotfix Can only click on stats detaiils when availible (#85)" (#86)

This reverts commit 80a01a1.

* Create deploy-test.yml

* Update deploy-test.yml

* Update deploy-test.yml

* Update deploy-test.yml

* Delete .github/workflows/deploy-check.yml

* Update deployment.sh

Updated for Docker deployment

* Update deployment.sh

Logrotate function explicitly returns 0 now.

* 🔒 Improve token handling in TokenActor: retry on failure and clear previous timeout (#87)

* Update deploy-test.yml

* Update deploy-test.yml

* Update deployment.sh

* Version 1.6.2

* version 1.6.2

* 🐛 fix(Root): defer auth cookie check to useEffect after actor initialization (#89)

* PR version 1.6.2 (#88)

* Hotfix Can only click on stats detaiils when availible (#85)

* update dependencies

* adds loading spinner to dashboard

* Hotfix: Stats details can only be clicked when stats avail.

* Revert "Hotfix Can only click on stats detaiils when availible (#85)" (#86)

This reverts commit 80a01a1.

* Delete .github/workflows/deploy-check.yml

Updated for Docker deployment

Logrotate function explicitly returns 0 now.

* 🔒 Improve token handling in TokenActor: retry on failure and clear previous timeout (#87)

* Update deploy-test.yml

* Version 1.6.2

* 🩹 fix(Root): defer auth cookie check to useEffect after actor initialization

- Move the inline  →  redirect out of the render path
- Perform the login‐redirect in a  that runs once after
- Prevents spurious redirects on re‐renders (e.g. when using React DevTools “pause”)

* 🐛 Feature/prevent question details without data (#90)

 🐛 Add ternary operator to conditionally disable question details button

* Feature/docker debian slim wkhtmltopdf install (#91)

* chore(docker): switch to node:20-slim and install wkhtmltopdf
- solves crash during export

* Feature/feature/root add spinner on init (#92)

* feat(Root): show spinner while initializing and refactor auth redirect

* :chore: issues with auto-deploy on the test server

* ✨ chore: issues with auto-deploy to testserver

* Update deploy-test.yml

* ✨ chore: issues with auto-deploy to testserver

* version bump
bitbacchus added a commit that referenced this pull request Jun 23, 2025
* Hotfix Can only click on stats details when available (#85)

* update dependencies

* adds loading spinner to dashboard

* Hotfix: Stats details can only be clicked when stats avail.

* Revert "Hotfix Can only click on stats detaiils when availible (#85)" (#86)

This reverts commit 80a01a1.

* Create deploy-test.yml

* Update deploy-test.yml

* Update deploy-test.yml

* Update deploy-test.yml

* Delete .github/workflows/deploy-check.yml

* Update deployment.sh

Updated for Docker deployment

* Update deployment.sh

Logrotate function explicitly returns 0 now.

* 🔒 Improve token handling in TokenActor: retry on failure and clear previous timeout (#87)

* Update deploy-test.yml

* Update deploy-test.yml

* Update deployment.sh

* Version 1.6.2

* version 1.6.2

* 🐛 fix(Root): defer auth cookie check to useEffect after actor initialization (#89)

* PR version 1.6.2 (#88)

* Hotfix Can only click on stats detaiils when availible (#85)

* update dependencies

* adds loading spinner to dashboard

* Hotfix: Stats details can only be clicked when stats avail.

* Revert "Hotfix Can only click on stats detaiils when availible (#85)" (#86)

This reverts commit 80a01a1.

* Delete .github/workflows/deploy-check.yml

Updated for Docker deployment

Logrotate function explicitly returns 0 now.

* 🔒 Improve token handling in TokenActor: retry on failure and clear previous timeout (#87)

* Update deploy-test.yml

* Version 1.6.2

* 🩹 fix(Root): defer auth cookie check to useEffect after actor initialization

- Move the inline  →  redirect out of the render path
- Perform the login‐redirect in a  that runs once after
- Prevents spurious redirects on re‐renders (e.g. when using React DevTools “pause”)

* 🐛 Feature/prevent question details without data (#90)

 🐛 Add ternary operator to conditionally disable question details button

* Feature/docker debian slim wkhtmltopdf install (#91)

* chore(docker): switch to node:20-slim and install wkhtmltopdf
- solves crash during export

* Feature/feature/root add spinner on init (#92)

* feat(Root): show spinner while initializing and refactor auth redirect

* :chore: issues with auto-deploy on the test server

* ✨ chore: issues with auto-deploy to testserver

* Update deploy-test.yml

* ✨ chore: issues with auto-deploy to testserver

* version bump

* 🐛 fix(authRefresh): break infinite refresh loop by returning exipry timestamp (#94)

* 🐛 fix(authRefresh): break infinite refresh loop by returning expiry timestamp

* version bump
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant