Skip to content

Commit 0a244b9

Browse files
fix(oauth): resolve multiple OAuth login issues (smart-mcp-proxy#23)
- Add debug logging for server key generation to diagnose key mismatches - Warn when tokens lack refresh_token (cannot be refreshed) - Improve RefreshManager startup logging for debugging token loading - Fix RefreshOAuthToken to detect dynamic OAuth (Protected Resource Metadata) - Previously only checked static OAuth config - Now also checks for stored tokens from runtime OAuth discovery - Uses GenerateServerKey to match PersistentTokenStore key format Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent aacbd90 commit 0a244b9

4 files changed

Lines changed: 90 additions & 10 deletions

File tree

internal/oauth/persistent_token_store.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,15 @@ func GenerateServerKey(serverName, serverURL string) string {
5454
hashStr := hex.EncodeToString(hash[:])
5555

5656
// Return first 16 characters of hash for readability (still highly unique)
57-
return fmt.Sprintf("%s_%s", serverName, hashStr[:16])
57+
key := fmt.Sprintf("%s_%s", serverName, hashStr[:16])
58+
59+
// Log key generation for debugging server key mismatches
60+
zap.L().Debug("Generated OAuth server key",
61+
zap.String("server_name", serverName),
62+
zap.String("server_url", serverURL),
63+
zap.String("generated_key", key))
64+
65+
return key
5866
}
5967

6068
// GetToken retrieves the OAuth token from persistent storage
@@ -67,11 +75,13 @@ func (p *PersistentTokenStore) GetToken(ctx context.Context) (*client.Token, err
6775
}
6876

6977
p.logger.Debug("🔍 Loading OAuth token from persistent storage",
78+
zap.String("server_name", p.serverName),
7079
zap.String("server_key", p.serverKey))
7180

7281
record, err := p.storage.GetOAuthToken(p.serverKey)
7382
if err != nil {
7483
p.logger.Debug("❌ No stored OAuth token found",
84+
zap.String("server_name", p.serverName),
7585
zap.String("server_key", p.serverKey),
7686
zap.Error(err))
7787
return nil, transport.ErrNoToken
@@ -148,6 +158,19 @@ func (p *PersistentTokenStore) GetToken(ctx context.Context) (*client.Token, err
148158
HasRefreshToken: record.RefreshToken != "",
149159
})
150160

161+
// Warn if returning an expired token without a refresh token - mcp-go cannot refresh this
162+
if isExpired && record.RefreshToken == "" {
163+
p.logger.Warn("⚠️ Returning expired token WITHOUT refresh_token - refresh will fail",
164+
zap.String("server_name", p.serverName),
165+
zap.String("server_key", p.serverKey),
166+
zap.Time("expired_at", record.ExpiresAt))
167+
} else if record.RefreshToken == "" {
168+
p.logger.Warn("⚠️ Token has no refresh_token - cannot be refreshed when it expires",
169+
zap.String("server_name", p.serverName),
170+
zap.String("server_key", p.serverKey),
171+
zap.Time("expires_at", record.ExpiresAt))
172+
}
173+
151174
// Return the token - mcp-go library will check IsExpired() and handle refresh if needed
152175
// For long-lived tokens, we subtract the grace period from ExpiresAt to trigger refresh earlier
153176
// For short-lived tokens, we use the actual expiration to avoid falsely marking them as expired

internal/oauth/refresh_manager.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,38 +191,58 @@ func (m *RefreshManager) Start(ctx context.Context) error {
191191
defer m.mu.Unlock()
192192

193193
if m.started {
194+
m.logger.Debug("RefreshManager already started, skipping")
194195
return nil // Already started
195196
}
196197

197198
// Create a cancellable context for all timers
198199
m.ctx, m.cancel = context.WithCancel(ctx)
199200
m.started = true
200201

201-
m.logger.Info("Starting RefreshManager")
202+
m.logger.Info("RefreshManager.Start() called")
202203

203204
// Track startup refresh stats
204205
var scheduled, immediateRefresh, expired int
205206

206207
// Load existing tokens and schedule refreshes
207208
if m.storage != nil {
209+
m.logger.Debug("Loading OAuth tokens from storage",
210+
zap.Bool("storage_available", true))
211+
208212
tokens, err := m.storage.ListOAuthTokens()
209213
if err != nil {
210-
m.logger.Warn("Failed to load existing tokens", zap.Error(err))
214+
m.logger.Warn("Failed to load existing tokens",
215+
zap.Error(err))
211216
// Continue - we can still handle new tokens
212217
} else {
218+
m.logger.Info("OAuth tokens retrieved from storage",
219+
zap.Int("count", len(tokens)))
213220
// Collect tokens that need immediate refresh (expired access token but valid refresh token)
214221
var tokensToRefresh []string
215222

216223
for _, token := range tokens {
217224
if token == nil || token.ExpiresAt.IsZero() {
225+
m.logger.Debug("Skipping nil or zero-expiry token")
218226
continue
219227
}
220228

221229
serverName := token.GetServerName()
222230
now := time.Now()
223231

232+
// Log each token being processed for debugging
233+
m.logger.Debug("Processing OAuth token",
234+
zap.String("server", serverName),
235+
zap.String("storage_key", token.ServerName),
236+
zap.Time("expires_at", token.ExpiresAt),
237+
zap.Bool("has_refresh_token", token.RefreshToken != ""),
238+
zap.Bool("is_expired", token.ExpiresAt.Before(now)),
239+
zap.Duration("time_until_expiry", token.ExpiresAt.Sub(now)))
240+
224241
if token.ExpiresAt.After(now) {
225242
// Token not expired - schedule proactive refresh at 80% lifetime
243+
m.logger.Debug("Scheduling proactive refresh for non-expired token",
244+
zap.String("server", serverName),
245+
zap.Time("expires_at", token.ExpiresAt))
226246
m.scheduleRefreshLocked(serverName, token.ExpiresAt)
227247
scheduled++
228248
} else if token.RefreshToken != "" {
@@ -269,11 +289,19 @@ func (m *RefreshManager) Start(ctx context.Context) error {
269289

270290
// Execute immediate refreshes asynchronously (after releasing the lock)
271291
if len(tokensToRefresh) > 0 {
292+
m.logger.Info("Starting asynchronous refresh for expired tokens",
293+
zap.Int("count", len(tokensToRefresh)),
294+
zap.Strings("servers", tokensToRefresh))
272295
go m.executeStartupRefreshes(tokensToRefresh)
273296
}
274297
}
298+
} else {
299+
m.logger.Warn("RefreshManager started with nil storage - token persistence disabled")
275300
}
276301

302+
m.logger.Info("RefreshManager startup complete",
303+
zap.Int("schedules_created", len(m.schedules)))
304+
277305
return nil
278306
}
279307

internal/runtime/lifecycle.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ func (r *Runtime) StartBackgroundInitialization() {
4242

4343
// Register token saved callback to wire PersistentTokenStore -> RefreshManager
4444
oauth.GetTokenStoreManager().SetTokenSavedCallback(func(serverName string, expiresAt time.Time) {
45+
r.logger.Info("OnTokenSaved callback fired - scheduling proactive refresh",
46+
zap.String("server", serverName),
47+
zap.Time("expires_at", expiresAt),
48+
zap.Duration("valid_for", time.Until(expiresAt)))
4549
r.refreshManager.OnTokenSaved(serverName, expiresAt)
4650
})
4751
r.logger.Info("Token saved callback registered for proactive refresh")

internal/upstream/manager.go

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2346,7 +2346,10 @@ func (m *Manager) SetUserLoggedOut(serverName string, loggedOut bool) error {
23462346

23472347
// RefreshOAuthToken triggers a token refresh for a specific server.
23482348
// This is called by the RefreshManager for proactive token refresh.
2349-
// TODO: This will be fully implemented in Phase 3 (US1) with RefreshManager integration.
2349+
//
2350+
// OAuth detection checks both:
2351+
// 1. Static config (serverConfig.OAuth) - for servers with pre-configured OAuth
2352+
// 2. Database tokens - for servers using dynamic OAuth discovery (Protected Resource Metadata)
23502353
func (m *Manager) RefreshOAuthToken(serverName string) error {
23512354
m.mu.RLock()
23522355
client, exists := m.clients[serverName]
@@ -2356,18 +2359,40 @@ func (m *Manager) RefreshOAuthToken(serverName string) error {
23562359
return fmt.Errorf("server not found: %s", serverName)
23572360
}
23582361

2359-
// Get the server config to check if it uses OAuth
2362+
// Check if server uses OAuth via either static config or dynamic discovery
23602363
serverConfig := client.GetConfig()
2361-
if serverConfig == nil || serverConfig.OAuth == nil {
2364+
hasStaticOAuth := serverConfig != nil && serverConfig.OAuth != nil
2365+
2366+
// Also check for OAuth tokens in the database (dynamic OAuth discovery)
2367+
// Servers like atlassian-remote, slack discover OAuth requirements at runtime
2368+
// via Protected Resource Metadata and store tokens without OAuth in static config
2369+
hasStoredTokens := false
2370+
if m.storage != nil && serverConfig != nil {
2371+
// IMPORTANT: Use GenerateServerKey to match how PersistentTokenStore stores tokens.
2372+
// Tokens are stored with key = SHA256(serverName|serverURL)[:16], not just serverName.
2373+
serverKey := oauth.GenerateServerKey(serverName, serverConfig.URL)
2374+
token, err := m.storage.GetOAuthToken(serverKey)
2375+
if err == nil && token != nil && token.RefreshToken != "" {
2376+
hasStoredTokens = true
2377+
m.logger.Debug("Found OAuth token in database for dynamic OAuth server",
2378+
zap.String("server", serverName),
2379+
zap.String("server_key", serverKey),
2380+
zap.Bool("has_refresh_token", token.RefreshToken != ""))
2381+
}
2382+
}
2383+
2384+
if !hasStaticOAuth && !hasStoredTokens {
23622385
return fmt.Errorf("server does not use OAuth: %s", serverName)
23632386
}
23642387

2365-
// TODO: Implement actual token refresh in Phase 3
2366-
// For now, force a reconnection which will trigger token refresh if needed
2367-
client.ForceReconnect("manual_token_refresh")
2388+
// Force a reconnection which will trigger token refresh via mcp-go's
2389+
// automatic token refresh when TokenStore provides a refresh token
2390+
client.ForceReconnect("oauth_token_refresh")
23682391

23692392
m.logger.Info("OAuth token refresh requested",
2370-
zap.String("server", serverName))
2393+
zap.String("server", serverName),
2394+
zap.Bool("has_static_oauth", hasStaticOAuth),
2395+
zap.Bool("has_stored_tokens", hasStoredTokens))
23712396

23722397
return nil
23732398
}

0 commit comments

Comments
 (0)