From 7176b114daa7f1bca61d4000dcca837ee6482059 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 7 Jan 2026 06:51:17 +0000 Subject: [PATCH] fix(auth): harden legacy auth.json cleanup --- CHANGELOG.md | 1 + README.md | 5 ++-- src/agents/auth-profiles.test.ts | 50 ++++++++++++++++++++++++++++++++ src/agents/auth-profiles.ts | 19 ++++++++---- 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1da8300..c9a60007f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ - Auto-reply: treat steer during compaction as a follow-up, queued until compaction completes. - Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure. - Auth/Doctor: migrate Anthropic OAuth configs from `anthropic:default` → `anthropic:` and surface a doctor hint on refresh failures. Thanks @RandyVentures for PR #361. (#363) +- Auth: delete legacy `auth.json` after migration to prevent stale OAuth token overwrites. Thanks @reeltimeapps for PR #368. - Gateway/CLI: stop forcing localhost URL in remote mode so remote gateway config works. Thanks @oswalpalash for PR #293. - Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins. - Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding). diff --git a/README.md b/README.md index 4baf9aee5..83e8620f3 100644 --- a/README.md +++ b/README.md @@ -453,5 +453,6 @@ Thanks to all clawtributors: azade-c andranik-sahakyan adamgall jalehman jarvis-medmatic mneves75 regenrek tobiasbischoff MSch obviyus dbhurley Asleep123 Iamadig imfing kitze nachoiacovino VACInc cash-echo-bot claude kiranjd pcty-nextgen-service-account minghinmatthewlam - ngutman onutc oswalpalash snopoke ManuelHettich loukotal hugobarauna AbhisekBasu1 emanuelst dantelex erikpr1994 antons RandyVentures -

+ ngutman onutc oswalpalash snopoke ManuelHettich loukotal hugobarauna AbhisekBasu1 emanuelst dantelex erikpr1994 antons RandyVentures + reeltimeapps +

diff --git a/src/agents/auth-profiles.test.ts b/src/agents/auth-profiles.test.ts index d9bfe938c..bf52a1931 100644 --- a/src/agents/auth-profiles.test.ts +++ b/src/agents/auth-profiles.test.ts @@ -1,8 +1,13 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + import { describe, expect, it } from "vitest"; import { type AuthProfileStore, calculateAuthProfileCooldownMs, + ensureAuthProfileStore, resolveAuthProfileOrder, } from "./auth-profiles.js"; @@ -280,6 +285,51 @@ describe("resolveAuthProfileOrder", () => { }); }); +describe("ensureAuthProfileStore", () => { + it("migrates legacy auth.json and deletes it (PR #368)", () => { + const agentDir = fs.mkdtempSync( + path.join(os.tmpdir(), "clawdbot-auth-profiles-"), + ); + try { + const legacyPath = path.join(agentDir, "auth.json"); + fs.writeFileSync( + legacyPath, + `${JSON.stringify( + { + anthropic: { + type: "oauth", + provider: "anthropic", + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 60_000, + }, + }, + null, + 2, + )}\n`, + "utf8", + ); + + const store = ensureAuthProfileStore(agentDir); + expect(store.profiles["anthropic:default"]).toMatchObject({ + type: "oauth", + provider: "anthropic", + }); + + const migratedPath = path.join(agentDir, "auth-profiles.json"); + expect(fs.existsSync(migratedPath)).toBe(true); + expect(fs.existsSync(legacyPath)).toBe(false); + + // idempotent + const store2 = ensureAuthProfileStore(agentDir); + expect(store2.profiles["anthropic:default"]).toBeDefined(); + expect(fs.existsSync(legacyPath)).toBe(false); + } finally { + fs.rmSync(agentDir, { recursive: true, force: true }); + } + }); +}); + describe("auth profile cooldowns", () => { it("applies exponential backoff with a 1h cap", () => { expect(calculateAuthProfileCooldownMs(1)).toBe(60_000); diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts index 0eeda809f..20400693a 100644 --- a/src/agents/auth-profiles.ts +++ b/src/agents/auth-profiles.ts @@ -11,6 +11,7 @@ import lockfile from "proper-lockfile"; import type { ClawdbotConfig } from "../config/config.js"; import { resolveOAuthPath } from "../config/paths.js"; import type { AuthProfileConfig } from "../config/types.js"; +import { createSubsystemLogger } from "../logging.js"; import { resolveUserPath } from "../utils.js"; import { resolveClawdbotAgentDir } from "./agent-paths.js"; import { normalizeProviderId } from "./model-selection.js"; @@ -29,6 +30,8 @@ const AUTH_STORE_LOCK_OPTIONS = { stale: 30_000, } as const; +const log = createSubsystemLogger("agents/auth-profiles"); + export type ApiKeyCredential = { type: "api_key"; provider: string; @@ -350,14 +353,20 @@ export function ensureAuthProfileStore(agentDir?: string): AuthProfileStore { saveJsonFile(authPath, store); } - // Delete legacy auth.json after successful migration to prevent stale tokens - // from being re-migrated and overwriting fresh credentials (fixes #363) - if (legacy !== null) { + // PR #368: legacy auth.json could get re-migrated from other agent dirs, + // overwriting fresh OAuth creds with stale tokens (fixes #363). Delete only + // after we've successfully written auth-profiles.json. + if (shouldWrite && legacy !== null) { const legacyPath = resolveLegacyAuthStorePath(agentDir); try { fs.unlinkSync(legacyPath); - } catch { - // Ignore if already deleted or permission issues + } catch (err) { + if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") { + log.warn("failed to delete legacy auth.json after migration", { + err, + legacyPath, + }); + } } }