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
This commit is contained in:
Classic298
2026-09-23 23:52:29 -04:00
committed by GitHub
parent 7924ce7d79
commit f7ce4024d6
3 changed files with 10 additions and 13 deletions
+2 -4
View File
@@ -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}',
+2 -2
View File
@@ -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}',
+6 -7
View File
@@ -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: