Skip to content

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

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-4631ae05
May 17, 2026
Merged

fix(xai-oauth): echo code_challenge in token POST so PKCE exchange succeeds (#26990)#27560
teknium1 merged 2 commits into
mainfrom
hermes/hermes-4631ae05

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Salvage of #26999 by @xxxigm, cherry-picked onto current main with authorship preserved.

Summary

xAI's OAuth token endpoint rejects some users' code-for-token exchanges with code_challenge is required even though Hermes was sending a valid code_verifier (RFC 7636 §4.5 compliant). Fix is to echo the original code_challenge + code_challenge_method=S256 in the token POST as defense-in-depth — harmless for strict RFC-compliant servers (RFC 6749 §3.2 says they MUST ignore unknown parameters), necessary for xAI's stricter code paths.

Most users don't hit this. The minority who do are likely routed to a stricter xAI backend variant, account-tier-gated path, or transient state where xAI's authorize-step session storage didn't retain the PKCE state. The defensive echo unblocks them without affecting anyone else.

Changes

  • hermes_cli/auth.py — extracts the token POST into _xai_oauth_exchange_code_for_tokens helper. Sends code_verifier as before, also echoes code_challenge + code_challenge_method=S256. Refuses to POST locally if code_verifier is empty (new error code xai_pkce_verifier_missing). Embeds HTTP status code in 4xx errors so callers can tell 400 (bad request) from 403 (tier-denied) at a glance.
  • tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py — 14 new regression tests pinning wire format, including a real-httpx.Client + stub-transport test that catches future data=json= refactors.

Validation

scripts/run_tests.sh tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py \
                     tests/hermes_cli/test_auth_xai_oauth_provider.py \
                     tests/run_agent/test_codex_xai_oauth_recovery.py
# 99 passed in 2.69s (14 new + 85 pre-existing)

E2E verified the actual wire bytes go out form-urlencoded with all PKCE fields, the empty-verifier guard refuses to POST, and the missing-challenge case still sends the standards-compliant request.

Closes #26990
Co-authored-by: xxxigm tuancanhnguyen706@gmail.com

xxxigm added 2 commits May 17, 2026 11:51
…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 #26847) at a glance instead of parsing
  the JSON body by eye.

Closes #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 #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 #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 #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).
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-4631ae05 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 8708 on HEAD, 8705 on base (🆕 +3)

🆕 New issues (3):

Rule Count
unresolved-import 2
invalid-argument-type 1
First entries
tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py:32: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py:31: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
tests/hermes_cli/test_xai_oauth_pkce_token_exchange.py:286: [invalid-argument-type] invalid-argument-type: Argument to function `_ok_response` is incorrect: Expected `dict[Unknown, Unknown]`, found `list[int]`

✅ Fixed issues: none

Unchanged: 4587 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@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) labels May 17, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Salvage of #26999 — same fix for #26990 (xAI PKCE code_challenge). Supersedes #26999.

@teknium1 teknium1 merged commit e3f7ff1 into main May 17, 2026
20 of 21 checks passed
@teknium1 teknium1 deleted the hermes/hermes-4631ae05 branch May 17, 2026 19:35
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 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