Skip to content

Commit 2ae1791

Browse files
authored
fix: tokens (#113)
### 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. <br><b>Example:</b> User refreshes to a new token and exits Overleaf, the next time he re-opens PaperDebugger, it uses the old refresh token. <br><b>Proposed solution:</b> Update authStore whenever tokens are set. <br> ### 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. <br><b>Example:</b> `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. <br><b>Proposed solution:</b> Use a promise for `refresh()`. <br> <br> Unsure if this fixes the exact problem in #110
1 parent de12255 commit 2ae1791

1 file changed

Lines changed: 25 additions & 9 deletions

File tree

webapp/_webapp/src/libs/apiclient.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { EventEmitter } from "events";
77
import { ErrorCode, ErrorSchema } from "../pkg/gen/apiclient/shared/v1/shared_pb";
88
import { errorToast } from "./toasts";
99
import { storage } from "./storage";
10+
import { useAuthStore } from "../stores/auth-store";
1011

1112
// Exhaustive type check helper - will cause compile error if a case is not handled
1213
const assertNever = (x: never): never => {
@@ -29,6 +30,7 @@ class ApiClient {
2930
private axiosInstance: AxiosInstance;
3031
private refreshToken: string | null;
3132
private onTokenRefreshedEventEmitter: EventEmitter;
33+
private refreshPromise: Promise<void> | null = null;
3234

3335
constructor(baseURL: string, apiVersion: ApiVersion) {
3436
this.axiosInstance = axios.create({
@@ -64,6 +66,8 @@ class ApiClient {
6466
}
6567

6668
setTokens(token: string, refreshToken: string): void {
69+
useAuthStore.getState().setToken(token);
70+
useAuthStore.getState().setRefreshToken(refreshToken);
6771
this.refreshToken = refreshToken;
6872
this.axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${token}`;
6973
}
@@ -89,15 +93,27 @@ class ApiClient {
8993
}
9094

9195
async refresh() {
92-
const response = await this.axiosInstance.post<JsonValue>("/auth/refresh", {
93-
refreshToken: this.refreshToken,
94-
});
95-
const resp = fromJson(RefreshTokenResponseSchema, response.data);
96-
this.setTokens(resp.token, resp.refreshToken);
97-
this.onTokenRefreshedEventEmitter.emit("tokenRefreshed", {
98-
token: resp.token,
99-
refreshToken: resp.refreshToken,
100-
});
96+
if (this.refreshPromise) {
97+
return this.refreshPromise;
98+
}
99+
100+
this.refreshPromise = (async () => {
101+
try {
102+
const response = await this.axiosInstance.post<JsonValue>("/auth/refresh", {
103+
refreshToken: this.refreshToken,
104+
});
105+
const resp = fromJson(RefreshTokenResponseSchema, response.data);
106+
this.setTokens(resp.token, resp.refreshToken);
107+
this.onTokenRefreshedEventEmitter.emit("tokenRefreshed", {
108+
token: resp.token,
109+
refreshToken: resp.refreshToken,
110+
});
111+
} finally {
112+
this.refreshPromise = null;
113+
}
114+
})();
115+
116+
return this.refreshPromise;
101117
}
102118

103119
private async requestWithRefresh(config: AxiosRequestConfig): Promise<JsonValue> {

0 commit comments

Comments
 (0)