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_

View original on GitHub ↗

16 Comments

fatalist · 11 days ago

Reproduced on 0.143.0 against 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 in rmcp. rmcp already exposes the correct API; Codex calls the wrong variant and drops the iss before validation.

TL;DR

Our AS puts a valid iss in the authorization response (RFC 9207). Codex parses the loopback callback, discards iss, then calls the issuer-less handle_callback(code, state). rmcp meanwhile sets require_issuer = true from the AS metadata flag authorization_response_iss_parameter_supported, so it demands an iss it was never handed → AuthorizationServerMissingIssuer. It fails 100% of the time for any spec-compliant AS, even though a correct iss is on the wire.

Evidence — the iss really is in the callback

Captured the exact URL the browser delivers to Codex's loopback server:

http://127.0.0.1:PORT/callback/<callback_id>?code=<long-code>&state=<state>&iss=https%3A%2F%2Fapi.example.com

iss=https://api.example.com is present and equal to the discovery issuer. Codex throws it away before validation.

Root cause — codex-rs/rmcp-client/src/perform_oauth_login.rs

(commit 2c85975)

  1. parse_oauth_callback() only captures code / state / error / error_description; the iss param falls into the catch-all and is dropped:

``rust
match key {
"code" => code = Some(decoded),
"state" => state = Some(decoded),
"error" => error = Some(decoded),
"error_description" => error_description = Some(decoded),
_ => {} // <--
iss discarded here
}
`
OauthCallbackResult { code, state }` has no field to carry it.

  1. 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 rmcp then rejects it — crates/rmcp/src/transport/auth.rs

(paths/lines on main at time of writing)

  • get_authorization_url() derives require_issuer straight 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_callbackAuthorizationSession::handle_callbackhandle_callback_with_issuer(code, csrf, None)exchange_code_for_token_with_issuer(code, csrf, None).
  • validate_authorization_response_issuer() (≈ L1528) with received_issuer = None and require_issuer = true returns AuthError::AuthorizationServerMissingIssuer (≈ L1544-1548).

So the RFC 9207 check rmcp added is correct; the value simply never reaches it because Codex didn't forward it.

The fix — rmcp already provides the right entry points

OAuthState exposes 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 via AuthorizationCallback::from_redirect_url, which does extract iss (≈ L2697-2707)

Option A (minimal): capture iss in parse_oauth_callback and thread it through:

// parse_oauth_callback:
"iss" => issuer = Some(decoded),
// OauthCallbackResult { code, state, issuer: Option<String> }

// finish():
self.oauth_state
    .handle_callback_with_issuer(&code, &csrf_state, issuer.as_deref())
    .await
    .context("failed to handle OAuth callback")?;

Option B: keep the raw callback URL and hand it to self.oauth_state.handle_callback_url(&full_url), letting rmcp parse code / state / iss itself.

---

This lines up with @NiceWaffel's guess about modelcontextprotocol/rust-sdk#896: that PR added issuer enforcement to rmcp. Codex's callback handler never forwarded iss, so the new enforcement surfaced the latent gap — hence "works in 0.141.0, broken in 0.143.0."

tobernguyen · 11 days ago

**Confirmed on 0.143.0 and 0.144.0 — the iss is present in the callback; Codex just doesn't forward it to the validator.**

The check itself (in bundled rmcp from modelcontextprotocol/rust-sdk#896, crates/rmcp/src/transport/auth.rs) is correct: AuthorizationServerMissingIssuer fires only when require_issuer == true (derived from the AS advertising authorization_response_iss_parameter_supported: true) and received_issuer == None. And AuthorizationCallback::from_redirect_url parses iss from the query string just fine.

So the bug is in Codex's caller wiring: it reaches the validator with received_issuer = None even when the authorization server redirects back with a valid iss. Likely Codex is calling exchange_code_for_token (which passes None) instead of exchange_code_for_token_with_issuer, or isn't propagating the parsed AuthorizationCallback.issuer.

Minimal repro (no real IdP needed). A stub authorization server that advertises the flag and sends iss on the callback:

  • AS metadata: { "issuer": "http://127.0.0.1:9700", "authorization_endpoint": ".../authorize", "token_endpoint": ".../token", "authorization_response_iss_parameter_supported": true }
  • /authorize302 to the loopback redirect_uri with ?code=...&state=<echoed>&iss=http%3A%2F%2F127.0.0.1%3A9700
codex mcp add stub --url http://127.0.0.1:9700/mcp --oauth-client-id stub-client
codex mcp login stub
# Error: ... Authorization server response missing required issuer: expected http://127.0.0.1:9700

The redirect Codex receives demonstrably contains iss:

302 http://127.0.0.1:51137/callback/...?code=stub-auth-code&state=...&iss=http%3A%2F%2F127.0.0.1%3A9700

Dropping authorization_response_iss_parameter_supported from the AS metadata makes login succeed — confirming the callback's iss is 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 sends iss and advertises support by default). Downgrading isn't a workable fix since newer models require newer Codex.

CaliLuke · 11 days ago

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:

{
  "issuer": "https://localhost:3000/api/auth",
  "authorization_response_iss_parameter_supported": true
}

The actual browser request to Codex's loopback listener contained the expected, percent-encoded iss (authorization code, state, and callback identifier redacted):

http://127.0.0.1:64626/callback/<redacted>?code=<redacted>&state=<redacted>&iss=https%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth

Codex nevertheless failed with:

Error: failed to handle OAuth callback

Caused by:
    Authorization server response missing required issuer: expected https://localhost:3000/api/auth

I reproduced the source-level diagnosis on the rust-v0.144.1 tag. In codex-rs/rmcp-client/src/perform_oauth_login.rs, OauthCallbackResult carries only code and state; parse_oauth_callback ignores iss; and finish calls the issuer-less handle_callback(&code, &csrf_state).

I then applied the minimal fix locally:

  1. Add issuer: Option<String> to OauthCallbackResult.
  2. Parse the iss query parameter (including percent decoding).
  3. Call handle_callback_with_issuer(&code, &csrf_state, issuer.as_deref()).
  4. Add a regression test using iss=https%3A%2F%2Fauth.example.com.

Fail-before evidence:

error[E0560]: struct `OauthCallbackResult` has no field named `issuer`

Pass-after evidence:

running 7 tests
...
test perform_oauth_login::tests::parse_oauth_callback_preserves_authorization_server_issuer ... ok
test result: ok. 7 passed; 0 failed

After building the patched 0.144.1 CLI, the unchanged authorization server and MCP stack completed the real flow:

Successfully logged in to MCP server 'autok_local'.

This isolates the failure to Codex's callback plumbing. The authorization server emits the correct iss, and rmcp correctly enforces it after the server advertises RFC 9207 support.

EvanNotFound · 8 days ago

Reproduced with codex-cli 0.144.0-alpha.4, using the binary bundled with ChatGPT.app on macOS:

/Applications/ChatGPT.app/Contents/Resources/codex mcp login <server>

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:

Authorization server response missing required issuer:
expected http://localhost:3000/api/auth

This matches the callback-plumbing regression described above. A temporary compatibility workaround is to omit authorization_response_iss_parameter_supported from the MCP-discovered authorization-server metadata.

microHoffman · 7 days ago

Same issue happening on 0.144.3 on Linux.

aditya-findem · 7 days ago

Same issue on 0.144.3.
Downgrading to 0.141.0 works.

antoniocasagrande-airia · 6 days ago

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 iss parameter and advertises authorization_response_iss_parameter_supported: true. Captured the literal redirect the AS sends to the loopback callback (values redacted):

GET /callback?state=<state>
    &iss=https%3A%2F%2Fauth.example.com
    &code=<code>

So iss is present and correct on the wire. Codex still fails with:

Authorization server response missing required issuer:
expected https://auth.example.com

The reason: in codex-rs/rmcp-client/src/perform_oauth_login.rs, the callback query parser only matches code, state, error, error_descriptioniss hits the _ => {} arm and is dropped. It then calls the no-issuer variant oauth_state.handle_callback(&code, &csrf_state), which passes received_issuer = None into rmcp.

Since rmcp modelcontextprotocol/rust-sdk#896 sets require_issuer from authorization_response_iss_parameter_supported, and received_issuer is None, validate_authorization_response_issuer returns AuthorizationServerMissingIssuer — even though the AS did send iss. That's why 0.141 works (pre-that-change, no enforcement) and 0.143+ breaks.

Fix: have the callback parser capture iss and pass it through — either use the SDK's AuthorizationCallback::from_redirect_url / handle_callback_url, or add "iss" => iss = Some(decoded) and call handle_callback_with_issuer(&code, &csrf_state, iss.as_deref()).

Reproduced: fails on 0.144.1, succeeds on 0.141.0 against the identical endpoint.

batmanwarrior · 6 days ago

Same issue on 0.144.4

aditya-findem · 6 days ago

Workaround for anyone/any agents reading this:

Temporary workaround for npm-installed Codex CLI users:

npm install -g @openai/codex@0.141.0
hash -r
codex --version

codex mcp login <mcp-server-name> \
  --scopes openid,profile,offline_access,mcp:read,mcp:call

This avoids the OAuth callback regression in Codex 0.143.x–0.144.3. Upgrade again once the issue is fixed.

Lyhtande · 5 days ago

Independent reproduction with n8n's official instance-level MCP server.

Environment

  • Windows 11 x64
  • Codex Desktop build: 26.707.72221
  • Bundled Codex CLI: 0.144.2
  • MCP transport: Streamable HTTP
  • MCP server: official n8n instance-level MCP at https://<redacted-n8n-host>/mcp-server/http
  • n8n: self-hosted 2.30.1, behind a reverse proxy

Authorization-server metadata

GET https://<redacted-n8n-host>/.well-known/oauth-authorization-server returns the following structure (hostname replaced):

{
  "issuer": "https://<redacted-n8n-host>",
  "authorization_endpoint": "https://<redacted-n8n-host>/mcp-oauth/authorize",
  "token_endpoint": "https://<redacted-n8n-host>/mcp-oauth/token",
  "registration_endpoint": "https://<redacted-n8n-host>/mcp-oauth/register",
  "revocation_endpoint": "https://<redacted-n8n-host>/mcp-oauth/revoke",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post", "client_secret_basic"],
  "code_challenge_methods_supported": ["S256"],
  "authorization_response_iss_parameter_supported": true
}

The protected-resource metadata correctly points to the same authorization server:

{
  "resource": "https://<redacted-n8n-host>/mcp-server/http",
  "bearer_methods_supported": ["header"],
  "authorization_servers": ["https://<redacted-n8n-host>"]
}

Reproduction

  1. Configure the server as a Streamable HTTP MCP server.
  2. Run:

``text
codex mcp login n8n_test
``

  1. Complete n8n's consent page. Dynamic client registration and consent succeed, and n8n lists Codex under connected OAuth clients.
  2. n8n redirects to Codex's loopback callback, which displays:

``text
Authentication complete. You may close this window.
``

  1. The CLI then fails with:

```text
Error: failed to handle OAuth callback

Caused by:
Authorization server response missing required issuer: expected https://<redacted-n8n-host>
```

  1. codex mcp list still reports the server as Not logged in.

This is reproducible through both Codex Desktop authentication and the bundled codex mcp login command. Changing browsers does not affect the result.

Expected behavior

Codex should preserve the RFC 9207 iss authorization-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() drops iss, after which Codex calls the issuer-less handle_callback(&code, &csrf_state) variant. The n8n 2.30.1 implementation explicitly includes iss in 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.

ChiragAgg5k · 5 days ago

Independent reproduction on Codex CLI 0.144.4 using an RFC 9207-compatible authorization server.

The flow is a standard MCP OAuth authorization-code flow with PKCE:

  1. Codex discovers the authorization server through the MCP protected-resource metadata.
  2. Authorization-server metadata includes an issuer and authorization_response_iss_parameter_supported: true.
  3. The authorization server redirects to Codex's loopback callback with code, state, and a percent-encoded iss exactly matching the discovered issuer.
  4. The browser receives Authentication complete. You may close this window.
  5. Codex then fails with:
Error: failed to handle OAuth callback

Caused by:
    Authorization server response missing required issuer: expected <discovered issuer>

We verified that the callback arriving at Codex has this shape:

http://127.0.0.1:<port>/callback/<id>?code=<redacted>&state=<redacted>&iss=https%3A%2F%2F<authorization-server>%2F<issuer-path>

The iss value is therefore present and correct on the wire. This matches the source-level diagnosis already discussed here: parse_oauth_callback() retains code and state but ignores iss, after which Codex calls the issuer-less handle_callback(&code, &csrf_state) API.

As a confirmation, temporarily advertising authorization_response_iss_parameter_supported: false while leaving the actual iss response 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 iss and call handle_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.

kriptoburak · 4 days ago

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:

  • just test -p codex-rmcp-client: 112 passed
  • just fix -p codex-rmcp-client: passed

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

CaliLuke · 4 days ago

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.

emlazzarin · 3 days ago

rust-v0.144.5 (latest stable) and rust-v0.145.0-alpha.20 both still call the issuer-less handle_callback at codex-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?

dfradehubs · 3 days ago

Reproduced on codex-cli 0.145.0-alpha.18 bundled with the Codex macOS app against Keycloak 26.3.1.

The authorization server advertises authorization_response_iss_parameter_supported: true, the Keycloak client has exclude.issuer.from.auth.response=false (confirmed both in the Admin UI and directly in Keycloak's PostgreSQL client_attributes table), and the browser authorization flow still fails with:

Authorization server response missing required issuer: expected https://<keycloak-realm-issuer>

Source inspection of the exact rust-v0.145.0-alpha.18 tag confirms parse_oauth_callback() discards iss and finish() still calls the issuer-less handle_callback(&code, &csrf_state), while bundled rmcp 1.8.0 exposes handle_callback_with_issuer(...).

I also traced the regression boundary across official tags:

  • rust-v0.143.0-alpha.6: rmcp 1.7.0, unaffected
  • rust-v0.143.0-alpha.7: rmcp 1.8.0, first affected alpha
  • rust-v0.142.5: last unaffected stable release
  • rust-v0.143.0: first affected stable release

Running 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.

aioue · 2 days ago

Reproduced with Google Workspace remote MCP on macOS arm64 using bundled codex-cli 0.145.0-alpha.18.

Server

  • https://drivemcp.googleapis.com/mcp/v1
  • Google OAuth desktop client
  • Google Workspace Developer Preview project registered; Drive API and Drive MCP API enabled

Reproduction

codex mcp login google-drive \
  --scopes https://www.googleapis.com/auth/drive.readonly,https://www.googleapis.com/auth/drive.file

Codex generates an authorization request with the expected scope and resource=https://drivemcp.googleapis.com/mcp/v1. After completing Google consent, the CLI fails:

Error: failed to handle OAuth callback

Caused by:
    Authorization server response missing required issuer: expected https://accounts.google.com

Google's localhost authorization callback omits iss. I also confirmed that adding iss to the configured redirect URI is not viable: Google rejects it with Invalid 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.