Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

CIMD: ChatGPT is refused with invalid_client (lists none in token_endpoint_auth_methods_supported, prefers private_key_jwt)

Open
#244 4 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@heskew is already working on this.

Since Oct 1, 2026.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
70/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
typescript

Research direction

Start in src/lib/mcp/cimd.ts at validateCimdDocument, especially the validation around lines 653–661, and compare it with the current behavior for documents without token_endpoint_auth_methods_supported. The change is done when a valid list containing none selects the existing public-client path, while malformed lists, mismatched preferences, and unsupported shared-secret methods fail with invalid_client; documents without the list remain unchanged.

Written by the indexing model from the issue text.

Description

Status (2026-10-02)

Fixed: ChatGPT links again. #245 (merged) implements the design in this comment: one permitted client-auth method per interactive CIMD client, verification of presented assertions, and grant binding.

  • By default ChatGPT resolves to none + PKCE.
  • With mcp.clientIdMetadataDocuments.privateKeyJwt.enabled: true, or with headless agents enabled, private_key_jwt is advertised and verified for interactive CIMD clients.
  • Headless agents on means ChatGPT needs the audience exception. Advertisement is server-wide, and Run C showed ChatGPT ignores the advertised signing algorithms. Whenever private_key_jwt is advertised, ChatGPT presents an RS256 assertion with the token-endpoint aud, so a server with headless agents enabled serves ChatGPT only with the exact-ID tokenEndpointAudience exception configured.

Interactive private_key_jwt stays opt-in. The live capture showed that whenever private_key_jwt is advertised, ChatGPT presents it on the code exchange and every refresh. Every assertion's aud was the token-endpoint URL, never the issuer. RFC 7523bis forbids that audience, because it allows audience injection: an AS can advertise another server's token-endpoint URL as its own and collect assertions addressed to that server. Advertisement is server-wide, so turning it on by default would refuse ChatGPT on every deployment without the audience exception. The ways to keep it on by default were rejected in #253:

  • accept the token-endpoint aud by default;
  • ignore such assertions and fall back to none, which breaks verify-or-reject and the method binding.

To opt in now: set privateKeyJwt.enabled: true and add the exact-ID tokenEndpointAudience exception for https://chatgpt.com/oauth/client.json, with an expiry you choose. #253 documents these settings and the audience trade-off.

Release gate for default activation:

  • ChatGPT signs its assertions with the issuer as aud. We don't expect a vendor response, so we observe this directly instead:
    • re-run the capture before each release;
    • on deployments that opted in, watch the token endpoint's logged audience form for ChatGPT (aud form), which shows the switch when it happens.
    • Publication of RFC 7523bis, which forbids the token-endpoint aud, is the most likely trigger.
  • A real ChatGPT code exchange and refresh captured (2026-09-30, linked above).
  • Key rotation tested with controlled keys, and a real rotation observed if one occurs.
  • Existing headless consumers moved to the issuer aud (tpsdev-ai/flair#2103). This doesn't block the interactive default, because the headless audience setting is separate.

When the first item is met, the default flip is a small follow-up to #253.


CIMD: ChatGPT is refused with invalid_client (lists none in token_endpoint_auth_methods_supported, prefers private_key_jwt)

What fails

ChatGPT cannot link to an MCP server that uses @harperfast/oauth. This was seen on 2.5.0 and 2.7.0 and is still present on main at a78e96f. Every authorize and token request with ChatGPT's client_id fails CIMD resolution:

invalid_client: token_endpoint_auth_method 'private_key_jwt' is not supported for interactive CIMD clients; use 'none'

The error is thrown by validateCimdDocument in src/lib/mcp/cimd.ts (lines 653–661 on main; dist/lib/mcp/cimd.js:579).

Why

ChatGPT's client ID metadata document (https://chatgpt.com/oauth/client.json, fetched 2026-09-29), trimmed:

{
	"client_id": "https://chatgpt.com/oauth/client.json",
	"redirect_uris": ["https://chatgpt.com/connector_platform_oauth_redirect"],
	"grant_types": ["authorization_code", "refresh_token"],
	"token_endpoint_auth_method": "private_key_jwt",
	"token_endpoint_auth_methods_supported": ["none", "private_key_jwt"],
	"token_endpoint_auth_signing_alg": "RS256",
	"jwks_uri": "https://chatgpt.com/oauth/jwks.json"
}

The singular field is ChatGPT's preference. The plural list holds every method it can use. OpenAI's Apps SDK auth docs say ChatGPT picks a method from the intersection of this list and the AS's token_endpoint_auth_methods_supported. They also say that an AS reading the plural field "should accept any method in the intersection" and "must reject methods outside the intersection". The plugin reads only the singular field.

The plugin can't honour the preference as it stands. It supports private_key_jwt only for the headless client_credentials shape (EdDSA keys, inline jwks, jwks_uri rejected, allowlist required). ChatGPT would sign RS256 assertions with keys published at a jwks_uri, on the authorization_code grant. But ChatGPT also supports none + PKCE, and that is exactly what the plugin supports for interactive CIMD clients.

Spec basis for treating it as a public client

  • OpenID Connect RP Metadata Choices 1.0 (Final, March 2026):
    • §2 defines the client metadata field token_endpoint_auth_methods_supported. If the singular token_endpoint_auth_method is also present, it MUST be one of the listed methods.
    • §6.1 registers the field in the IANA OAuth Dynamic Client Registration Metadata registry.
    • §4: when the server does not support the singular value but does support another listed choice, it SHOULD NOT treat that as an error and SHOULD use a choice it supports.
  • CIMD (draft-ietf-oauth-client-id-metadata-document-02, §4): a document's values are the ones in that RFC 7591 registry, so the list is a valid CIMD field.
  • RFC 7591:
    • §2: token_endpoint_auth_method is the requested method, and none means a public client with no client secret.
    • §3.2.1: the AS MAY replace a requested value with a suitable one.
  • OAuth 2.1 (draft-ietf-oauth-v2-1-16):
    • §2.1: clients without credentials are public clients.
    • §4.1.1: the AS MUST enforce code_challenge / code_verifier.
    • §4.3.1: public clients need refresh-token replay protection. The plugin already rotates refresh tokens and revokes the family on replay.
  • MCP authorization (2025-11-25), "Client ID Metadata Documents": clients MAY use private_key_jwt, and the spec's example document uses none.
  • MCP SEP-3149 (draft, modelcontextprotocol/modelcontextprotocol#3149, co-authored by OpenAI; ChatGPT's document is its reference implementation). Its method resolution:
    • client ["private_key_jwt", "none"] + AS ["none"] → use none;
    • the AS MUST NOT reject a client solely because its singular preference is unsupported when another listed method is mutually supported;
    • an empty intersection → reject.

One point needs a maintainer's judgement. CIMD (-00 §6.2, -02 §8.2) says a document that declares private_key_jwt "establishes this client as a confidential client", and -02 adds that the AS MUST then require RFC 7523 client authentication. Read against the singular field alone, that supports refusing ChatGPT, which is what the plugin does today. The proposal reads it together with RP Metadata Choices. A client that also lists none has said it can run as a public client, and the method the AS selects from the list becomes the registered method.

Proposed fix

In validateCimdDocument, treat token_endpoint_auth_methods_supported as authoritative whenever the document carries it:

  • It includes none → resolve as a public client (none + S256 PKCE, no secret).
  • It does not include none → refuse, as a singular non-none method is refused today.

The list also fails closed with invalid_client when it is not an array of strings, when the singular preference is not one of its members, or when it names a shared-secret method (CIMD §4.1).

Documents without the list resolve exactly as today. PKCE, redirect-URI and redirect-host validation are unchanged. A PR follows.

Related, not in scope

With mcp.clientCredentials.enabled: true, the AS metadata advertises private_key_jwt for every client. private_key_jwt is then in ChatGPT's intersection, so ChatGPT will send an RS256 client_assertion on the authorization_code exchange. The public-client path does not read client_assertion, so the assertion is ignored and the exchange succeeds on PKCE alone. That is also how any none client is treated today. Two ways to handle it properly, which need a separate decision:

  • verify private_key_jwt for interactive CIMD clients (RS256 via jwks_uri, which adds a second fetch that needs SSRF guards);
  • reject assertions on the public path, which breaks ChatGPT on servers that enable clientCredentials.
Dominant language
JavaScript
Stars
1
Forks
3
Avg merge
1d 10m
Merged PRs (30d)
26

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from HarperFast/oauth

All issues in HarperFast/oauth

Similar issues

More JavaScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.