Skip to content

Commit 206b5fe

Browse files
authored
🐛(sync) (Codex) OAuth refresh token race (#1392)
* fix(sync): OAuth refresh token race * Update auth.ts
1 parent f25fa5d commit 206b5fe

2 files changed

Lines changed: 76 additions & 29 deletions

File tree

packages/filesystem/auth.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,30 @@ describe("AuthVerify", () => {
4242
await expect(AuthVerify("onedrive")).resolves.toBe("cached-access");
4343
expect(fetchMock).not.toHaveBeenCalled();
4444
});
45+
46+
it("concurrent expired token verification should share one refresh request", async () => {
47+
await localStorageDAO.saveValue(key, {
48+
accessToken: "old-access",
49+
refreshToken: "old-refresh",
50+
createtime: Date.now() - 3600000 - 1000,
51+
});
52+
53+
const fetchMock = vi.fn().mockResolvedValue({
54+
json: vi.fn().mockResolvedValue({
55+
code: 0,
56+
data: {
57+
token: {
58+
access_token: "new-access",
59+
refresh_token: "new-refresh",
60+
},
61+
},
62+
}),
63+
} as unknown as Response);
64+
vi.stubGlobal("fetch", fetchMock);
65+
66+
await expect(
67+
Promise.all([AuthVerify("onedrive"), AuthVerify("onedrive"), AuthVerify("onedrive")])
68+
).resolves.toEqual(["new-access", "new-access", "new-access"]);
69+
expect(fetchMock).toHaveBeenCalledTimes(1);
70+
});
4571
});

packages/filesystem/auth.ts

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,47 @@ export type Token = {
7373
refreshToken: string;
7474
createtime: number;
7575
};
76+
const refreshTokenPromises: Partial<Record<NetDiskType, Promise<string>>> = {};
77+
78+
function refreshAccessToken(
79+
netDiskType: NetDiskType,
80+
token: Token,
81+
invalid: boolean | undefined,
82+
key: string,
83+
localStorageDAO: LocalStorageDAO
84+
) {
85+
if (refreshTokenPromises[netDiskType]) {
86+
return refreshTokenPromises[netDiskType];
87+
}
88+
89+
const refreshPromiseFn = async () => {
90+
const resp = await RefreshToken(netDiskType, token.refreshToken);
91+
if (resp.code !== 0) {
92+
await localStorageDAO.delete(key);
93+
// 刷新失败,并且标记失效,尝试重新获取token
94+
if (invalid) {
95+
return await AuthVerify(netDiskType);
96+
}
97+
throw new WarpTokenError(new Error(resp.msg));
98+
}
99+
const newToken = {
100+
accessToken: resp.data.token.access_token,
101+
refreshToken: resp.data.token.refresh_token,
102+
createtime: Date.now(),
103+
};
104+
// 更新token
105+
await localStorageDAO.saveValue(key, newToken);
106+
return newToken.accessToken;
107+
};
108+
const refreshPromise: Promise<string> = refreshPromiseFn().finally(() => {
109+
if (refreshTokenPromises[netDiskType] === refreshPromise) {
110+
delete refreshTokenPromises[netDiskType];
111+
}
112+
});
113+
114+
refreshTokenPromises[netDiskType] = refreshPromise;
115+
return refreshPromise;
116+
}
76117

77118
export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
78119
let token: Token | undefined = undefined;
@@ -99,36 +140,16 @@ export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
99140
invalid = false;
100141
await localStorageDAO.saveValue(key, token);
101142
}
102-
// token过期或者失效
103-
const expired = Date.now() >= token.createtime + 3600000;
104-
if (expired || invalid) {
105-
// 大于一小时刷新token
106-
try {
107-
const resp = await RefreshToken(netDiskType, token.refreshToken);
108-
if (resp.code !== 0) {
109-
await localStorageDAO.delete(key);
110-
// 刷新失败,并且标记失效,尝试重新获取token
111-
if (invalid) {
112-
return await AuthVerify(netDiskType);
113-
}
114-
throw new WarpTokenError(new Error(resp.msg));
115-
}
116-
token = {
117-
accessToken: resp.data.token.access_token,
118-
refreshToken: resp.data.token.refresh_token,
119-
createtime: Date.now(),
120-
};
121-
// 更新token
122-
await localStorageDAO.saveValue(key, token);
123-
} catch (e) {
124-
// 已过期或已被服务端判定失效的 token 不能继续回退使用
125-
console.warn(e);
126-
throw e;
127-
}
128-
} else {
129-
return token.accessToken;
143+
// token未过期(一小时内)及有效则保留,不用刷新token
144+
const unexpired = Date.now() < token.createtime + 3600000;
145+
if (unexpired && !invalid) return token.accessToken;
146+
try {
147+
return await refreshAccessToken(netDiskType, token, invalid, key, localStorageDAO);
148+
} catch (e) {
149+
// 已过期或已被服务端判定失效的 token 不能继续回退使用
150+
console.warn(e);
151+
throw e;
130152
}
131-
return token.accessToken;
132153
}
133154

134155
export const netDiskTypeMap: Partial<Record<FileSystemType, NetDiskType>> = {

0 commit comments

Comments
 (0)