Skip to content

Commit 7445cb3

Browse files
julienldclaude
andauthored
fix(oauth): add OpenID Configuration endpoint for ChatGPT compatibility (#531)
* fix(oauth): add OpenID Configuration endpoint for ChatGPT compatibility ChatGPT's MCP connector expects /.well-known/openid-configuration in addition to /.well-known/oauth-authorization-server. Per RFC 8414, many OAuth servers support both endpoints with identical metadata for compatibility. This adds the openid-configuration endpoint that returns the same enhanced metadata as the oauth-authorization-server endpoint, fixing ChatGPT integration while maintaining claude.ai compatibility. Fixes #368 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(oauth): grant default scopes when client registers without them ChatGPT MCP connector registers clients without specifying scopes, then requests scopes during authorization. The MCP SDK rejects this with "invalid_scope: Client was not registered with scope X". Fix: When a client registers without scopes, automatically grant all valid scopes (homeassistant, mcp). This allows ChatGPT to complete the OAuth flow while maintaining security (only valid scopes granted). Changes: - Auto-grant valid scopes when client_info.scope is None - Add test for ChatGPT compatibility behavior - Log when default scopes are granted for visibility Fixes the "invalid_scope" error shown in network traces Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 72f6426 commit 7445cb3

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

src/ha_mcp/auth/provider.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,21 @@ async def enhanced_metadata_handler(request: Request) -> Response:
248248
else:
249249
enhanced_routes.append(route)
250250

251+
# Add OpenID Configuration endpoint for ChatGPT compatibility
252+
# ChatGPT expects /.well-known/openid-configuration (OpenID Connect Discovery)
253+
# in addition to /.well-known/oauth-authorization-server (OAuth 2.1)
254+
# Per RFC 8414, many servers support both endpoints with identical metadata
255+
from mcp.server.auth.routes import cors_middleware
256+
enhanced_routes.append(
257+
Route(
258+
path="/.well-known/openid-configuration",
259+
endpoint=cors_middleware(
260+
enhanced_metadata_handler, ["GET", "OPTIONS"]
261+
),
262+
methods=["GET", "OPTIONS"],
263+
)
264+
)
265+
251266
# Add consent form routes (these override the default authorize behavior)
252267
consent_routes = [
253268
Route("/consent", endpoint=self._consent_get, methods=["GET"]),
@@ -263,6 +278,17 @@ async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
263278

264279
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
265280
"""Register a new OAuth client."""
281+
# Set default scopes if client doesn't specify any (ChatGPT compatibility)
282+
# ChatGPT registers without scopes, then requests them during authorization
283+
if (
284+
client_info.scope is None
285+
and self.client_registration_options is not None
286+
and self.client_registration_options.valid_scopes is not None
287+
):
288+
# Grant all valid scopes by default
289+
client_info.scope = " ".join(self.client_registration_options.valid_scopes)
290+
logger.info(f"Client registered without scopes, granting all valid scopes: {client_info.scope}")
291+
266292
# Validate scopes if configured
267293
if (
268294
client_info.scope is not None

tests/src/unit/test_oauth.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,25 @@ async def test_register_client_validates_scopes(self, provider):
152152
with pytest.raises(ValueError, match="not valid"):
153153
await provider.register_client(client_info)
154154

155+
@pytest.mark.asyncio
156+
async def test_register_client_without_scopes_gets_defaults(self, provider):
157+
"""Test client registration without scopes gets all valid scopes (ChatGPT compat)."""
158+
from mcp.shared.auth import OAuthClientInformationFull
159+
160+
# ChatGPT registers without specifying scopes
161+
client_info = OAuthClientInformationFull(
162+
client_id="chatgpt-client",
163+
redirect_uris=["https://chatgpt.com/callback"],
164+
scope=None, # No scopes specified
165+
)
166+
167+
await provider.register_client(client_info)
168+
169+
# Should have been granted all valid scopes
170+
stored = await provider.get_client("chatgpt-client")
171+
assert stored is not None
172+
assert stored.scope == "homeassistant mcp"
173+
155174
@pytest.mark.asyncio
156175
async def test_get_client_not_found(self, provider):
157176
"""Test getting non-existent client returns None."""
@@ -508,6 +527,21 @@ async def test_enhanced_metadata_handler(self, provider):
508527

509528
# Note: Full handler testing requires ASGI app context, which is tested in E2E tests
510529

530+
@pytest.mark.asyncio
531+
async def test_openid_configuration_endpoint(self, provider):
532+
"""Test OpenID Configuration endpoint exists for ChatGPT compatibility."""
533+
routes = provider.get_routes()
534+
openid_route = next(
535+
(r for r in routes if r.path == "/.well-known/openid-configuration"),
536+
None
537+
)
538+
539+
# Verify the route exists (required by ChatGPT MCP connector)
540+
assert openid_route is not None
541+
assert openid_route.path == "/.well-known/openid-configuration"
542+
543+
# Note: Should return same metadata as oauth-authorization-server for compatibility
544+
511545
@pytest.mark.asyncio
512546
async def test_consent_get_success(self, provider, mock_request):
513547
"""Test consent form GET with valid transaction."""

0 commit comments

Comments
 (0)