OAuth authentication fails at issuer validation
Open 💬 16 comments Opened Jul 8, 2026 by NiceWaffel
What version of Codex CLI is running?
0.143.0
What subscription do you have?
Free
Which model were you using?
_No response_
What platform is your computer?
_No response_
What terminal emulator and version are you using (if applicable)?
_No response_
Codex doctor report
What issue are you seeing?
Trying to log in to an MCP Server using the Authorization Code flow fails with an error:
Error: failed to handle OAuth callback
Caused by:
Authorization server response missing required issuer: expected https://my.mcp.server/issuer
This issue was introduced recently (works in 0.141.0 for example). I assume it is related to https://github.com/modelcontextprotocol/rust-sdk/pull/896
What steps can reproduce the bug?
- Setup an MCP server with OAuth login
- Start logging in using
codex mcp login my_mcp_server
What is the expected behavior?
Codex should handle the issuer contained in the iss parameter of the callback URL (see RFC 9207).
Additional information
_No response_
16 Comments
Reproduced on
0.143.0against a self-hosted MCP server (custom OAuth 2.1 AS, RFC 9207 compliant). I traced this end to end — it's a bug in Codex's callback handler, not inrmcp.rmcpalready exposes the correct API; Codex calls the wrong variant and drops theissbefore validation.TL;DR
Our AS puts a valid
issin the authorization response (RFC 9207). Codex parses the loopback callback, discardsiss, then calls the issuer-lesshandle_callback(code, state).rmcpmeanwhile setsrequire_issuer = truefrom the AS metadata flagauthorization_response_iss_parameter_supported, so it demands anissit was never handed →AuthorizationServerMissingIssuer. It fails 100% of the time for any spec-compliant AS, even though a correctissis on the wire.Evidence — the
issreally is in the callbackCaptured the exact URL the browser delivers to Codex's loopback server:
iss=https://api.example.comis present and equal to the discoveryissuer. Codex throws it away before validation.Root cause —
codex-rs/rmcp-client/src/perform_oauth_login.rs(commit
2c85975)parse_oauth_callback()only capturescode/state/error/error_description; theissparam falls into the catch-all and is dropped:``
rustissmatch key {
"code" => code = Some(decoded),
"state" => state = Some(decoded),
"error" => error = Some(decoded),
"error_description" => error_description = Some(decoded),
_ => {} // <--
discarded here
`}
OauthCallbackResult { code, state }` has no field to carry it.OauthLoginFlow::finish()then calls the issuer-less variant:``
rust
``self.oauth_state
.handle_callback(&code, &csrf_state) // issuer = None
.await
.context("failed to handle OAuth callback")?; // <-- the error wrapper users see
Why
rmcpthen rejects it —crates/rmcp/src/transport/auth.rs(paths/lines on
mainat time of writing)get_authorization_url()derivesrequire_issuerstraight from the AS metadata flag (≈ L1373):``
rust
``let require_issuer = metadata.additional_fields
.get("authorization_response_iss_parameter_supported")
.and_then(|v| v.as_bool())
.unwrap_or(false);
OAuthState::handle_callback→AuthorizationSession::handle_callback→handle_callback_with_issuer(code, csrf, None)→exchange_code_for_token_with_issuer(code, csrf, None).validate_authorization_response_issuer()(≈ L1528) withreceived_issuer = Noneandrequire_issuer = truereturnsAuthError::AuthorizationServerMissingIssuer(≈ L1544-1548).So the RFC 9207 check
rmcpadded is correct; the value simply never reaches it because Codex didn't forward it.The fix —
rmcpalready provides the right entry pointsOAuthStateexposes all three (≈ L3146-3179):handle_callback(code, csrf)— issuer-less (current call)handle_callback_with_issuer(code, csrf, Option<&str>)handle_callback_url(callback_url)— parses the full redirect URL viaAuthorizationCallback::from_redirect_url, which does extractiss(≈ L2697-2707)Option A (minimal): capture
issinparse_oauth_callbackand thread it through:Option B: keep the raw callback URL and hand it to
self.oauth_state.handle_callback_url(&full_url), lettingrmcpparsecode/state/issitself.---
This lines up with @NiceWaffel's guess about modelcontextprotocol/rust-sdk#896: that PR added issuer enforcement to
rmcp. Codex's callback handler never forwardediss, so the new enforcement surfaced the latent gap — hence "works in 0.141.0, broken in 0.143.0."**Confirmed on 0.143.0 and 0.144.0 — the
issis present in the callback; Codex just doesn't forward it to the validator.**The check itself (in bundled
rmcpfrom modelcontextprotocol/rust-sdk#896,crates/rmcp/src/transport/auth.rs) is correct:AuthorizationServerMissingIssuerfires only whenrequire_issuer == true(derived from the AS advertisingauthorization_response_iss_parameter_supported: true) andreceived_issuer == None. AndAuthorizationCallback::from_redirect_urlparsesissfrom the query string just fine.So the bug is in Codex's caller wiring: it reaches the validator with
received_issuer = Noneeven when the authorization server redirects back with a validiss. Likely Codex is callingexchange_code_for_token(which passesNone) instead ofexchange_code_for_token_with_issuer, or isn't propagating the parsedAuthorizationCallback.issuer.Minimal repro (no real IdP needed). A stub authorization server that advertises the flag and sends
isson the callback:{ "issuer": "http://127.0.0.1:9700", "authorization_endpoint": ".../authorize", "token_endpoint": ".../token", "authorization_response_iss_parameter_supported": true }/authorize→302to the loopbackredirect_uriwith?code=...&state=<echoed>&iss=http%3A%2F%2F127.0.0.1%3A9700The redirect Codex receives demonstrably contains
iss:Dropping
authorization_response_iss_parameter_supportedfrom the AS metadata makes login succeed — confirming the callback'sissis never consumed and the check is gated purely on the advertised flag.This breaks OAuth login against fully RFC 9207-compliant servers (e.g. any
node-oidc-provider-based AS, which sendsissand advertises support by default). Downgrading isn't a workable fix since newer models require newer Codex.Independent reproduction on Codex CLI 0.144.1 (
aarch64-apple-darwin, macOS 26.5.1) against a local Auto-K MCP server confirms the callback adapter drops a valid RFC 9207 issuer.Authorization-server metadata returned:
The actual browser request to Codex's loopback listener contained the expected, percent-encoded
iss(authorization code, state, and callback identifier redacted):Codex nevertheless failed with:
I reproduced the source-level diagnosis on the
rust-v0.144.1tag. Incodex-rs/rmcp-client/src/perform_oauth_login.rs,OauthCallbackResultcarries onlycodeandstate;parse_oauth_callbackignoresiss; andfinishcalls the issuer-lesshandle_callback(&code, &csrf_state).I then applied the minimal fix locally:
issuer: Option<String>toOauthCallbackResult.issquery parameter (including percent decoding).handle_callback_with_issuer(&code, &csrf_state, issuer.as_deref()).iss=https%3A%2F%2Fauth.example.com.Fail-before evidence:
Pass-after evidence:
After building the patched 0.144.1 CLI, the unchanged authorization server and MCP stack completed the real flow:
This isolates the failure to Codex's callback plumbing. The authorization server emits the correct
iss, andrmcpcorrectly enforces it after the server advertises RFC 9207 support.Reproduced with
codex-cli 0.144.0-alpha.4, using the binary bundled with ChatGPT.app on macOS:The authorization server is Better Auth 1.6.23 with issuer
http://localhost:3000/api/auth. Dynamic registration, authorization, login, and consent complete successfully, but Codex reports:This matches the callback-plumbing regression described above. A temporary compatibility workaround is to omit
authorization_response_iss_parameter_supportedfrom the MCP-discovered authorization-server metadata.Same issue happening on 0.144.3 on Linux.
Same issue on 0.144.3.
Downgrading to 0.141.0 works.
Confirmed root cause — this is a callback-parsing regression in Codex, not the auth server.
Our MCP server sits in front of a standard OAuth 2.1 / OIDC authorization server that does emit the RFC 9207
issparameter and advertisesauthorization_response_iss_parameter_supported: true. Captured the literal redirect the AS sends to the loopback callback (values redacted):So
issis present and correct on the wire. Codex still fails with:The reason: in
codex-rs/rmcp-client/src/perform_oauth_login.rs, the callback query parser only matchescode,state,error,error_description—isshits the_ => {}arm and is dropped. It then calls the no-issuer variantoauth_state.handle_callback(&code, &csrf_state), which passesreceived_issuer = Noneinto rmcp.Since rmcp modelcontextprotocol/rust-sdk#896 sets
require_issuerfromauthorization_response_iss_parameter_supported, andreceived_issuerisNone,validate_authorization_response_issuerreturnsAuthorizationServerMissingIssuer— even though the AS did sendiss. That's why 0.141 works (pre-that-change, no enforcement) and 0.143+ breaks.Fix: have the callback parser capture
issand pass it through — either use the SDK'sAuthorizationCallback::from_redirect_url/handle_callback_url, or add"iss" => iss = Some(decoded)and callhandle_callback_with_issuer(&code, &csrf_state, iss.as_deref()).Reproduced: fails on 0.144.1, succeeds on 0.141.0 against the identical endpoint.
Same issue on 0.144.4
Workaround for anyone/any agents reading this:
Temporary workaround for npm-installed Codex CLI users:
This avoids the OAuth callback regression in Codex 0.143.x–0.144.3. Upgrade again once the issue is fixed.
Independent reproduction with n8n's official instance-level MCP server.
Environment
26.707.722210.144.2https://<redacted-n8n-host>/mcp-server/http2.30.1, behind a reverse proxyAuthorization-server metadata
GET https://<redacted-n8n-host>/.well-known/oauth-authorization-serverreturns the following structure (hostname replaced):The protected-resource metadata correctly points to the same authorization server:
Reproduction
``
text
``codex mcp login n8n_test
Codexunder connected OAuth clients.``
text
``Authentication complete. You may close this window.
```text
Error: failed to handle OAuth callback
Caused by:
Authorization server response missing required issuer: expected https://<redacted-n8n-host>
```
codex mcp liststill reports the server asNot logged in.This is reproducible through both Codex Desktop authentication and the bundled
codex mcp logincommand. Changing browsers does not affect the result.Expected behavior
Codex should preserve the RFC 9207
issauthorization-response parameter, validate it against the issuer advertised by the authorization server, exchange the authorization code, and store the OAuth credentials.This appears to be the same callback-plumbing regression already diagnosed in this issue:
parse_oauth_callback()dropsiss, after which Codex calls the issuer-lesshandle_callback(&code, &csrf_state)variant. The n8n 2.30.1 implementation explicitly includesissin successful and error authorization redirects:https://github.com/n8n-io/n8n/blob/n8n%402.30.1/packages/cli/src/modules/oauth-server/oauth.helpers.ts
No authorization codes, access tokens, refresh tokens, client secrets, or session identifiers are included in this report.
Independent reproduction on Codex CLI
0.144.4using an RFC 9207-compatible authorization server.The flow is a standard MCP OAuth authorization-code flow with PKCE:
issuerandauthorization_response_iss_parameter_supported: true.code,state, and a percent-encodedissexactly matching the discovered issuer.Authentication complete. You may close this window.We verified that the callback arriving at Codex has this shape:
The
issvalue is therefore present and correct on the wire. This matches the source-level diagnosis already discussed here:parse_oauth_callback()retainscodeandstatebut ignoresiss, after which Codex calls the issuer-lesshandle_callback(&code, &csrf_state)API.As a confirmation, temporarily advertising
authorization_response_iss_parameter_supported: falsewhile leaving the actualissresponse parameter unchanged makes the same Codex login complete successfully. That isolates the failure to the callback issuer not being forwarded into validation.It would be helpful for Codex to preserve
issand callhandle_callback_with_issuer(...)(or pass the full callback URL to the SDK), with a regression test covering an authorization server that both advertises RFC 9207 support and returns a matching issuer.I reproduced this regression and prepared a focused fix: https://github.com/kriptoburak/codex/commit/acb1f572d689a2f15378c8e831d7d6eb3be292c2
The callback adapter currently discards the decoded RFC 9207 iss value and then invokes the issuer-less rmcp callback handler. The patch preserves iss in OauthCallbackResult and passes it to handle_callback_with_issuer, with regression coverage for a percent-encoded issuer.
Verification:
I could not open a PR because this repository currently limits pull requests to collaborators. A maintainer can cherry-pick the commit or enable an external PR for the branch: https://github.com/kriptoburak/codex/tree/codex/preserve-mcp-oauth-issuer
please contribute to the discussion https://github.com/openai/codex/discussions/33682 to raise visibility with the team. The MCP bug reports are very clearly being ignored right now.
rust-v0.144.5(latest stable) andrust-v0.145.0-alpha.20both still call the issuer-lesshandle_callbackatcodex-rs/rmcp-client/src/perform_oauth_login.rs:606, so the fix described above doesn't seem to have shipped in a release yet.This blocks every interactive OAuth MCP login from Codex against any RFC 9207-compliant authorization server.
Would be great to get the one-line fix (
handle_callback_with_issuer/handle_callback_url) into an upcoming 0.145 build?Reproduced on
codex-cli 0.145.0-alpha.18bundled with the Codex macOS app against Keycloak26.3.1.The authorization server advertises
authorization_response_iss_parameter_supported: true, the Keycloak client hasexclude.issuer.from.auth.response=false(confirmed both in the Admin UI and directly in Keycloak's PostgreSQLclient_attributestable), and the browser authorization flow still fails with:Source inspection of the exact
rust-v0.145.0-alpha.18tag confirmsparse_oauth_callback()discardsissandfinish()still calls the issuer-lesshandle_callback(&code, &csrf_state), while bundledrmcp 1.8.0exposeshandle_callback_with_issuer(...).I also traced the regression boundary across official tags:
rust-v0.143.0-alpha.6:rmcp 1.7.0, unaffectedrust-v0.143.0-alpha.7:rmcp 1.8.0, first affected alpharust-v0.142.5: last unaffected stable releaserust-v0.143.0: first affected stable releaseRunning
npx -y @openai/codex@0.142.5 mcp login <server>completes successfully against the same unchanged Keycloak and MCP deployment, confirming this is the Codex callback integration regression rather than an authorization-server configuration issue.Reproduced with Google Workspace remote MCP on macOS arm64 using bundled
codex-cli 0.145.0-alpha.18.Server
https://drivemcp.googleapis.com/mcp/v1Reproduction
Codex generates an authorization request with the expected
scopeandresource=https://drivemcp.googleapis.com/mcp/v1. After completing Google consent, the CLI fails:Google's localhost authorization callback omits
iss. I also confirmed that addingissto the configured redirect URI is not viable: Google rejects it withInvalid redirect_uri contains reserved response param iss.This appears to be the same regression, now blocking the current Google Workspace MCP Developer Preview endpoints as well.