Skip to content

Commit 544acf2

Browse files
committed
fix(auth): skip OAuth refresh adapter when credential has no refresh token
OAuth credentials that loaded without their sidecar material (no access, no refresh) would still enter the refresh path inside the per-profile lock, where the adapter call is bounded by OAUTH_REFRESH_CALL_TIMEOUT_MS (120s). That made the eventual "No API key found for provider" surface to the user only after a long stall, even though the resolver had no usable material to attempt with. Short-circuit doRefreshOAuthTokenWithLock to return null when there is no refresh token to use, after the in-lock main-store adoption and external bootstrap-credential checks have already had a chance to recover. Thanks @RomneyDa.
1 parent b33deb4 commit 544acf2

3 files changed

Lines changed: 48 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
4343
- CLI/perf: keep `secrets --help` and `nodes --help` on the precomputed help path so parent help avoids loading action-heavy command runtime modules. (#84818) Thanks @frankekn.
4444
- CLI/perf: serve `doctor`, `gateway`, `models`, and `plugins` parent help from startup metadata so common subcommand help avoids full CLI program construction. (#84786) Thanks @frankekn.
4545
- Codex/Lossless: keep context-engine history on the canonical run session when Telegram DMs use per-peer runtime policy keys. Fixes #84936. (#84954) Thanks @neeravmakwana.
46+
- Auth/OAuth: skip the refresh adapter when a stored OAuth credential has no refresh token so agent turns fail fast on missing-key instead of waiting on the 120s refresh timeout. Thanks @romneyda.
4647

4748
## 2026.5.20
4849

src/agents/auth-profiles/oauth-manager.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,49 @@ describe("createOAuthManager", () => {
555555
});
556556
});
557557

558+
it("skips the refresh adapter when the credential has no refresh token", async () => {
559+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-no-refresh-"));
560+
tempDirs.push(tempRoot);
561+
process.env.OPENCLAW_STATE_DIR = tempRoot;
562+
const agentDir = path.join(tempRoot, "agents", "main", "agent");
563+
await fs.mkdir(agentDir, { recursive: true });
564+
const profileId = "openai-codex:default";
565+
const credential = createCredential({
566+
access: "",
567+
refresh: "",
568+
expires: Date.now() - 60_000,
569+
});
570+
saveAuthProfileStore(
571+
{
572+
version: 1,
573+
profiles: {
574+
[profileId]: credential,
575+
},
576+
},
577+
agentDir,
578+
{ filterExternalAuthProfiles: false },
579+
);
580+
const refreshCredential = vi.fn(async () => null);
581+
const manager = createOAuthManager({
582+
buildApiKey: async (_provider, value) => value.access,
583+
refreshCredential,
584+
readBootstrapCredential: () => null,
585+
isRefreshTokenReusedError: () => false,
586+
});
587+
588+
const result = await manager.resolveOAuthAccess({
589+
store: ensureAuthProfileStoreWithoutExternalProfiles(agentDir, {
590+
allowKeychainPrompt: false,
591+
}),
592+
profileId,
593+
credential,
594+
agentDir,
595+
});
596+
597+
expect(result).toBeNull();
598+
expect(refreshCredential).not.toHaveBeenCalled();
599+
});
600+
558601
it("redacts the external oauth credential attempted during refresh failures", async () => {
559602
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-refresh-redact-"));
560603
tempDirs.push(tempRoot);

src/agents/auth-profiles/oauth-manager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { OpenClawConfig } from "../../config/types.openclaw.js";
2+
import { normalizeSecretInputString } from "../../config/types.secrets.js";
23
import { formatErrorMessage } from "../../infra/errors.js";
34
import { withFileLock } from "../../infra/file-lock.js";
45
import { redactSensitiveText } from "../../logging/redact.js";
@@ -533,6 +534,9 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
533534
}
534535
}
535536

537+
if (normalizeSecretInputString(credentialToRefresh.refresh) === undefined) {
538+
return null;
539+
}
536540
const refreshedCredentials = await withRefreshCallTimeout(
537541
`refreshOAuthCredential(${cred.provider})`,
538542
OAUTH_REFRESH_CALL_TIMEOUT_MS,

0 commit comments

Comments
 (0)