fix(auth): migrate OAuth token refresh to platform.claude.com with fallback (#3246)

Anthropic migrated their OAuth infrastructure from console.anthropic.com
to platform.claude.com (Claude Code v2.1.81+). Update _refresh_oauth_token()
to try the new endpoint first, falling back to the old one for tokens
issued before the migration.

Also switches Content-Type from application/x-www-form-urlencoded to
application/json to match current Claude Code behavior.

Salvaged from PR #2741 by kshitijk4poor.
This commit is contained in:
Teknium
2026-03-26 13:26:56 -07:00
committed by GitHub
parent c6fe75e99b
commit 2c719f0701

View File

@@ -210,9 +210,12 @@ def _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]:
Only works for credentials that have a refresh token (from claude /login Only works for credentials that have a refresh token (from claude /login
or claude setup-token with OAuth flow). or claude setup-token with OAuth flow).
Tries the new platform.claude.com endpoint first (Claude Code >=2.1.81),
then falls back to console.anthropic.com for older tokens.
Returns the new access token, or None if refresh fails. Returns the new access token, or None if refresh fails.
""" """
import urllib.parse import time
import urllib.request import urllib.request
refresh_token = creds.get("refreshToken", "") refresh_token = creds.get("refreshToken", "")
@@ -223,38 +226,42 @@ def _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]:
# Client ID used by Claude Code's OAuth flow # Client ID used by Claude Code's OAuth flow
CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
data = urllib.parse.urlencode({ # Anthropic migrated OAuth from console.anthropic.com to platform.claude.com
# (Claude Code v2.1.81+). Try new endpoint first, fall back to old.
token_endpoints = [
"https://platform.claude.com/v1/oauth/token",
"https://console.anthropic.com/v1/oauth/token",
]
payload = json.dumps({
"grant_type": "refresh_token", "grant_type": "refresh_token",
"refresh_token": refresh_token, "refresh_token": refresh_token,
"client_id": CLIENT_ID, "client_id": CLIENT_ID,
}).encode() }).encode()
req = urllib.request.Request( headers = {
"https://console.anthropic.com/v1/oauth/token", "Content-Type": "application/json",
data=data, "User-Agent": f"claude-cli/{_CLAUDE_CODE_VERSION} (external, cli)",
headers={ }
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": f"claude-cli/{_CLAUDE_CODE_VERSION} (external, cli)",
},
method="POST",
)
try: for endpoint in token_endpoints:
with urllib.request.urlopen(req, timeout=10) as resp: req = urllib.request.Request(
result = json.loads(resp.read().decode()) endpoint, data=payload, headers=headers, method="POST",
new_access = result.get("access_token", "") )
new_refresh = result.get("refresh_token", refresh_token) try:
expires_in = result.get("expires_in", 3600) # seconds with urllib.request.urlopen(req, timeout=10) as resp:
result = json.loads(resp.read().decode())
new_access = result.get("access_token", "")
new_refresh = result.get("refresh_token", refresh_token)
expires_in = result.get("expires_in", 3600)
if new_access: if new_access:
import time new_expires_ms = int(time.time() * 1000) + (expires_in * 1000)
new_expires_ms = int(time.time() * 1000) + (expires_in * 1000) _write_claude_code_credentials(new_access, new_refresh, new_expires_ms)
# Write refreshed credentials back to ~/.claude/.credentials.json logger.debug("Refreshed Claude Code OAuth token via %s", endpoint)
_write_claude_code_credentials(new_access, new_refresh, new_expires_ms) return new_access
logger.debug("Successfully refreshed Claude Code OAuth token") except Exception as e:
return new_access logger.debug("Token refresh failed at %s: %s", endpoint, e)
except Exception as e:
logger.debug("Failed to refresh Claude Code token: %s", e)
return None return None