From c818a9fb4e5f17e250a960777593b21d0676104e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 16 May 2026 22:54:15 +0800 Subject: [PATCH] fix(agents): redact oauth refresh errors --- .../auth-profiles/oauth-manager.test.ts | 34 ++++++++++++++++++ src/agents/auth-profiles/oauth-manager.ts | 35 +++++++++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/agents/auth-profiles/oauth-manager.test.ts b/src/agents/auth-profiles/oauth-manager.test.ts index 87eed838d16..811f898440d 100644 --- a/src/agents/auth-profiles/oauth-manager.test.ts +++ b/src/agents/auth-profiles/oauth-manager.test.ts @@ -153,6 +153,40 @@ describe("OAuthManagerRefreshError", () => { expect(serialized).not.toContain("store-access"); expect(serialized).not.toContain("store-refresh"); }); + + it("redacts credential secrets from the refresh error message", () => { + const refreshedStore: AuthProfileStore = { + version: 1, + profiles: { + "openai-codex:default": createCredential({ + access: "store-access", + refresh: "store-refresh", + idToken: "store-id-token", + }), + }, + }; + const error = new OAuthManagerRefreshError({ + credential: createCredential({ + access: "error-access", + refresh: "error-refresh", + idToken: "error-id-token", + }), + profileId: "openai-codex:default", + refreshedStore, + cause: new Error( + "refresh rejected error-access error-refresh error-id-token store-access store-refresh store-id-token", + ), + }); + + expect(error.message).toContain("refresh rejected"); + expect(error.message).not.toContain("error-access"); + expect(error.message).not.toContain("error-refresh"); + expect(error.message).not.toContain("error-id-token"); + expect(error.message).not.toContain("store-access"); + expect(error.message).not.toContain("store-refresh"); + expect(error.message).not.toContain("store-id-token"); + expect(error.message.match(/\[redacted\]/g)?.length).toBe(6); + }); }); describe("createOAuthManager", () => { diff --git a/src/agents/auth-profiles/oauth-manager.ts b/src/agents/auth-profiles/oauth-manager.ts index 26aaf9c75a2..b8b3cfb46fd 100644 --- a/src/agents/auth-profiles/oauth-manager.ts +++ b/src/agents/auth-profiles/oauth-manager.ts @@ -80,10 +80,17 @@ export class OAuthManagerRefreshError extends Error { structuredCause?.code === "refresh_contention" && structuredCause.cause ? structuredCause.cause : params.cause; - super( - `OAuth token refresh failed for ${params.credential.provider}: ${formatErrorMessage(params.cause)}`, - { cause: delegatedCause }, + const storedCredential = params.refreshedStore.profiles[params.profileId]; + const causeMessage = redactOAuthCredentialSecrets( + formatErrorMessage(params.cause), + collectOAuthCredentialSecrets( + params.credential, + storedCredential?.type === "oauth" ? storedCredential : undefined, + ), ); + super(`OAuth token refresh failed for ${params.credential.provider}: ${causeMessage}`, { + cause: delegatedCause, + }); this.name = "OAuthManagerRefreshError"; this.#credential = params.credential; this.profileId = params.profileId; @@ -154,6 +161,28 @@ function canReuseOAuthCredentialAfterRefreshFailure(params: { return !params.forceRefresh || hasOAuthCredentialChanged(params.attempted, params.candidate); } +function collectOAuthCredentialSecrets( + ...credentials: Array +): string[] { + const secrets = new Set(); + for (const credential of credentials) { + for (const secret of [credential?.access, credential?.refresh, credential?.idToken]) { + if (secret) { + secrets.add(secret); + } + } + } + return Array.from(secrets); +} + +function redactOAuthCredentialSecrets(message: string, secrets: string[]): string { + let redacted = message; + for (const secret of secrets) { + redacted = redacted.split(secret).join("[redacted]"); + } + return redacted; +} + async function loadFreshStoredOAuthCredential(params: { profileId: string; agentDir?: string;