-
Notifications
You must be signed in to change notification settings - Fork 897
fix(auth): auto-refresh expired Anthropic OAuth tokens #3510
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -78,11 +78,28 @@ export class ChatService { | |||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| getAnthropicAuthStatus(): AuthStatus { | ||||||||||||||
| async getAnthropicAuthStatus(): Promise<AuthStatus> { | ||||||||||||||
| const authStorage = this.getAuthStorage(); | ||||||||||||||
| authStorage.reload(); | ||||||||||||||
| const storedCredential = authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID); | ||||||||||||||
| let storedCredential = authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID); | ||||||||||||||
| const hasManagedOAuth = storedCredential?.type === "oauth"; | ||||||||||||||
|
|
||||||||||||||
| // If managed OAuth is past its expiry, give mastracode a chance to | ||||||||||||||
| // refresh it before downgrading status to "expired". Mastracode's | ||||||||||||||
| // getApiKey uses the stored refresh token via the anthropic provider. | ||||||||||||||
| if ( | ||||||||||||||
| storedCredential?.type === "oauth" && | ||||||||||||||
| typeof storedCredential.expires === "number" && | ||||||||||||||
| storedCredential.expires <= Date.now() | ||||||||||||||
| ) { | ||||||||||||||
| try { | ||||||||||||||
| await authStorage.getApiKey(ANTHROPIC_AUTH_PROVIDER_ID); | ||||||||||||||
| authStorage.reload(); | ||||||||||||||
| storedCredential = authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID); | ||||||||||||||
| } catch { | ||||||||||||||
| // Refresh failed; fall through to expired-state handling below. | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+99
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Avoid silently swallowing Anthropic token refresh failures; log the error context so refresh issues can be diagnosed. (Based on your team's feedback about handling async errors explicitly and avoiding empty catch blocks.) Prompt for AI agents
Suggested change
|
||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+81
to
+102
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In MastraCode, authStorage is an instance of the AuthStorage class, created via createAuthStorage and returned by createMastraCode. It manages authentication credentials for AI providers like Anthropic (Claude) and OpenAI, supporting both API keys (stored in auth.json) and OAuth. Key methods and usage from source code (mastracode/src/index.ts): - authStorage.getStoredApiKey(provider): Retrieves a stored API key for a given provider (e.g., 'anthropic', 'openai', MEMORY_GATEWAY_PROVIDER). Used at startup to populate environment variables like ANTHROPIC_API_KEY if not already set. - authStorage.loadStoredApiKeysIntoEnv(providerEnvVars): Loads all stored API keys into process.env. - authStorage.get(providerId): Gets credentials for a provider, distinguishing between {type: 'api_key', key: string} and {type: 'oauth'}. - authStorage.hasStoredApiKey(provider): Checks if a stored API key exists for a provider. - authStorage.isLoggedIn(oauthId): Checks OAuth login status. - authStorage.login(providerId, callbacks) and authStorage.logout(providerId): Handle OAuth flows. API keys are entered interactively via prompts (stored persistently in auth.json), auto-detected from env vars, or loaded from storage. OAuth is used for subscriptions like Claude Max/OpenAI ChatGPT Plus. No explicit documentation or source snippets found for "in-flight refresh" (e.g., OAuth token renewal) or "deduplication" mechanisms in authStorage.getApiKey. Related concepts: - Mastra has past issues with duplicate messages/storage in multi-step/client-side executions ( Citations:
🏁 Script executed: cd packages/chat && git ls-files src/server/desktop/chat-service/chat-service.tsRepository: superset-sh/superset Length of output: 111 🏁 Script executed: cat -n packages/chat/src/server/desktop/chat-service/chat-service.ts | sed -n '75,110p'Repository: superset-sh/superset Length of output: 1724 🌐 Web query:
💡 Result: Short answer: Mastra Code’s auth storage is designed to be a single shared AuthStorage instance (initialized via a dedicated createAuthStorage export), and it doesn’t document a specific “OAuth refresh token deduplication” mechanism in the sources found. Separately, Mastra/SDK-related “deduplication” issues seen in 2025–2026 appear to be about message duplication (not OAuth refresh), while OAuth refresh-token “race/reuse” handling is addressed in other projects (e.g., OpenAI Codex) and generally in auth libraries via refresh token rotation/reuse detection. What I found in Mastra Code (2025–2026) 1) Shared authStorage initialization (Mastra Code) - Mastra Code added/exports a createAuthStorage so auth providers can be initialized without creating a full createMastraCode instance. - The PR notes unit tests asserting createAuthStorage returns a single shared AuthStorage instance and wires it to multiple provider modules. - createMastraCode returns authStorage as an AuthStorage instance, described as “Storage for OAuth credentials”. Sources indicate the focus here is on centralized/one-instance auth storage wiring, not refresh-token request deduplication. 2) OAuth token lifecycle for HTTP MCP servers (Mastra) - Mastra added an authProvider option for HTTP-based MCP servers (merged July/Aug 2025) “to enable OAuth authentication with automatic token refresh.” - A later enhancement issue (Mar 2026) describes the lack of first-class dynamic auth configuration for HTTP MCP servers (static headers only), and mentions adding first-class support; it also references a PR adding OAuth 2.1 authentication for HTTP MCP servers. What I found about “deduplication” vs OAuth refresh - The Mastra issues surfaced in search results for 2025–2026 use “duplicating response messages” terminology and discuss deduping/merging UI messages from storage; this is not OAuth refresh-token deduplication. - For OAuth refresh, I found authoritative guidance from auth systems about refresh token rotation and the security problem of refresh token reuse (race conditions / replay). For example: - Auth.js explicitly notes there is no built-in automatic refresh token rotation and discusses that refresh tokens are usually only usable once, leading to potential race conditions if multiple requests refresh concurrently. - Auth0 describes refresh token rotation and “automatic reuse detection”/invalidating previously-used refresh tokens to prevent replay. - For an explicit “deduplicate/race-guard” approach around OAuth refresh across processes, I found a Codex PR: it fixes “refresh-token races across Codex processes” by doing a guarded reload flow—reloading shared credentials before refresh attempt and comparing persisted vs in-memory snapshots; if persisted credentials changed, it adopts them and skips local refresh; otherwise it refreshes and persists rotated credentials. Conclusion for your query - For Mastra Code: based on the sources found, the clearly documented change in 2025–2026 is centralized shared authStorage initialization (createAuthStorage + shared instance), and OAuth support for HTTP MCP servers via an authProvider (automatic token refresh). - However, I did not find Mastra-specific documentation or code excerpts in these sources that explicitly describe “OAuth refresh deduplication” (e.g., ensuring concurrent refresh calls share one in-flight refresh, or guarding refresh-token reuse across processes) as a named feature. - In general auth systems, dedup/race handling is often implemented via refresh token rotation + reuse detection (Auth0) and by addressing concurrency races (Auth.js mentions the inherent once-only limitation and race conditions). For a concrete guarded approach, the Codex PR provides an example pattern. If you want, I can do a deeper targeted code-level search in the Mastra repository for terms like “refresh_token_reused”, “reuse detection”, “inFlight”, “mutex”, “dedupe”, or “guarded reload” specifically within mastracode and HTTP MCP OAuth flows. Citations:
🏁 Script executed: rg "getAnthropicAuthStatus" --type ts --type tsx -B 2 -A 2Repository: superset-sh/superset Length of output: 91 🏁 Script executed: cat -n packages/chat/src/server/desktop/chat-service/chat-service.ts | sed -n '1,80p' | tail -30Repository: superset-sh/superset Length of output: 1286 🏁 Script executed: rg "authStorage\.getApiKey|refresh.*oauth|dedupe|inFlight" packages/chat/src/server/desktop/chat-service/ -A 2Repository: superset-sh/superset Length of output: 935 🏁 Script executed: rg "getAnthropicAuthStatus" -t ts --type-add 'ts:*.ts' -B 3 -A 3Repository: superset-sh/superset Length of output: 13673 🏁 Script executed: cat -n packages/chat/src/server/desktop/chat-service/chat-service.ts | sed -n '100,150p'Repository: superset-sh/superset Length of output: 2269 🏁 Script executed: grep -r "getAnthropicAuthStatus" --include="*.ts" -l | head -10Repository: superset-sh/superset Length of output: 301 Ensure The method now performs network I/O (via 🤖 Prompt for AI Agents |
||||||||||||||
| const configCredential = getAnthropicCredentialsFromConfig(); | ||||||||||||||
| const keychainCredential = getAnthropicCredentialsFromKeychain(); | ||||||||||||||
| const externalCandidates = [configCredential, keychainCredential].filter( | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getApiKeycalled unconditionally for all OAuth credentialsgetCredentialsFromAuthStoragecallsauthStorage.getApiKey()for every OAuth credential regardless of whether it has expired. The PR description states "Resolvers now callauthStorage.getApiKey("anthropic")on expired oauth creds", but there is no expiry guard here.Both
chat-service.ts(lines 91–93) andresolveAnthropicCredential.ts(line 103) check expiry before callinggetApiKey. Calling it unconditionally means every credential resolution for a still-valid OAuth token goes throughgetApiKeyrather than just reading the already-loaded value fromauthStorage.get(). If mastracode'sgetApiKeywere to do I/O or a network call for non-expired tokens in a future release, this would add unnecessary latency on every message send.Aligning with the pattern used in the other two call sites — only calling
getApiKeywhenexpires <= Date.now()— would be more consistent and forward-safe.Prompt To Fix With AI