Skip to content

Claude Pro/Max auth flow returns Invalid token. #18315

Closed as not planned
Closed as not planned

Description

@heesungjang

Description

Hi, I’ve been getting a “Failed refresh token” error since this morning. I’ve tried reinstalling Opencode and clearing the cache, but the issue still persists.

I then attempted to reconnect the provider using the Claude Pro/Max auth flow, but now it returns an “Invalid Token” error.

I’m wondering whether this is an issue with my local environment or a bug.

Plugins

No response

OpenCode version

1.2.27

Steps to reproduce

  1. Connect using Claude Pro/Max auth flow.
  2. Click auth link
  3. Copy auth token and paste into dialog.

Screenshot and/or share link

No response

Operating System

macOS 26.2 (25C56)

Terminal

Ghostty

Activity

  1. jacocanete commented on Mar 20, 2026

    @jacocanete

    Same here

    I'm on fedora using kitty but I think that's hardly relevant.

  2. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Mar 20, 2026
  3. github-actions commented on Mar 20, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If your issue is the same as one of these, please add a comment there instead of opening a new one.

  4. ilyeshammadi commented on Mar 20, 2026

    @ilyeshammadi

    I have same issue on Arch

  5. tad-hq commented on Mar 20, 2026

    @tad-hq

    Same here on Fedora

  6. mahyarmirrashed commented on Mar 20, 2026

    @mahyarmirrashed
  7. akemmanuel commented on Mar 20, 2026

    @akemmanuel

    Same issue.

    Image
  8. cckelly commented on Mar 20, 2026

    @cckelly

    Same issue

  9. avall commented on Mar 20, 2026

    @avall

    Logged in claude (PRO Subscription):
    Image

    cat ~/.config/opencode/opencode.json
    {
    "$schema": "https://opencode.ai/config.json",
    "snapshot": false,
    "plugin": [
    "opencode-claude-auth"
    ]
    }

    npm ls -g --depth=0 opencode-claude-auth
    $HOME/.local/share/mise/installs/node/22.22.1/lib
    └── opencode-claude-auth@0.5.2

    Image

    Restart opencode > /connect > Anthropic (Claude Max or API Key)
    ... Nothing happens. It seems hanged

  10. Infiland commented on Mar 20, 2026

    @Infiland

    They removed it #18186

  11. samuelwilk commented on Mar 20, 2026

    @samuelwilk

    FUCK

  12. 1 remaining item

  13. vinzabe commented on Mar 20, 2026

    @vinzabe
  14. 4rzumanyan commented on Mar 20, 2026

    @4rzumanyan

    here's the fix https://github.com/vinzabe/opencode-anthropic-oauth-fix

    It works. After applying the fix and logging in again, I saw some error messages, but the Claude models still appeared.

    Thanks 🔥

  15. shashilaheshanmaestro commented on Mar 20, 2026

    @shashilaheshanmaestro

    none of these fixes are working , don't install anything like third party plugins in ur machine , and they will put malicious code into ur machine and steal , Just wait until open code officially patch this issue .

  16. shahidshabbir-se commented on Mar 20, 2026

    @shahidshabbir-se

    New plugin: opencode-anthropic-oauth

    For anyone hitting "Invalid token" or "Failed to authorize" — built a plugin that handles the OAuth flow directly. No Claude Code needed.

    npm install -g opencode-anthropic-oauth
    {
      "plugin": ["opencode-anthropic-oauth"]
    }

    Then /connect → Anthropic → Claude Pro/Max → authorize in browser → paste code → done.

    Fixes the two root causes identified in this thread:

    • Uses application/x-www-form-urlencoded (not JSON) for token exchange
    • Sets correct User-Agent header to avoid 429s

    ~100 lines, zero dependencies beyond OpenCode's plugin SDK.

    GitHub: https://github.com/shahidshabbir-se/opencode-anthropic-oauth

  17. crottolo commented on Mar 20, 2026

    @crottolo

    Applied the patch from https://github.com/vinzabe/opencode-anthropic-oauth-fix — the PKCE and curl fixes look correct (verified the patched index.mjs is loaded), but token exchange still fails with 429 rate_limit_error both on refresh and fresh login.

    What works: https://github.com/shahidshabbir-se/opencode-anthropic-oauth

    This is a standalone plugin replacement. Install:

    npm install -g opencode-anthropic-oauth

    Then replace opencode-anthropic-auth with opencode-anthropic-oauth in your opencode.json:

    {
      "plugin": [
        "opencode-anthropic-oauth"
      ]
    }

    Re-login with opencode providers login -p anthropic, and it works. Tested with anthropic/claude-sonnet-4-6 — token exchange and refresh both succeed.

    Alternative workaround: Claude models via Antigravity (google/antigravity-claude-sonnet-4-6) also work fine.

  18. deadpyxel commented on Mar 22, 2026

    @deadpyxel

    I am unable to get this work work even with the proposed fixes. I tested that the subscription and such is working, but auth workflow keeps on returning invalid token. Tested on multiple browser (Zen, Brave, Chromium).

  19. dotCipher commented on Mar 23, 2026

    @dotCipher

    For anyone still hitting OAuth issues with Anthropic, I built opencode-claude-bridge which combines fixes from several community plugins into one. It auto-syncs tokens from Claude CLI's keychain (zero-click if you're already logged in), handles token refresh, and emulates Claude Code's exact request format. Install: just add "plugin": ["opencode-claude-bridge"] to your OpenCode config — it auto-installs from npm.

  20. akemmanuel commented on Mar 23, 2026

    @akemmanuel

    Create .opencode/plugins/anthropic-oauth.ts and add this:

    import type { Plugin } from "@opencode-ai/plugin";
    import { randomBytes, createHash } from "node:crypto";
    
    const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
    const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
    const TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
    const REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
    const SCOPES = "org:create_api_key user:profile user:inference";
    const CLAUDE_CODE_VERSION = "2.1.76";
    const BILLING_SALT = "59cf53e54c78";
    
    // --- OAuth helpers ---
    
    function base64url(buf: Buffer): string {
      return buf.toString("base64url").replace(/=+$/, "");
    }
    
    function createAuthorizationRequest() {
      const verifier = base64url(randomBytes(32));
      const challenge = base64url(createHash("sha256").update(verifier).digest());
      const params = new URLSearchParams({
        code: "true",
        response_type: "code",
        client_id: CLIENT_ID,
        redirect_uri: REDIRECT_URI,
        scope: SCOPES,
        code_challenge: challenge,
        code_challenge_method: "S256",
        state: verifier,
      });
      return { url: `${AUTHORIZE_URL}?${params}`, verifier };
    }
    
    function parseAuthCode(raw: string): string {
      const hashIdx = raw.indexOf("#");
      return hashIdx >= 0 ? raw.slice(0, hashIdx) : raw;
    }
    
    async function exchangeCodeForTokens(rawCode: string, verifier: string) {
      const code = parseAuthCode(rawCode.trim());
      const body = new URLSearchParams({
        grant_type: "authorization_code",
        code,
        code_verifier: verifier,
        client_id: CLIENT_ID,
        redirect_uri: REDIRECT_URI,
        state: verifier,
      });
      const res = await fetch(TOKEN_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
          "User-Agent": "anthropic",
        },
        body: body.toString(),
      });
      if (!res.ok) {
        const text = await res.text().catch(() => "");
        throw new Error(
          `Token exchange failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`
        );
      }
      const data = (await res.json()) as {
        access_token: string;
        refresh_token: string;
        expires_in: number;
      };
      return {
        access: data.access_token,
        refresh: data.refresh_token,
        expires: Date.now() + data.expires_in * 1000,
      };
    }
    
    // --- Token refresh with 429 retry ---
    
    async function refreshTokenWithRetry(refresh: string): Promise<{
      access_token: string;
      refresh_token: string;
      expires_in: number;
    }> {
      let lastErr: Error | null = null;
      for (let attempt = 0; attempt < 5; attempt++) {
        try {
          const res = await fetch(TOKEN_URL, {
            method: "POST",
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
              "User-Agent": "anthropic",
            },
            body: new URLSearchParams({
              grant_type: "refresh_token",
              refresh_token: refresh,
              client_id: CLIENT_ID,
            }).toString(),
          });
          if (res.status === 429) {
            const wait = Math.min(1000 * Math.pow(2, attempt), 16000);
            await new Promise((r) => setTimeout(r, wait));
            continue;
          }
          if (!res.ok) {
            throw new Error(`Token refresh failed: ${res.status}`);
          }
          return await res.json();
        } catch (e) {
          lastErr = e instanceof Error ? e : new Error(String(e));
          const wait = Math.min(1000 * Math.pow(2, attempt), 16000);
          await new Promise((r) => setTimeout(r, wait));
        }
      }
      throw lastErr ?? new Error("Token refresh failed after retries");
    }
    
    // --- Billing header ---
    
    function sampleCodeUnits(text: string, indices: number[]): string {
      return indices
        .map((i) => (i < text.length ? text.charCodeAt(i).toString(16) : "30"))
        .join("");
    }
    
    function billingHeader(firstUserMsg: string): string {
      const sampled = sampleCodeUnits(firstUserMsg, [4, 7, 20]);
      const hash = createHash("sha256")
        .update(`${BILLING_SALT}${sampled}${CLAUDE_CODE_VERSION}`)
        .digest("hex")
        .slice(0, 3);
      return `x-anthropic-billing-header: cc_version=${CLAUDE_CODE_VERSION}.${hash}; cc_entrypoint=cli; cch=00000;`;
    }
    
    function firstUserText(messages: any[]): string {
      for (const msg of messages) {
        if (msg.role !== "user") continue;
        if (typeof msg.content === "string") return msg.content;
        if (Array.isArray(msg.content)) {
          for (const block of msg.content) {
            if (block.type === "text" && block.text) return block.text;
          }
        }
      }
      return "";
    }
    
    // --- Plugin ---
    
    const plugin: Plugin = async ({ client }) => {
      return {
        auth: {
          provider: "anthropic",
          async loader(getAuth, _provider) {
            const auth = await getAuth();
            if (auth.type !== "oauth" || !auth.refresh) return { apiKey: "" };
    
            // Zero out cost for max plan
            for (const model of Object.values(_provider.models)) {
              (model as any).cost = {
                input: 0,
                output: 0,
                cache: { read: 0, write: 0 },
              };
            }
    
            return {
              apiKey: "",
              async fetch(input: any, init: any) {
                const auth = await getAuth();
                if (auth.type !== "oauth") return fetch(input, init);
    
                // Refresh token if needed
                if (!auth.access || (auth.expires ?? 0) < Date.now()) {
                  const json = await refreshTokenWithRetry(auth.refresh);
                  await client.auth.set({
                    path: { id: "anthropic" },
                    body: {
                      type: "oauth",
                      refresh: json.refresh_token,
                      access: json.access_token,
                      expires: Date.now() + json.expires_in * 1000,
                    },
                  });
                  auth.access = json.access_token;
                }
    
                // Build headers
                const headers = new Headers();
                if (input instanceof Request) {
                  input.headers.forEach((v, k) => headers.set(k, v));
                }
                if (init?.headers) {
                  if (init.headers instanceof Headers) {
                    init.headers.forEach((v: string, k: string) => headers.set(k, v));
                  } else if (Array.isArray(init.headers)) {
                    for (const [k, v] of init.headers) {
                      if (v !== undefined) headers.set(k, String(v));
                    }
                  } else {
                    for (const [k, v] of Object.entries(init.headers)) {
                      if (v !== undefined) headers.set(k, String(v));
                    }
                  }
                }
    
                headers.set("authorization", `Bearer ${auth.access}`);
                headers.set("user-agent", `claude-code/${CLAUDE_CODE_VERSION}`);
                headers.set("anthropic-beta", "oauth-2025-04-20");
                headers.set("x-app", "cli");
                headers.delete("x-api-key");
    
                // Inject billing header into system messages
                let body = init?.body;
                if (body && typeof body === "string") {
                  try {
                    const parsed = JSON.parse(body);
                    const userMsg = firstUserText(parsed.messages ?? []);
                    const billing = billingHeader(userMsg);
    
                    if (!parsed.system) {
                      parsed.system = [{ type: "text", text: billing }];
                    } else if (Array.isArray(parsed.system)) {
                      parsed.system = [{ type: "text", text: billing }, ...parsed.system];
                    } else if (typeof parsed.system === "string") {
                      parsed.system = [
                        { type: "text", text: billing },
                        { type: "text", text: parsed.system },
                      ];
                    }
                    body = JSON.stringify(parsed);
                  } catch {
                    // not JSON, pass through
                  }
                }
    
                // Build final request
                let requestInput = input;
                let requestUrl: URL | null = null;
                try {
                  if (typeof input === "string" || input instanceof URL) {
                    requestUrl = new URL(input.toString());
                  } else if (input instanceof Request) {
                    requestUrl = new URL(input.url);
                  }
                } catch {
                  requestUrl = null;
                }
    
                if (requestUrl && requestUrl.pathname === "/v1/messages") {
                  requestUrl.searchParams.set("beta", "true");
                  requestInput =
                    input instanceof Request
                      ? new Request(requestUrl.toString(), input)
                      : requestUrl;
                }
    
                return fetch(requestInput, { ...init, body, headers });
              },
            };
          },
          methods: [
            {
              type: "oauth" as const,
              label: "Claude Pro/Max",
              authorize() {
                const { url, verifier } = createAuthorizationRequest();
                return Promise.resolve({
                  url,
                  instructions:
                    "Open the link above to authenticate with your Claude account. " +
                    "After authorizing, you'll receive a code — paste it below.",
                  method: "code" as const,
                  async callback(code: string) {
                    try {
                      const tokens = await exchangeCodeForTokens(code, verifier);
                      return {
                        type: "success" as const,
                        access: tokens.access,
                        refresh: tokens.refresh,
                        expires: tokens.expires,
                      };
                    } catch (err) {
                      console.error(
                        "anthropic-oauth: token exchange failed:",
                        err instanceof Error ? err.message : err
                      );
                      return { type: "failed" as const };
                    }
                  },
                });
              },
            },
          ],
        },
      };
    };
    
    export default plugin;

    This is one of the easiest fixes right now.

  21. dotCipher commented on Mar 23, 2026

    @dotCipher

    @akemmanuel

    Create .opencode/plugins/anthropic-oauth.ts and add this:

    import type { Plugin } from "@opencode-ai/plugin";
    import { randomBytes, createHash } from "node:crypto";

    const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
    const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
    const TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
    ...

    export default plugin;
    This is one of the easiest fixes right now.

    Most people don't like using raw source and find it easier to use a plugin or similar

  22. mahyarmirrashed commented on Mar 23, 2026

    @mahyarmirrashed

    For all those wondering, the best solution presented so far is just setting up Claude Code as a local "server" of sorts, and then to pipe communicate with it via OpenCode.

  23. dotCipher commented on Mar 23, 2026

    @dotCipher

    For all those wondering, the best solution presented so far is just setting up Claude Code as a local "server" of sorts, and then to pipe communicate with it via OpenCode.

    Exactly what I do here: https://github.com/dotCipher/opencode-claude-bridge

  24. mrp4sten commented on Mar 24, 2026

    @mrp4sten

    "opencode-claude-auth"

    I use that config and now I'ts working. Literally I just modify this:

      74   │   "plugin": [
      75 ~ │     "opencode-anthropic-auth",
      76 ~ │     "opencode-claude-auth"
      77   │   ],

    And now I can use my models from Anthropic (Claude Pro)

  25. IgorTrevelin commented on Mar 29, 2026

    @IgorTrevelin

    "opencode-claude-auth"

    I use that config and now I'ts working. Literally I just modify this:

    74 │ "plugin": [
    75 ~ │ "opencode-anthropic-auth",
    76 ~ │ "opencode-claude-auth"
    77 │ ],
    And now I can use my models from Anthropic (Claude Pro)

    Worked for me, thank you!

  26. dotCipher commented on Apr 16, 2026

    @dotCipher

    "opencode-claude-auth"

    I use that config and now I'ts working. Literally I just modify this:
    74 │ "plugin": [
    75 ~ │ "opencode-anthropic-auth",
    76 ~ │ "opencode-claude-auth"
    77 │ ],
    And now I can use my models from Anthropic (Claude Pro)

    Worked for me, thank you!

    Most of the plugins referenced in the above are stale.

    I've been keeping this up-to-date: https://github.com/dotCipher/opencode-claude-bridge

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingcoreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions