Skip to content

Commit 20666db

Browse files
authored
🐛 fix: fix oidc auth timeout issue on the desktop (lobehub#10025)
* add tests * fix auth timeout issue * update locale * fix tests
1 parent e6fc44b commit 20666db

File tree

2 files changed

+759
-39
lines changed

2 files changed

+759
-39
lines changed

apps/desktop/src/main/controllers/AuthCtr.ts

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,38 @@ const logger = createLogger('controllers:AuthCtr');
1414

1515
/**
1616
* Authentication Controller
17-
* 使用中间页 + 轮询的方式实现 OAuth 授权流程
17+
* Implements OAuth authorization flow using intermediate page + polling mechanism
1818
*/
1919
export default class AuthCtr extends ControllerModule {
2020
/**
21-
* 远程服务器配置控制器
21+
* Remote server configuration controller
2222
*/
2323
private get remoteServerConfigCtr() {
2424
return this.app.getController(RemoteServerConfigCtr);
2525
}
2626

2727
/**
28-
* 当前的 PKCE 参数
28+
* Current PKCE parameters
2929
*/
3030
private codeVerifier: string | null = null;
3131
private authRequestState: string | null = null;
3232

3333
/**
34-
* 轮询相关参数
34+
* Polling related parameters
3535
*/
3636
// eslint-disable-next-line no-undef
3737
private pollingInterval: NodeJS.Timeout | null = null;
3838
private cachedRemoteUrl: string | null = null;
3939

4040
/**
41-
* 自动刷新定时器
41+
* Auto-refresh timer
4242
*/
4343
// eslint-disable-next-line no-undef
4444
private autoRefreshTimer: NodeJS.Timeout | null = null;
4545

4646
/**
47-
* 构造 redirect_uri,确保授权和令牌交换时使用相同的 URI
48-
* @param remoteUrl 远程服务器 URL
49-
* @param includeHandoffId 是否包含 handoff ID(仅在授权时需要)
47+
* Construct redirect_uri, ensuring the same URI is used for authorization and token exchange
48+
* @param remoteUrl Remote server URL
5049
*/
5150
private constructRedirectUri(remoteUrl: string): string {
5251
const callbackUrl = new URL('/oidc/callback/desktop', remoteUrl);
@@ -59,9 +58,12 @@ export default class AuthCtr extends ControllerModule {
5958
*/
6059
@ipcClientEvent('requestAuthorization')
6160
async requestAuthorization(config: DataSyncConfig) {
61+
// 清理任何旧的授权状态
62+
this.clearAuthorizationState();
63+
6264
const remoteUrl = await this.remoteServerConfigCtr.getRemoteServerUrl(config);
6365

64-
// 缓存远程服务器 URL 用于后续轮询
66+
// Cache remote server URL for subsequent polling
6567
this.cachedRemoteUrl = remoteUrl;
6668

6769
logger.info(
@@ -115,7 +117,7 @@ export default class AuthCtr extends ControllerModule {
115117
}
116118

117119
/**
118-
* 启动轮询机制获取凭证
120+
* Start polling mechanism to retrieve credentials
119121
*/
120122
private startPolling() {
121123
if (!this.authRequestState) {
@@ -133,7 +135,7 @@ export default class AuthCtr extends ControllerModule {
133135
// Check if polling has timed out
134136
if (Date.now() - startTime > maxPollTime) {
135137
logger.warn('Credential polling timed out');
136-
this.stopPolling();
138+
this.clearAuthorizationState();
137139
this.broadcastAuthorizationFailed('Authorization timed out');
138140
return;
139141
}
@@ -167,14 +169,14 @@ export default class AuthCtr extends ControllerModule {
167169
}
168170
} catch (error) {
169171
logger.error('Error during credential polling:', error);
170-
this.stopPolling();
172+
this.clearAuthorizationState();
171173
this.broadcastAuthorizationFailed('Polling error: ' + error.message);
172174
}
173175
}, pollInterval);
174176
}
175177

176178
/**
177-
* 停止轮询
179+
* Stop polling
178180
*/
179181
private stopPolling() {
180182
if (this.pollingInterval) {
@@ -184,18 +186,30 @@ export default class AuthCtr extends ControllerModule {
184186
}
185187

186188
/**
187-
* 启动自动刷新定时器
189+
* 清理授权状态
190+
* 在开始新的授权流程前或授权失败/超时后调用
191+
*/
192+
private clearAuthorizationState() {
193+
logger.debug('Clearing authorization state');
194+
this.stopPolling();
195+
this.codeVerifier = null;
196+
this.authRequestState = null;
197+
this.cachedRemoteUrl = null;
198+
}
199+
200+
/**
201+
* Start auto-refresh timer
188202
*/
189203
private startAutoRefresh() {
190-
// 先停止现有的定时器
204+
// Stop existing timer first
191205
this.stopAutoRefresh();
192206

193-
const checkInterval = 2 * 60 * 1000; // 每 2 分钟检查一次
207+
const checkInterval = 2 * 60 * 1000; // Check every 2 minutes
194208
logger.debug('Starting auto-refresh timer');
195209

196210
this.autoRefreshTimer = setInterval(async () => {
197211
try {
198-
// 检查 token 是否即将过期 (提前 5 分钟刷新)
212+
// Check if token is expiring soon (refresh 5 minutes in advance)
199213
if (this.remoteServerConfigCtr.isTokenExpiringSoon()) {
200214
const expiresAt = this.remoteServerConfigCtr.getTokenExpiresAt();
201215
logger.info(
@@ -208,7 +222,7 @@ export default class AuthCtr extends ControllerModule {
208222
this.broadcastTokenRefreshed();
209223
} else {
210224
logger.error(`Auto-refresh failed: ${result.error}`);
211-
// 如果自动刷新失败,停止定时器并清除 token
225+
// If auto-refresh fails, stop timer and clear token
212226
this.stopAutoRefresh();
213227
await this.remoteServerConfigCtr.clearTokens();
214228
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
@@ -222,7 +236,7 @@ export default class AuthCtr extends ControllerModule {
222236
}
223237

224238
/**
225-
* 停止自动刷新定时器
239+
* Stop auto-refresh timer
226240
*/
227241
private stopAutoRefresh() {
228242
if (this.autoRefreshTimer) {
@@ -233,44 +247,44 @@ export default class AuthCtr extends ControllerModule {
233247
}
234248

235249
/**
236-
* 轮询获取凭证
237-
* 直接发送 HTTP 请求到远程服务器
250+
* Poll for credentials
251+
* Sends HTTP request directly to remote server
238252
*/
239253
private async pollForCredentials(): Promise<{ code: string; state: string } | null> {
240254
if (!this.authRequestState || !this.cachedRemoteUrl) {
241255
return null;
242256
}
243257

244258
try {
245-
// 使用缓存的远程服务器 URL
259+
// Use cached remote server URL
246260
const remoteUrl = this.cachedRemoteUrl;
247261

248-
// 构造请求 URL
262+
// Construct request URL
249263
const url = new URL('/oidc/handoff', remoteUrl);
250264
url.searchParams.set('id', this.authRequestState);
251265
url.searchParams.set('client', 'desktop');
252266

253267
logger.debug(`Polling for credentials: ${url.toString()}`);
254268

255-
// 直接发送 HTTP 请求
269+
// Send HTTP request directly
256270
const response = await fetch(url.toString(), {
257271
headers: {
258272
'Content-Type': 'application/json',
259273
},
260274
method: 'GET',
261275
});
262276

263-
// 检查响应状态
277+
// Check response status
264278
if (response.status === 404) {
265-
// 凭证还未准备好,这是正常情况
279+
// Credentials not ready yet, this is normal
266280
return null;
267281
}
268282

269283
if (!response.ok) {
270284
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
271285
}
272286

273-
// 解析响应数据
287+
// Parse response data
274288
const data = (await response.json()) as {
275289
data: {
276290
id: string;
@@ -511,15 +525,15 @@ export default class AuthCtr extends ControllerModule {
511525
}
512526

513527
/**
514-
* 应用启动后初始化
528+
* Initialize after app is ready
515529
*/
516530
afterAppReady() {
517531
logger.debug('AuthCtr initialized, checking for existing tokens');
518532
this.initializeAutoRefresh();
519533
}
520534

521535
/**
522-
* 清理所有定时器
536+
* Clean up all timers
523537
*/
524538
cleanup() {
525539
logger.debug('Cleaning up AuthCtr timers');
@@ -528,59 +542,59 @@ export default class AuthCtr extends ControllerModule {
528542
}
529543

530544
/**
531-
* 初始化自动刷新功能
532-
* 在应用启动时检查是否有有效的 token,如果有就启动自动刷新定时器
545+
* Initialize auto-refresh functionality
546+
* Checks for valid token at app startup and starts auto-refresh timer if token exists
533547
*/
534548
private async initializeAutoRefresh() {
535549
try {
536550
const config = await this.remoteServerConfigCtr.getRemoteServerConfig();
537551

538-
// 检查是否配置了远程服务器且处于活动状态
552+
// Check if remote server is configured and active
539553
if (!config.active || !config.remoteServerUrl) {
540554
logger.debug(
541555
'Remote server not active or configured, skipping auto-refresh initialization',
542556
);
543557
return;
544558
}
545559

546-
// 检查是否有有效的访问令牌
560+
// Check if valid access token exists
547561
const accessToken = await this.remoteServerConfigCtr.getAccessToken();
548562
if (!accessToken) {
549563
logger.debug('No access token found, skipping auto-refresh initialization');
550564
return;
551565
}
552566

553-
// 检查是否有过期时间信息
567+
// Check if token expiration time exists
554568
const expiresAt = this.remoteServerConfigCtr.getTokenExpiresAt();
555569
if (!expiresAt) {
556570
logger.debug('No token expiration time found, skipping auto-refresh initialization');
557571
return;
558572
}
559573

560-
// 检查 token 是否已经过期
574+
// Check if token has already expired
561575
const currentTime = Date.now();
562576
if (currentTime >= expiresAt) {
563577
logger.info('Token has expired, attempting to refresh it');
564578

565-
// 尝试刷新 token
579+
// Attempt to refresh token
566580
const refreshResult = await this.remoteServerConfigCtr.refreshAccessToken();
567581
if (refreshResult.success) {
568582
logger.info('Token refresh successful during initialization');
569583
this.broadcastTokenRefreshed();
570-
// 重新启动自动刷新定时器
584+
// Restart auto-refresh timer
571585
this.startAutoRefresh();
572586
return;
573587
} else {
574588
logger.error(`Token refresh failed during initialization: ${refreshResult.error}`);
575-
// 只有在刷新失败时才清除 token 并要求重新授权
589+
// Clear token and require re-authorization only on refresh failure
576590
await this.remoteServerConfigCtr.clearTokens();
577591
await this.remoteServerConfigCtr.setRemoteServerConfig({ active: false });
578592
this.broadcastAuthorizationRequired();
579593
return;
580594
}
581595
}
582596

583-
// 启动自动刷新定时器
597+
// Start auto-refresh timer
584598
logger.info(
585599
`Token is valid, starting auto-refresh timer. Token expires at: ${new Date(expiresAt).toISOString()}`,
586600
);

0 commit comments

Comments
 (0)