Anthropic OAuth token endpoint URL migration issues
NousResearch/hermes-agent · cluster 45250 · agent
Data as of
Nothing open on GitHub. Fix merged in #51723. Check it covers your case.
The short answer
- Known?
- Yes. 14 related items grouped here, 7 issues already closed.
- Fix event
- A fix was merged to main in PR #51723. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #51723. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Multiple reports and fixes around the Anthropic OAuth login flow using a stale console.anthropic.com token endpoint that returns HTTP 404, with various proposed fixes updating or adding fallbacks to platform.claude.com.
Open issues0
Open PRs0
Fixes on record
-
#51723 fix(anthropic): use platform.claude.com for OAuth login token exchange
files:
agent/anthropic_adapter.py,hermes_cli/web_server.py,scripts/release.py,tests/agent/test_anthropic_oauth_pkce.pyShow the diff
-
agent/anthropic_adapter.py@@ -1297,7 +1297,15 @@ def run_oauth_setup_token() -> Optional[str]: # Stores credentials in ~/.hermes/.anthropic_oauth.json (our own file). _OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" -_OAUTH_TOKEN_URL = "[redacted:secret-assignment]" +# Anthropic migrated the OAuth token endpoint to platform.claude.com; +# console.anthropic.com now 404s. Callers should iterate _OAUTH_TOKEN_URLS +# (new host first, console fallback). _OAUTH_TOKEN_URL is kept as the primary +# for backward compatibility with existing imports and now points at the live host. +_OAUTH_TOKEN_URLS = [ + "https://platform.claude.com/v1/oauth/token", + "https://console.anthropic.com/v1/oauth/token", +] +_OAUTH_TOKEN_URL = [redacted:secret-assignment] _OAUTH_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback" _OAUTH_SCOPES = "org:create_api_key user:profile user:inference" _HERMES_OAUTH_FILE = get_hermes_home() / ".anthropic_oauth.json" @@ -1395,18 +1403,34 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]: "code_verifier": verifier, }).encode() - req = urllib.request.Request( - _OAUTH_TOKEN_URL, - data=exchange_data, - headers={ - "Content-Type": "application/json", - "User-Agent": f"claude-cli/{_get_claude_code_version()} (external, cli)", - }, - method="POST", - ) + # Anthropic migrated the OAuth token endpoint to platform.claude.com; + # console.anthropic.com now 404s. Try the new host first, then fall + # back to console for older deployments (mirrors the refresh path). + result = None + last_error = None + for endpoint in _OAUTH_TOKEN_URLS: + req = urllib.request.Request( + endpoint, + data=exchange_data, + headers={ + "Content-Type": "application/json", + "User-Agent": f"claude-cli/{_get_claude_code_version()} (external, cli)", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=15) as resp: + result = json.loads(resp.read().decode()) + break + except Exception as exc: + last_error = exc + logger.debug("Anthropic token exchange failed at %s: %s", endpoint, exc) + continue - with urllib.request.urlopen(req, timeout=15) as resp: - result = json.loads(resp.read().decode()) + if result is None: + raise last_error if last_error is not None else ValueError( + "Anthropic token exchange failed" + ) except Exception as e: print(f"Token exchange failed: {e}") return None -
hermes_cli/web_server.py@@ -6123,6 +6123,7 @@ async def disconnect_oauth_provider( from agent.anthropic_adapter import ( _OAUTH_CLIENT_ID as _ANTHROPIC_OAUTH_CLIENT_ID, _OAUTH_TOKEN_URL as _ANTHROPIC_OAUTH_TOKEN_URL, + _OAUTH_TOKEN_URLS as _ANTHROPIC_OAUTH_TOKEN_URLS, _OAUTH_REDIRECT_URI as _ANTHROPIC_OAUTH_REDIRECT_URI, _OAUTH_SCOPES as _ANTHROPIC_OAUTH_SCOPES, _generate_pkce as _generate_pkce_pair, @@ -6311,22 +6312,31 @@ def _submit_anthropic_pkce( "redirect_uri": _ANTHROPIC_OAUTH_REDIRECT_URI, "code_verifier": sess["verifier"], }).encode() - req = urllib.request.Request( - _ANTHROPIC_OAUTH_TOKEN_URL, - data=exchange_data, - headers={ - "Content-Type": "application/json", - "User-Agent": "hermes-dashboard/1.0", - }, - method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=20) as resp: - result = json.loads(resp.read().decode()) - except Exception as e: + # Anthropic migrated the OAuth token endpoint to platform.claude.com; + # console.anthropic.com now 404s. Try the new host first, then fall back. + result = None + last_exc = None + for _endpoint in _ANTHROPIC_OAUTH_TOKEN_URLS: + req = urllib.request.Request( + _endpoint, + data=exchange_data, + headers={ + "Content-Type": "application/json", + "User-Agent": "hermes-dashboard/1.0", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=20) as resp: + result = json.loads(resp.read().decode()) + break + except Exception as e: + last_exc = e + continue + if result is None: with _oauth_sessions_lock: sess["status"] = "error" - sess["error_message"] = f"Token exchange failed: {e}" + sess["error_message"] = f"Token exchange failed: {last_exc}" return {"ok": False, "status": "error", "message": sess["error_message"]} access_token = [redacted:secret-assignment]"access_token", "") -
scripts/release.py@@ -47,6 +47,7 @@ AUTHOR_MAP = { "[redacted:email]": "wgu9", # PR #51468 salvage (WSL/no-systemd orphan gateway tracking, #51325) "[redacted:email]": "uperLu", # PR #50958 salvage (rename plugins/cron → plugins/cron_providers; #50872) + "[redacted:email]": "yusekiotacode", # PR #48706 salvage (anthropic OAuth login token endpoint → platform.claude.com; #45250/#49821) "[redacted:email]": "s010mn", # PR #29221 salvage (ollama-cloud reasoning_effort xhigh→max) "[redacted:email]": "jeeves-assistant", # PR #50771 (computer-use CuaDriver vision capture routing) "[redacted:email]": "ScotterMonk", # PR #50145 salvage (cron output truncation: adapter-aware chunking, #50126) -
tests/agent/test_anthropic_oauth_pkce.py@@ -148,6 +148,111 @@ def fake_input(*_a, **_kw): ) +def test_login_token_exchange_uses_platform_claude_host(monkeypatch, tmp_path): + """The login token exchange must hit ``platform.claude.com`` first. + + Anthropic migrated the OAuth token endpoint to ``platform.claude.com``; + ``console.anthropic.com`` now 404s, so a hardcoded console host makes a + fresh login impossible (issue #45250 / #49821). The refresh path already + iterates the new host first — the login path must do the same. + """ + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + + captured_token: [redacted:secret-assignment] Any] = {} + captured_url: Dict[str, str] = {} + _patch_oauth_flow( + monkeypatch, + callback_code="placeholder", + capture_token_request=[redacted:secret-assignment] + capture_auth_url=captured_url, + ) + + import builtins + + def fake_input(*_a, **_kw): + qs = parse_qs(urlparse(captured_url.get("url", "")).query) + state = qs.get("state", [""])[0] + return f"auth-code#{state}" + + monkeypatch.setattr(builtins, "input", fake_input) + + from agent.anthropic_adapter import run_hermes_oauth_login_pure + + result = run_hermes_oauth_login_pure() + + assert result is not None, "login should succeed against the live host" + assert captured_token["url"] == "https://platform.claude.com/v1/oauth/token", ( + "login token exchange must target platform.claude.com first, not the " + "dead console.anthropic.com host (regression of #45250 / #49821)" + ) + + +def test_login_token_exchange_falls_back_to_console_host(monkeypatch, tmp_path): + """If ``platform.claude.com`` is unreachable, the login path must fall back + to the legacy ``console.anthropic.com`` host — mirroring the refresh path's + fallback list — rather than failing outright. + """ + import urllib.request + + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + + captured_url: Dict[str, str] = {} + _patch_oauth_flow( + monkeypatch, + callback_code="placeholder", + capture_auth_url=captured_url, + ) + + attempts: list[str] = [] + + class _FakeResponse: + def __init__(self, body: bytes) -> None: + self._body = body + + def __enter__(self): + return self + + def __exit__(self, *_exc): + return False + + def read(self): + return self._body + + def fake_urlopen(req, *_a, **_kw): + attempts.append(req.full_url) + if req.full_url.startswith("https://platform.claude.com"): + raise RuntimeError("HTTP Error 404: Not Found") + body = json.dumps( + { + "access_token": "sk-ant-test-access", + "refresh_token": "sk-ant-test-refresh", + "expires_in": 3600, + } + ).encode() + return _FakeResponse(body) + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + + import builtins + + def fake_input(*_a, **_kw): + qs = parse_qs(urlparse(captured_url.get("url", "")).query) + state = qs.get("state", [""])[0] + return f"auth-code#{state}" + + monkeypatch.setattr(builtins, "input", fake_input) + + from agent.anthropic_adapter import run_hermes_oauth_login_pure + + result = run_hermes_oauth_login_pure() + + assert result is not None, "login should succeed via the console fallback" + assert attempts == [ + "https://platform.claude.com/v1/oauth/token", + "https://console.anthropic.com/v1/oauth/token", + ], "login must try platform.claude.com first, then fall back to console" + + def test_callback_state_mismatch_aborts(monkeypatch, tmp_path, caplog): """If the state returned in the callback does not match the one we sent in the authorization URL, the flow must abort before exchanging the code.
-
6 PRs closed without merging
From the close comments: won't fix / by design 3 · duplicate 1 · superseded by another PR 1 · other 1
- #45254 fix(auth): iterate token endpoints in Anthropic OAuth login flow · won't fix / by design
-
#45339 fix(auth): try fallback token endpoints for Anthropic OAuth login (#45250) · superseded by another PR →
#45254,#45250 -
#47765 fix(auth): update OAuth token endpoint to platform.claude.com (#47692) · duplicate →
#45254,#46251,#45441,#47692 - #47989 fix(auth): update Anthropic OAuth token URL (#47964) · won't fix / by design
- #47992 fix(auth): correct OAuth redirect URI to platform.claude.com (#47966) · won't fix / by design
Show 1 more
- #50982 fix: update Anthropic OAuth token endpoint from console.anthropic.com to platform.claude.com · other
Closed issues · 7
- Closed#45250Anthropic OAuth login (auth add anthropic --type oauth) fails with 404 — login flow uses stale token endpoint
- Closed#47692OAuth login (anthropic) hits dead console.anthropic.com token endpoint → HTTP 404; refresh path already moved to platform.claude.com
- Closed#47964OAuth: _OAUTH_TOKEN_URL points to console.anthropic.com (404) — should be api.anthropic.com
- Closed#47966PKCE redirect_uri mismatch: console.anthropic.com vs platform.claude.com causes invalid tokens
- Closed#49821Anthropic OAuth login 404s: token exchange still uses migrated console.anthropic.com endpoint
- Closed#51559anthropic OAuth login token exchange hardcodes dead console.anthropic.com URL → HTTP 404, login never completes
- Closed#52630Anthropic OAuth login (auth add anthropic --type oauth) 404s: _OAUTH_TOKEN_URL points to console.anthropic.com; refresh path already uses platform.claude.com
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.14: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- agent
- Merged PRs
- #51723
- Closed issues
- 7
- Attempts
- 6 PRs closed without merging
- Fix link checked
- 24 Jun 2026