CIMD: ChatGPT is refused with invalid_client (lists none in token_endpoint_auth_methods_supported, prefers private_key_jwt)
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
- Domain
- authentication
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_jwtis 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_jwtis advertised, ChatGPT presents an RS256 assertion with the token-endpointaud, so a server with headless agents enabled serves ChatGPT only with the exact-IDtokenEndpointAudienceexception 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
audby 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 singulartoken_endpoint_auth_methodis 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.
- §2 defines the client metadata field
- 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_methodis the requested method, andnonemeans a public client with no client secret. - §3.2.1: the AS MAY replace a requested value with a suitable one.
- §2:
- 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 usesnone. - 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"]→ usenone; - 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.
- client
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-nonemethod 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_jwtfor interactive CIMD clients (RS256 viajwks_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
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from HarperFast/oauth
-
Difficulty 5/5 Over a week Newbie friendliness 18/100
HarperFast/oauth#280 ·
Maintainers usually reply within 1 day
-
MCP OAuth (mcp: true / withMCPAuth) does not populate context.user for Harper core's native /mcp endpointPossibly taken @kriszyp claimed this 1 day ago. Open
HarperFast/oauth#279 · 1 assignee ·
Maintainers usually reply within 1 day
-
Open-issue triage and roadmap (post-2.9.0)Possibly taken @heskew claimed this 15 days ago. Open
HarperFast/oauth#278 · 1 assignee ·
Maintainers usually reply within 1 day
-
Session docs overstate expiry: no default lifetime, and invalidation clears fields rather than deleting the rowPossibly taken @heskew claimed this 15 days ago. Open
HarperFast/oauth#277 · 1 assignee ·
Maintainers usually reply within 1 day
-
Azure ?appid= form of the shared-alias keys URL isn't recognizedPossibly taken @heskew claimed this 15 days ago. Open
HarperFast/oauth#275 · 1 assignee ·
Maintainers usually reply within 1 day
All issues in HarperFast/oauth
Similar issues
-
documentation good first issue theming
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
feature
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
drevendev/PatchPitch#2 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 65/100
521xueweihan/HelloGitHub#4025 ·