From 8406355e7fa30b80499123fc8a166a429e14a296 Mon Sep 17 00:00:00 2001 From: fantiu Date: Wed, 1 Apr 2026 14:48:58 +0800 Subject: [PATCH] fix(auth): prevent auth code expiration error on OAuth callback page refresh --- internal/auth/oauth_helpers.go | 8 ++-- internal/auth/oauth_provider.go | 78 +++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/internal/auth/oauth_helpers.go b/internal/auth/oauth_helpers.go index cd9f744d..8ac17c7a 100644 --- a/internal/auth/oauth_helpers.go +++ b/internal/auth/oauth_helpers.go @@ -339,9 +339,9 @@ select:focus{outline:none;border-color:#1890ff;box-shadow:0 0 0 2px rgba(24,144, .link:hover{text-decoration:underline} .success-msg{display:none;width:100%;min-height:36px;gap:12px;padding:8px 12px;margin-top:20px; background:linear-gradient(0deg,rgba(0,102,255,0.12) 0%,rgba(0,102,255,0.12) 100%),linear-gradient(0deg,#FFFFFF 0%,#FFFFFF 100%); - border-radius:12px;align-items:flex-start;justify-content:flex-start} -.success-msg-icon{width:16px;height:16px;flex-shrink:0;margin-top:3px} -.success-msg-text{flex:1;color:#181C1F;font-size:14px;line-height:22px} + border-radius:12px;align-items:center;justify-content:flex-start} +.success-msg-icon{width:16px;height:16px;flex-shrink:0} +.success-msg-text{flex:1;color:#181C1F;font-size:14px;line-height:22px;text-align:left} .error-msg{color:#ff4d4f;font-size:14px;margin-top:8px;display:none} .loading{display:inline-block;width:16px;height:16px;border:2px solid #fff;border-top-color:transparent; border-radius:50%;animation:spin 0.8s linear infinite;margin-right:8px;vertical-align:middle} @@ -352,7 +352,7 @@ select:focus{outline:none;border-color:#1890ff;box-shadow:0 0 0 2px rgba(24,144,

该组织尚未开启 CLI 数据访问权限

你所选择的组织管理员尚未开启「允许成员通过 CLI 访问其个人数据」的权限。

- +
diff --git a/internal/auth/oauth_provider.go b/internal/auth/oauth_provider.go index cb819f4d..290323c7 100644 --- a/internal/auth/oauth_provider.go +++ b/internal/auth/oauth_provider.go @@ -131,10 +131,11 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro // Shared state for API handlers (protected by mutex) var ( callbackToken *TokenData - callbackProcessed bool + callbackProcessedCode string // The auth code that has been successfully processed callbackAuthDisabled bool callbackApplySent bool // Whether apply request was sent callbackSelectedAdminId string // Selected admin ID for apply + callbackCodeInProgress string // Code currently being processed (to prevent concurrent exchange) callbackTokenMu sync.Mutex ) @@ -146,23 +147,48 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro code = r.URL.Query().Get("code") } - // Check if this is a page refresh (no code) and callback was already processed + // Check state and handle page refresh or concurrent requests callbackTokenMu.Lock() - if code == "" && callbackProcessed { + processedCode := callbackProcessedCode + processedAuthDisabled := callbackAuthDisabled + codeInProgress := callbackCodeInProgress + hasToken := callbackToken != nil + + // Case 1: This code was already successfully processed - show cached page + if code != "" && code == processedCode { + callbackTokenMu.Unlock() w.Header().Set("Content-Type", "text/html; charset=utf-8") - if callbackAuthDisabled { + if processedAuthDisabled { _, _ = fmt.Fprint(w, notEnabledHTML) } else { _, _ = fmt.Fprint(w, successHTML) } - callbackTokenMu.Unlock() return } - // Reset state for new authorization (user switched org) - if code != "" && callbackProcessed { - callbackProcessed = false - callbackApplySent = false - callbackSelectedAdminId = "" + + // Case 2: This code is being processed by another request - show wait page + if code != "" && code == codeInProgress { + callbackTokenMu.Unlock() + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = fmt.Fprint(w, `

正在处理授权,请稍候...

`) + return + } + + // Case 3: No code but we have a processed token - show cached page + if code == "" && hasToken { + callbackTokenMu.Unlock() + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if processedAuthDisabled { + _, _ = fmt.Fprint(w, notEnabledHTML) + } else { + _, _ = fmt.Fprint(w, successHTML) + } + return + } + + // Case 4: New code - mark as in-progress and process + if code != "" { + callbackCodeInProgress = code } callbackTokenMu.Unlock() @@ -176,20 +202,13 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro return } - // Exchange code for token immediately in callback + // Exchange code for token tokenData, exchangeErr := p.exchangeCode(ctx, code) if exchangeErr != nil { - // Check if we already have a processed state (authCode reused on refresh) + // Clear in-progress state on error callbackTokenMu.Lock() - if callbackProcessed { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - if callbackAuthDisabled { - _, _ = fmt.Fprint(w, notEnabledHTML) - } else { - _, _ = fmt.Fprint(w, successHTML) - } - callbackTokenMu.Unlock() - return + if callbackCodeInProgress == code { + callbackCodeInProgress = "" } callbackTokenMu.Unlock() @@ -202,14 +221,25 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro return } + // Mark as processed immediately after successful exchange + callbackTokenMu.Lock() + previouslyProcessed := callbackProcessedCode != "" + callbackToken = tokenData + callbackProcessedCode = code // Remember this code was successfully processed + callbackCodeInProgress = "" // Clear in-progress state + // Reset apply state for new authorization (user switched org) + if previouslyProcessed { + callbackApplySent = false + callbackSelectedAdminId = "" + } + callbackTokenMu.Unlock() + // Check CLI auth enabled status authStatus, statusErr := p.CheckCLIAuthEnabled(ctx, tokenData.AccessToken) cliAuthDisabled := statusErr == nil && authStatus.Success && !authStatus.Result.CLIAuthEnabled - // Store token and state for API handlers and refresh handling + // Update CLI auth disabled state callbackTokenMu.Lock() - callbackToken = tokenData - callbackProcessed = true callbackAuthDisabled = cliAuthDisabled callbackTokenMu.Unlock()