fix(auth): harden legacy auth.json cleanup

This commit is contained in:
Peter Steinberger
2026-01-07 06:51:17 +00:00
parent 0707b1e487
commit 7176b114da
4 changed files with 68 additions and 7 deletions

View File

@@ -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);

View File

@@ -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,
});
}
}
}