This commit is contained in:
Timothy Jaeryang Baek 2026-06-29 02:57:58 -05:00
parent 534206095f
commit 7be009649a
5 changed files with 60 additions and 6 deletions

View file

@ -2265,6 +2265,7 @@ async def register_client(request, client_id: str) -> bool:
server_url = connection.get('url')
auth_type = connection.get('auth_type', 'none')
oauth_scope = (connection.get('info') or {}).get('oauth_scope') or (connection.get('config') or {}).get('oauth_scope')
oauth_server_key = (connection.get('config') or {}).get('oauth_server_key')
try:
@ -2288,6 +2289,7 @@ async def register_client(request, client_id: str) -> bool:
server_url,
oauth_client_id=oauth_client_id,
oauth_client_secret=oauth_client_secret,
oauth_scope=oauth_scope,
)
else:
oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration(
@ -2295,6 +2297,7 @@ async def register_client(request, client_id: str) -> bool:
client_id,
server_url,
oauth_server_key,
oauth_scope=oauth_scope,
)
except Exception as e:
log.error(f'OAuth client re-registration failed for {client_id}: {e}')

View file

@ -155,6 +155,7 @@ class OAuthClientRegistrationForm(BaseModel):
client_name: str | None = None
client_secret: str | None = None
oauth_server_url: str | None = None
oauth_scope: str | None = None
@router.post('/oauth/clients/register')
@ -179,10 +180,11 @@ async def register_oauth_client(
oauth_server_url,
oauth_client_id=form_data.client_id,
oauth_client_secret=form_data.client_secret,
oauth_scope=form_data.oauth_scope,
)
else:
oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration(
request, oauth_client_id, oauth_server_url
request, oauth_client_id, oauth_server_url, oauth_scope=form_data.oauth_scope
)
return {
'status': True,

View file

@ -465,6 +465,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
client_id: str,
oauth_server_url: str,
oauth_server_key: Optional[str] = None,
oauth_scope: str | None = None,
) -> OAuthClientInformationFull:
try:
oauth_server_metadata = None
@ -487,7 +488,10 @@ async def get_oauth_client_info_with_dynamic_client_registration(
# Prefer the resource-specific scopes from the Protected Resource Metadata
# (RFC 9728) over the AS's full scopes_supported catalog, for least
# privilege. Mirrors the static-credentials flow (#24690).
if resource_metadata.scopes_supported:
scope_override = ' '.join(oauth_scope.replace(',', ' ').split()) if oauth_scope else None
if scope_override:
oauth_client_metadata.scope = scope_override
elif resource_metadata.scopes_supported:
oauth_client_metadata.scope = ' '.join(resource_metadata.scopes_supported)
discovery_urls = resource_metadata.get_discovery_urls(oauth_server_url)
@ -587,6 +591,7 @@ async def get_oauth_client_info_with_static_credentials(
oauth_server_url: str,
oauth_client_id: str,
oauth_client_secret: str,
oauth_scope: str | None = None,
) -> OAuthClientInformationFull:
"""
Build an OAuthClientInformationFull from user-provided static credentials.
@ -621,7 +626,9 @@ async def get_oauth_client_info_with_static_credentials(
# Unlike the Authorization Server's scopes_supported (which is a full catalog
# of every scope the server can grant), the PRM scopes_supported represents
# what this specific resource requires — making it safe to request them all.
scope = ' '.join(resource_metadata.scopes_supported) if resource_metadata.scopes_supported else None
scope = (' '.join(oauth_scope.replace(',', ' ').split()) if oauth_scope else None) or (
' '.join(resource_metadata.scopes_supported) if resource_metadata.scopes_supported else None
)
# Determine token_endpoint_auth_method
token_endpoint_auth_method = 'client_secret_post'
@ -687,10 +694,18 @@ def get_connection_oauth_resource_parameter(connection: dict) -> OAuthResourcePa
def apply_connection_oauth_options(connection: dict, oauth_client_info: dict) -> dict:
return {
info = connection.get('info') or {}
config = connection.get('config') or {}
oauth_scope = info.get('oauth_scope') or config.get('oauth_scope')
oauth_scope = ' '.join(oauth_scope.replace(',', ' ').split()) if oauth_scope else None
options = {
**oauth_client_info,
'oauth_resource_parameter': get_connection_oauth_resource_parameter(connection),
}
if oauth_scope:
options['scope'] = oauth_scope
return options
def scope_has_resource_indicator(scope: str | None) -> bool:

View file

@ -466,6 +466,7 @@ type RegisterOAuthClientForm = {
client_name?: string;
client_secret?: string;
oauth_server_url?: string;
oauth_scope?: string;
};
export const registerOAuthClient = async (

View file

@ -61,6 +61,7 @@
let oauthClientId = '';
let oauthClientSecret = '';
let oauthServerUrl = '';
let oauthScope = '';
let oauthResourceParameter = 'auto';
let enable = true;
@ -93,9 +94,11 @@
client_id: string;
client_secret?: string;
oauth_server_url?: string;
oauth_scope?: string;
} = {
url: url,
client_id: id,
...(oauthScope ? { oauth_scope: oauthScope } : {}),
...(auth_type === 'oauth_2.1_static'
? { client_secret: oauthClientSecret, oauth_server_url: oauthServerUrl }
: {})
@ -226,6 +229,7 @@
id = data.info.id ?? '';
name = data.info.name ?? '';
description = data.info.description ?? '';
oauthScope = data.info.oauth_scope ?? '';
oauthResourceParameter = data.info.oauth_resource_parameter ?? 'auto';
}
@ -262,7 +266,10 @@
name: name,
description: description,
...(type === 'mcp' && ['oauth_2.1', 'oauth_2.1_static'].includes(auth_type)
? { oauth_resource_parameter: oauthResourceParameter }
? {
...(oauthScope ? { oauth_scope: oauthScope } : {}),
oauth_resource_parameter: oauthResourceParameter
}
: {})
}
}
@ -348,7 +355,10 @@
name: name,
description: description,
...(type === 'mcp' && ['oauth_2.1', 'oauth_2.1_static'].includes(auth_type)
? { oauth_resource_parameter: oauthResourceParameter }
? {
...(oauthScope ? { oauth_scope: oauthScope } : {}),
oauth_resource_parameter: oauthResourceParameter
}
: {}),
...(oauthClientInfo ? { oauth_client_info: oauthClientInfo } : {}),
...(auth_type === 'oauth_2.1_static'
@ -385,6 +395,7 @@
oauthClientId = '';
oauthClientSecret = '';
oauthServerUrl = '';
oauthScope = '';
oauthResourceParameter = 'auto';
enable = true;
@ -413,6 +424,7 @@
oauthClientId = connection.info?.oauth_client_id ?? '';
oauthClientSecret = connection.info?.oauth_client_secret ?? '';
oauthServerUrl = connection.info?.oauth_server_url ?? '';
oauthScope = connection.info?.oauth_scope ?? '';
oauthResourceParameter = connection.info?.oauth_resource_parameter ?? 'auto';
enable = connection.config?.enable ?? true;
@ -885,6 +897,27 @@
{/if}
{#if type === 'mcp' && ['oauth_2.1', 'oauth_2.1_static'].includes(auth_type)}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label
for="oauth-scope"
class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>{$i18n.t('OAuth Scopes')}</label
>
<div class="flex flex-1 items-center">
<input
id="oauth-scope"
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
type="text"
bind:value={oauthScope}
placeholder={$i18n.t('Use discovered scopes')}
autocomplete="off"
/>
</div>
</div>
</div>
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<label