Skip to content

fix: tokens#113

Open
kah-seng wants to merge 1 commit intomainfrom
fix/tokens
Open

fix: tokens#113
kah-seng wants to merge 1 commit intomainfrom
fix/tokens

Conversation

@kah-seng
Copy link
Member

@kah-seng kah-seng commented Feb 5, 2026

1. Local Storage Not Updated

Access tokens and refresh tokens are only saved to local storage on login, subsequent token refreshes do not update local storage.

Example: User refreshes to a new token and exits Overleaf, the next time he re-opens PaperDebugger, it uses the old refresh token.

Proposed solution: Update authStore whenever tokens are set.


2. Race Conditions When Refreshing

PaperDebugger often calls multiple endpoints at the same time, which results in a race condition if the token needs to be refreshed.

Example: v2/chats/models and v2/chats/conversations are called at the same time, and the access token needs refreshing, the refresh endpoint is called twice. In some occasions, the frontend uses the 2nd refresh token received which differs from the one stored in the backend. This can be easily reproduced by setting the JWT expiration in the backend to a very short time.

Proposed solution: Use a promise for refresh().



Unsure if this fixes the exact problem in #110

@kah-seng kah-seng linked an issue Feb 5, 2026 that may be closed by this pull request
Copy link
Member

@4ndrelim 4ndrelim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was investigating the bug too but saw you raised a PR before i did anything. So left some comments based on my udnerstanding. Seems like the right approach! Ill try to simulate some possible set-ups and test with ur fix.

}

setTokens(token: string, refreshToken: string): void {
useAuthStore.getState().setToken(token);
Copy link
Member

@4ndrelim 4ndrelim Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May i check if im understanding the situation right. This fix is for 401 error?

In particular, when we refresh due to 401 error from sending a request with expired tokens, we get the newly generated tokens but this was never updated to auth-store ( and not written to browser localStorage). And so, on the next session, e.g. page reload, auth-store loads the stale tokens leading to 401 token expiry error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's my understanding at least

token: resp.token,
refreshToken: resp.refreshToken,
});
if (this.refreshPromise) {
Copy link
Member

@4ndrelim 4ndrelim Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice dedup logic. Yes this should address the multiple async calls to refresh, leading to multiple redundant token generation, and possible 401 bug across sessions.

This is actually rather tricky to test though since i think the bug is probabilistic? i believe for the current session, neither caller (your e.g. v2/chats/models and v2/chats/conversations) will bug out since auth-store uses last write valid token. It is only when caller_A and caller_B each calls refresh() and backend mongo receives a different set of token from what was written to browser localStorage.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, it's definitely not consistent. I was able to reproduce it a few (rare) times by lowering the expiration time of the token, and then switching between the settings and chat tabs. Although I am not 100% sure if it is the problem stated in #110

From my testing, my understanding is that it is possible for either caller_A or caller_B to bug out in the current session.

For example, caller_A and caller_B calls refresh() with token_A, there were a few possibilities that I encountered:

  1. caller_A refreshes to token_B and the API call is retried, caller_B fails to refresh with token_A because the backend is updated to token_B, and this API call is not retried and fails.
  2. caller_A receives a new token_B and caller_B receives a new token_C at the same time. Assuming the frontend receives token_C latest, it stores it in its ApiClient instance. However, in some rare occurrences, the backend stores token_B instead. This causes subsequent API calls to fail. Coincidentally, this happened again literally as I was typing this.
  3. Same as the above but the backend stores the same token as the frontend, so subsequent API calls succeed as per normal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah good catch and test. Yeah but i think the reason the current session bugs out its because the token expired? That is when mongo backend realizes it has a different token from what was written to browser localStorage.

Let me know if im off but i think if the expiration is long (or default), apiclient will simply use the last write token from the multiple refresh() calls and so the bug does not surface. It might surface after token expiry (e.g. 1 day later) when last write token is written to mongo, but no guarantee which token is written to browser localStorage due to network uncertainties (ie both tokens go through auth-store but we don't know which one might be written last to browser). This seems to correspond with past experience where occasionally, logging in a 1-2 days later, the bubble for invalid token somehow repeatedly pops up.

Yup, setting a very small expiration time will increase likelihood of replicating this bug. nice!

As for #110, im not sure if this resolves it either. It seems @wjiayis issue might be elsewhere, specifically latency from API and context. But she will know better so tagging her for inputs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feel free to mark it ready once you're done and i will test it before approving.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's my understanding as well. Just that I believe (correct me if I'm wrong) prior to this change, localStorage is actually never written to except right after a new login, hence the bug below:

Example: User refreshes to a new token and exits Overleaf, the next time he re-opens PaperDebugger, it uses the old refresh token.

I think I have nothing else to add for this aside from maybe @wjiayis's inputs, will mark it ready for now, thank you

@kah-seng kah-seng marked this pull request as ready for review February 6, 2026 11:49
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.

[BUG] Token expiration occurs more frequently than expected

2 participants