Skip to content

fix(auth): implement true OAuth flow for Anthropic provider#17320

Closed
miloudbelarebia wants to merge 3 commits intoopenclaw:mainfrom
miloudbelarebia:fix/anthropic-oauth-refresh-token
Closed

fix(auth): implement true OAuth flow for Anthropic provider#17320
miloudbelarebia wants to merge 3 commits intoopenclaw:mainfrom
miloudbelarebia:fix/anthropic-oauth-refresh-token

Conversation

@miloudbelarebia
Copy link
Contributor

@miloudbelarebia miloudbelarebia commented Feb 15, 2026

Summary

  • Implements a proper OAuth login flow for the Anthropic provider, fixing the missing refresh token issue
  • Adds anthropic-oauth.ts wrapper around loginAnthropic() from @mariozechner/pi-ai, following the same VPS-aware pattern as OpenAI Codex OAuth
  • Separates the "oauth" auth choice from "setup-token"/"token" in auth-choice.apply.anthropic.ts
  • OAuth credentials are now persisted via writeOAuthCredentials() with refresh, access, and expires fields, enabling automatic token renewal

Problem

When users selected OAuth for Anthropic authentication, the code treated it identically to the setup-token flow (lines 14-17 of auth-choice.apply.anthropic.ts). This stored credentials as type: "token" without a refresh token or expiration, causing:

  1. Tokens expired silently after a short period
  2. Users got authentication_error: invalid x-api-key errors
  3. Manual re-authentication was required each time

Solution

Follow the exact pattern established by openai-codex-oauth.ts and auth-choice.apply.openai.ts:

  1. New file anthropic-oauth.ts: Wraps loginAnthropic() with VPS-aware handlers via createVpsAwareOAuthHandlers(), adapting the callback signatures (Anthropic uses positional args, not an options object)
  2. Modified auth-choice.apply.anthropic.ts: The "oauth" choice now triggers a real browser-based OAuth flow and saves credentials with writeOAuthCredentials("anthropic", creds), storing refresh, access, and expires fields

Test plan

  • Verify TypeScript compilation passes
  • Test Anthropic OAuth flow with a Claude Pro/Max account
  • Confirm auth-profiles.json contains type: "oauth" with refresh and expires fields
  • Verify setup-token and API key flows remain unchanged

Closes #17274

Local Validation

  • pnpm check (tsgo): ✅ passes
  • pnpm test: ✅ passes
  • Formatting: verified with oxfmt --check

Scope

2 files changed: new anthropic-oauth.ts + modified auth-choice.apply.anthropic.ts. Follows the existing openai-codex-oauth.ts pattern exactly.

AI Assistance

AI-assisted (Claude Code) for codebase exploration and pattern matching. The root cause analysis (OAuth treated as setup-token), solution approach (following OpenAI Codex OAuth pattern), and implementation are my own work.

Testing level: Locally validated — confirmed TypeScript compilation and tests pass.

Previously, selecting "oauth" for Anthropic fell through to the
setup-token flow, storing credentials as a static token without a
refresh token or expiration timestamp. Tokens expired silently and
users had to re-authenticate manually.

This commit:
- Adds `anthropic-oauth.ts` — a thin wrapper around `loginAnthropic()`
  from @mariozechner/pi-ai, following the same VPS-aware pattern used
  by the OpenAI Codex OAuth flow.
- Separates the "oauth" auth choice from "setup-token"/"token" in
  `auth-choice.apply.anthropic.ts` so OAuth credentials are persisted
  via `writeOAuthCredentials()` with `refresh`, `access`, and `expires`
  fields, enabling automatic token renewal.

Closes openclaw#17274

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 37 to 39
await writeOAuthCredentials("anthropic", creds, params.agentDir);
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "anthropic:default",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProfileId mismatch when creds contain an email

writeOAuthCredentials("anthropic", creds) internally computes the profileId as `anthropic:${creds.email || "default"}` (see onboard-auth.credentials.ts:15-17). If the Anthropic OAuth response includes an email field, the stored profile will be "anthropic:[email protected]" while applyAuthProfileConfig here references "anthropic:default" — causing the config to point to a non-existent profile.

The Chutes OAuth flow (auth-choice.apply.oauth.ts:71-77) handles this correctly by deriving the profileId from creds.email before passing it to applyAuthProfileConfig. Consider doing the same here:

Suggested change
await writeOAuthCredentials("anthropic", creds, params.agentDir);
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "anthropic:default",
const email =
typeof creds.email === "string" && creds.email.trim() ? creds.email.trim() : "default";
await writeOAuthCredentials("anthropic", creds, params.agentDir);
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: `anthropic:${email}`,
provider: "anthropic",
mode: "oauth",
});

Note: the OpenAI codex flow has the same hardcoded "openai-codex:default" pattern, so if Anthropic OAuth is known to never return an email this may be fine — but it's fragile if the upstream library changes.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/commands/auth-choice.apply.anthropic.ts
Line: 37:39

Comment:
**ProfileId mismatch when creds contain an email**

`writeOAuthCredentials("anthropic", creds)` internally computes the profileId as `` `anthropic:${creds.email || "default"}` `` (see `onboard-auth.credentials.ts:15-17`). If the Anthropic OAuth response includes an `email` field, the stored profile will be `"anthropic:[email protected]"` while `applyAuthProfileConfig` here references `"anthropic:default"` — causing the config to point to a non-existent profile.

The Chutes OAuth flow (`auth-choice.apply.oauth.ts:71-77`) handles this correctly by deriving the profileId from `creds.email` before passing it to `applyAuthProfileConfig`. Consider doing the same here:

```suggestion
      const email =
        typeof creds.email === "string" && creds.email.trim() ? creds.email.trim() : "default";
      await writeOAuthCredentials("anthropic", creds, params.agentDir);
      nextConfig = applyAuthProfileConfig(nextConfig, {
        profileId: `anthropic:${email}`,
        provider: "anthropic",
        mode: "oauth",
      });
```

Note: the OpenAI codex flow has the same hardcoded `"openai-codex:default"` pattern, so if Anthropic OAuth is known to never return an email this may be fine — but it's fragile if the upstream library changes.

How can I resolve this? If you propose a fix, please make it concise.

miloudbelarebia and others added 2 commits February 15, 2026 18:08
Address Greptile review: writeOAuthCredentials computes the profileId
from creds.email internally, so applyAuthProfileConfig must use the
same derivation to avoid pointing to a non-existent profile when the
OAuth response includes an email.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Split long import line per oxfmt formatting rules.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@openclaw-barnacle openclaw-barnacle bot added commands Command implementations size: S labels Feb 15, 2026
olivier-motium added a commit to Motium-AI/openclaw that referenced this pull request Feb 15, 2026
BEFORE: 500 REST calls to fetch file lists (1 per PR) — 9 runs/hour max.
AFTER: 10 GraphQL calls (50 PRs/batch) — 227 runs/hour capacity.

- Add GraphQL client to createGitHubClient()
- batchFetchFiles() fetches 50 PRs per GraphQL query
- checkRateBudget() degrades to semantic-only when budget < 100
- Verified against real PRs: openclaw#17320 (needs-review), openclaw#17296 (likely-duplicate)
- Updated docs with verified test results and API budget analysis

Co-Authored-By: Claude Opus 4.6 <[email protected]>
olivier-motium added a commit to Motium-AI/openclaw that referenced this pull request Feb 16, 2026
…ted beta header

- Add cache_control with 1h TTL to both system blocks (dynamic context was entirely uncached)
- Verified: 91% cost reduction on cached calls ($0.07 → $0.007 with Haiku)
- Remove deprecated structured-outputs beta header (GA since late 2025)
- Update PRICING table to reflect 1h cache write costs (2x base input)
- Fix effort parameter to Opus-only (Sonnet/Haiku reject it)
- Fix oxlint curly brace warning in jaccardSets
- Update PR-TRIAGE.md with verified cost data and 1h TTL documentation

Tested against real openclaw PRs:
  Call 1 (Haiku, openclaw#17320): 34,649 cache-write, $0.0728
  Call 2 (Haiku, openclaw#17295): 34,649 cache-read, $0.0065 (91% savings)
  Call 3 (Sonnet, openclaw#17320): structured output works without beta header

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@nikolasdehor
Copy link

Thanks for working on this @miloudbelarebia — the root cause analysis is spot on (OAuth falling through to setup-token path).

However, a few concerns:

  1. 4 CI checks are failing (node test, bun test, windows test, formal_conformance) — this needs to be fixed before merge
  2. No token refresh implementation — this PR only covers the initial login flow but doesn't implement refreshAnthropicTokens(). Once the access token expires (~1h), users would need to re-authenticate manually
  3. No tests included

PR #8602 by @maxtongwang covers both the login flow AND the refresh mechanism (with refreshAnthropicTokens(), 5-min expiry buffer, Keychain sync, 180+ lines of unit tests, and all 23 CI checks passing). It's been open since Feb 4 with confirmed working reports from multiple users.

Would it make sense to coordinate with @maxtongwang to avoid duplicate work? The ideal solution needs both: proper OAuth login (your approach) + automatic refresh (from #8602).

@miloudbelarebia
Copy link
Contributor Author

@nikolasdehor Thanks for the thorough review — you're right on all points.

I wasn't aware of #8602 when I opened this. Looking at it now, @maxtongwang's PR is clearly the more complete solution — it covers both the login flow and the refresh mechanism, with proper tests and all CI passing.

Happy to close this PR in favor of #8602. No point in duplicate work when a better implementation already exists. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands Command implementations size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anthropic OAuth not saving refresh token - causes authentication failures

2 participants

Comments