Skip to content

fix(xai-oauth): echo code_challenge in token POST so PKCE exchange succeeds (#26990) - #26999

Closed
xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/xai-oauth-pkce-verifier-26990
Closed

fix(xai-oauth): echo code_challenge in token POST so PKCE exchange succeeds (#26990)#26999
xxxigm wants to merge 2 commits into
NousResearch:mainfrom
xxxigm:fix/xai-oauth-pkce-verifier-26990

Conversation

@xxxigm

@xxxigm xxxigm commented May 16, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes hermes auth add xai-oauth getting stuck at the token-exchange step with code_challenge is required.

Issue #26990 reported a P1 regression where the loopback OAuth flow succeeds at the browser-side authorize step (callback comes back with a valid code), but the follow-up POST to xAI's token endpoint fails with code_challenge is required — even though Hermes was already sending a valid code_verifier exactly as RFC 7636 §4.5 prescribes.

Root cause — xAI's OAuth implementation at auth.x.ai re-validates the PKCE code_challenge at the token endpoint, not just at the authorize step. A standards-compliant client that sends only code_verifier (the canonical RFC 7636 token-step input) is rejected. This is a known quirk of permissive OAuth implementations: while RFC 7636 doesn't require the client to echo the challenge at the token endpoint, it also doesn't forbid it, and many home-rolled servers cross-validate.

Fix — extract the token POST into a dedicated, unit-testable _xai_oauth_exchange_code_for_tokens helper and:

  1. Send code_verifier as before (unchanged contract).
  2. Also echo the original code_challenge and code_challenge_method=S256 in the form body. Strict RFC-compliant servers ignore extras at the token endpoint; xAI's permissive server accepts the exchange. This is the standard defensive-echo workaround used by every OAuth client targeting a server with this quirk.
  3. Refuse to fire the POST when code_verifier is empty — leaking an auth code to a server that can't redeem it is worse than failing locally with an actionable error (new code: xai_pkce_verifier_missing, message points at xai-oauth token exchange fails: code_challenge is required (PKCE code_verifier missing) #26990).
  4. Surface the HTTP status code prominently in 4xx errors (xAI token exchange failed (HTTP 400). Response: …) so users / maintainers can tell a 400 (PKCE / bad request) from a 403 (tier denied, see [Bug]: xAI OAuth (xai-oauth) returns HTTP 403 for standard SuperGrok subscribers — backend enforcing Heavy-only despite docs claiming all tiers #26847) at a glance instead of parsing the JSON body by eye.

Related Issue

Closes #26990

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • hermes_cli/auth.py — extract token POST into _xai_oauth_exchange_code_for_tokens(...); echo code_challenge + code_challenge_method=S256 alongside code_verifier; refuse to POST with empty code_verifier (raises xai_pkce_verifier_missing); embed HTTP status code in 4xx error messages. _xai_oauth_loopback_login now delegates to the helper — 40 lines of inline httpx replaced with one call.
  • tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py14 new regression tests covering:
    • code_verifier is on the wire (RFC 7636 §4.5).
    • code_challenge + code_challenge_method=S256 are echoed (the xai-oauth token exchange fails: code_challenge is required (PKCE code_verifier missing) #26990 defense-in-depth).
    • Static fields locked: grant_type, code, redirect_uri, client_id.
    • Content-Type is form-urlencoded; Accept is JSON.
    • Supplied token_endpoint used verbatim (no hard-coded constant).
    • timeout_seconds forwarded; floored at 20s.
    • Empty code_verifier raises locally without sending — and NOTHING is POSTed.
    • Empty code_challenge keeps the standards-compliant request flowing (defensive echo is opt-in).
    • 4xx responses surface both HTTP <status> and the response body.
    • Transport errors wrap as AuthError with the right code.
    • Non-dict JSON payloads raise xai_token_exchange_invalid.
    • 200 happy path returns the parsed dict verbatim.
    • End-to-end wire-format guard via a real httpx.Client + stub transport that captures the bytes and parses them — catches a future refactor that swaps data= for json= (which xAI would silently reject).

How to Test

# 1. New regression suite passes
python -m pytest tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py -q
# expected: 14 passed

# 2. All existing xAI OAuth tests still pass (no regression to refresh,
#    discovery, 403-entitlement handling, or recovery paths)
python -m pytest tests/hermes_cli/test_auth_xai_oauth_provider.py \
                 tests/run_agent/test_codex_xai_oauth_recovery.py -q
# expected: 87 passed

# 3. Combined sweep
python -m pytest tests/hermes_cli/test_auth_xai_oauth_provider.py \
                 tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py \
                 tests/run_agent/test_codex_xai_oauth_recovery.py -q
# expected: 101 passed

Why "defensive echo" and not "switch to a different flow"?

  • RFC 7636 §4.5 explicitly only requires code_verifier at the token endpoint — so the pre-fix code was technically correct.
  • But xAI's server rejects the technically-correct request, and the issue is P1 (login is broken).
  • Echoing code_challenge at the token endpoint is harmless for strict servers (they MUST ignore unknown parameters per RFC 6749 §3.2) and necessary for xAI.
  • Switching to device-code or PAR-style flows would require xAI registering us for a different OAuth profile — out of our control.
  • The same defensive echo is used by other production OAuth clients targeting servers with this quirk (Spotify-style cross-validation, certain Auth0 deployments, etc.).

xxxigm added 2 commits May 16, 2026 23:11
…cceeds

xAI's OAuth implementation at ``auth.x.ai`` validates the PKCE
``code_challenge`` at the **token** endpoint, not just at the
authorize step.  When Hermes sends the standards-compliant token
POST with ``code_verifier`` alone — exactly what RFC 7636 §4.5
prescribes — xAI rejects the exchange with ``code_challenge is
required`` and the user is stuck with no working OAuth login.

The fix:

* Extract the token POST into ``_xai_oauth_exchange_code_for_tokens``
  so the wire format is unit-testable in isolation.
* Send the original ``code_challenge`` and ``code_challenge_method``
  in the form body alongside ``code_verifier``.  Strict RFC-compliant
  servers ignore the extras at the token endpoint, and xAI's
  permissive implementation accepts the exchange.  This is the
  standard "defensive echo" workaround used by every OAuth client
  that targets a server with this quirk.
* Refuse to fire the POST when ``code_verifier`` is empty — leaking
  the authorization code to a server that can't redeem it is worse
  than failing locally with an actionable error.  The new error
  code is ``xai_pkce_verifier_missing`` and the message points at
  this issue for context.
* Surface the HTTP status code prominently in the 4xx error message
  (``xAI token exchange failed (HTTP 400). Response: …``) so users
  and maintainers can tell a 400 (bad request / PKCE problem) from
  a 403 (tier denied, see NousResearch#26847) at a glance instead of parsing
  the JSON body by eye.

Closes NousResearch#26990
14 focused tests on the extracted helper
``_xai_oauth_exchange_code_for_tokens`` cover:

Core contract:
* ``code_verifier`` is on the wire (RFC 7636 §4.5).
* ``code_challenge`` + ``code_challenge_method=S256`` are echoed
  (the NousResearch#26990 defense-in-depth that makes xAI's token endpoint
  stop rejecting valid exchanges).
* ``grant_type=authorization_code``, ``code``, ``redirect_uri``,
  and ``client_id`` are all locked.
* Content-Type is ``application/x-www-form-urlencoded`` (xAI
  rejects ``application/json`` on this endpoint).
* The supplied ``token_endpoint`` URL is used verbatim — no
  hard-coded constant sneaks in via a future refactor.
* ``timeout_seconds`` is forwarded; floored at 20s.

Sanity guard:
* Empty ``code_verifier`` raises ``xai_pkce_verifier_missing``
  with a link to NousResearch#26990 — and NOTHING is sent.  Leaking the auth
  code to a server that can't redeem it is the wrong failure mode.
* Empty ``code_challenge`` omits only the defensive echo; the
  standards-compliant ``code_verifier`` request still goes out so
  RFC-compliant servers keep working.

Error surfacing:
* Non-200 responses include both ``HTTP <status>`` and the body
  verbatim — disambiguates 400 (PKCE / bad request) from 403
  (tier denied, see NousResearch#26847).
* Transport errors are wrapped as ``AuthError`` with the
  ``xai_token_exchange_failed`` code, so the surrounding
  ``format_auth_error`` UI mapping still fires.
* Non-dict JSON payloads raise ``xai_token_exchange_invalid``.
* 200 happy path returns the parsed payload dict verbatim.

End-to-end wire-format guard:
* A real ``httpx.Client`` with a stub transport captures the bytes
  on the wire and asserts every PKCE field round-trips through
  ``urlencode``.  Catches a future refactor that swaps
  ``data=`` for ``json=`` (which xAI would silently reject).
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround area/auth Authentication, OAuth, credential pools provider/xai xAI (Grok) comp/cli CLI entry point, hermes_cli/, setup wizard labels May 16, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Salvaged into main via #27560 — your commits were cherry-picked onto current main with your authorship preserved in git log. The defensive code_challenge echo + 14 regression tests landed verbatim. Thanks for the fix and the thorough test coverage!

@teknium1 teknium1 closed this May 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard P1 High — major feature broken, no workaround provider/xai xAI (Grok) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

xai-oauth token exchange fails: code_challenge is required (PKCE code_verifier missing)

3 participants