From f7ce4024d67efd5588293305873a6890108e849a Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 24 Sep 2026 05:52:29 +0200 Subject: [PATCH] fix: keep requested MCP OAuth scope when DCR response omits it (#30384) MCP tool servers using OAuth 2.1 with dynamic client registration authorized without any scope when the authorization server left scope out of its registration response, which RFC 7591 allows (Atlassian and Notion do). Consent completed and the tool showed as connected, but the issued token lacked the scopes the resource requires, so every tool call was refused. Discovered scopes and the custom OAuth Scopes field were both affected. The stored client now falls back to the scope sent in the registration request when the response has none. A scope the server does return is kept as is. Connections registered before this fix already have a null scope stored. The protected resource metadata recovery that static-credential clients already use now also runs for dynamically registered clients, so those connections pick up the discovered scopes on the next load without registering again. Fixes #29967 --- backend/open_webui/main.py | 6 ++---- backend/open_webui/routers/configs.py | 4 ++-- backend/open_webui/utils/oauth.py | 13 ++++++------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index e7c2915130..3e0f9db89f 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -264,7 +264,7 @@ from open_webui.utils.oauth import ( encrypt_data, get_oauth_client_info_with_dynamic_client_registration, get_oauth_client_info_with_static_credentials, - recover_static_oauth_client_metadata, + recover_oauth_client_metadata, resolve_oauth_client_info, ) from open_webui.utils.plugin import install_tool_and_function_dependencies @@ -636,9 +636,7 @@ async def initialize_runtime_config(app: FastAPI): if server_id and auth_type in ('oauth_2.1', 'oauth_2.1_static'): try: oauth_client_info = resolve_oauth_client_info(tool_server_connection) - oauth_client_info = await recover_static_oauth_client_metadata( - tool_server_connection, oauth_client_info - ) + oauth_client_info = await recover_oauth_client_metadata(tool_server_connection, oauth_client_info) oauth_client_info = apply_connection_oauth_options(tool_server_connection, oauth_client_info) app.state.oauth_client_manager.add_client( f'mcp:{server_id}', diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py index c9531702e7..35f1b23cf4 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -22,7 +22,7 @@ from open_webui.utils.oauth import ( get_discovery_urls, get_oauth_client_info_with_dynamic_client_registration, get_oauth_client_info_with_static_credentials, - recover_static_oauth_client_metadata, + recover_oauth_client_metadata, resolve_oauth_client_info, ) from open_webui.utils.tools import ( @@ -273,7 +273,7 @@ async def set_tool_servers_config( if auth_type in ('oauth_2.1', 'oauth_2.1_static') and server_id: try: oauth_client_info = resolve_oauth_client_info(connection) - oauth_client_info = await recover_static_oauth_client_metadata(connection, oauth_client_info) + oauth_client_info = await recover_oauth_client_metadata(connection, oauth_client_info) oauth_client_info = apply_connection_oauth_options(connection, oauth_client_info) request.app.state.oauth_client_manager.add_client( f'{server_type}:{server_id}', diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index d523aad0ba..25c6a86a99 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -612,6 +612,8 @@ async def get_oauth_client_info_with_dynamic_client_registration( oauth_client_info = OAuthClientInformationFull.model_validate( { **registration_response_json, + # RFC 7591: the server may omit scope; keep the requested one. + 'scope': registration_response_json.get('scope') or oauth_client_metadata.scope, 'issuer': oauth_server_metadata_url, 'server_metadata': oauth_server_metadata, 'resource': resource, @@ -799,10 +801,7 @@ def build_oauth_request_params(client_info: OAuthClientInformationFull | None) - return params -async def recover_static_oauth_client_metadata(connection: dict, oauth_client_info: dict) -> dict: - if connection.get('auth_type') != 'oauth_2.1_static': - return oauth_client_info - +async def recover_oauth_client_metadata(connection: dict, oauth_client_info: dict) -> dict: if oauth_client_info.get('scope') and oauth_client_info.get('resource'): return oauth_client_info @@ -813,13 +812,13 @@ async def recover_static_oauth_client_metadata(connection: dict, oauth_client_in try: resource_metadata = await get_protected_resource_metadata(server_url) except Exception as e: - log.debug('Unable to recover static OAuth metadata for %s: %s', server_url, e) + log.debug('Unable to recover OAuth metadata for %s: %s', server_url, e) return oauth_client_info recovered = {**oauth_client_info} if not recovered.get('scope') and resource_metadata.scopes_supported: recovered['scope'] = ' '.join(resource_metadata.scopes_supported) - log.info('Recovered static OAuth scopes for %s from protected resource metadata', server_url) + log.info('Recovered OAuth scopes for %s from protected resource metadata', server_url) if not recovered.get('resource') and resource_metadata.resource: recovered['resource'] = resource_metadata.resource @@ -916,7 +915,7 @@ class OAuthClientManager: try: oauth_client_info = resolve_oauth_client_info(connection) - oauth_client_info = await recover_static_oauth_client_metadata(connection, oauth_client_info) + oauth_client_info = await recover_oauth_client_metadata(connection, oauth_client_info) oauth_client_info = apply_connection_oauth_options(connection, oauth_client_info) return self.add_client(expected_client_id, OAuthClientInformationFull(**oauth_client_info))['client'] except InvalidToken: