mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-06 02:48:04 +00:00
feat: add MCP_OAUTH_ALLOWED_SCOPES to restrict OAuth scopes requested from MCP servers
When connecting to an MCP server, Open WebUI requests every OAuth scope the server advertises (via Authorization Server metadata during dynamic client registration, or via RFC 9728 Protected Resource Metadata with static credentials). Some servers advertise scope catalogs that are broader than what the connection needs, or even mutually exclusive: Google's Gmail MCP advertises gmail.metadata alongside the full-access https://mail.google.com/ scope, and requesting both can break operations (metadata-scoped tokens reject FULL_CONTENT reads) and over-grants access. Add an optional MCP_OAUTH_ALLOWED_SCOPES environment variable (space- or comma-separated allowlist). When set, advertised scopes not in the list are not requested; when unset/empty, behavior is unchanged. Scope strings are matched exactly, and a scope filtered down to an empty list falls back to requesting no explicit scope (scope=None) rather than an empty string. This is a deployment-level least-privilege knob; per-connection scope selection in the connection UI would be a natural follow-up.
This commit is contained in:
parent
b1d40f3409
commit
9870cb5da8
2 changed files with 37 additions and 2 deletions
|
|
@ -554,6 +554,15 @@ try:
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
MCP_INITIALIZE_TIMEOUT = 10
|
MCP_INITIALIZE_TIMEOUT = 10
|
||||||
|
|
||||||
|
# Optional allowlist restricting which OAuth scopes are requested when
|
||||||
|
# connecting to an MCP server. Space- or comma-separated list of scope
|
||||||
|
# strings; scopes advertised by a server that are not in the list are not
|
||||||
|
# requested. Empty/unset keeps the default behavior (request everything
|
||||||
|
# the server advertises). Useful for servers that advertise broad or
|
||||||
|
# mutually-exclusive scopes, e.g. Google's Gmail MCP advertises both
|
||||||
|
# gmail.metadata and the full-access mail.google.com scope.
|
||||||
|
MCP_OAUTH_ALLOWED_SCOPES = os.getenv('MCP_OAUTH_ALLOWED_SCOPES', '').replace(',', ' ').split()
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# AIOHTTP Connection Pool
|
# AIOHTTP Connection Pool
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ from open_webui.env import (
|
||||||
AIOHTTP_CLIENT_SESSION_SSL,
|
AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
ENABLE_OAUTH_EMAIL_FALLBACK,
|
ENABLE_OAUTH_EMAIL_FALLBACK,
|
||||||
ENABLE_OAUTH_ID_TOKEN_COOKIE,
|
ENABLE_OAUTH_ID_TOKEN_COOKIE,
|
||||||
|
MCP_OAUTH_ALLOWED_SCOPES,
|
||||||
OAUTH_CLIENT_INFO_ENCRYPTION_KEY,
|
OAUTH_CLIENT_INFO_ENCRYPTION_KEY,
|
||||||
OAUTH_MAX_SESSIONS_PER_USER,
|
OAUTH_MAX_SESSIONS_PER_USER,
|
||||||
REDIS_KEY_PREFIX,
|
REDIS_KEY_PREFIX,
|
||||||
|
|
@ -287,6 +288,28 @@ def get_parsed_and_base_url(server_url) -> tuple[urllib.parse.ParseResult, str]:
|
||||||
return parsed, base_url
|
return parsed, base_url
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_scopes(scopes: list[str]) -> list[str]:
|
||||||
|
"""
|
||||||
|
Optionally restrict discovered OAuth scopes to an admin-defined allowlist.
|
||||||
|
|
||||||
|
Set MCP_OAUTH_ALLOWED_SCOPES (space- or comma-separated) to limit which of
|
||||||
|
a server's advertised scopes are actually requested. Some servers advertise
|
||||||
|
broad or mutually-exclusive scopes — e.g. Google's Gmail MCP advertises
|
||||||
|
both gmail.metadata and the full-access mail.google.com scope — and
|
||||||
|
requesting all of them can break operations or over-grant access.
|
||||||
|
|
||||||
|
Scopes are matched exactly; any advertised scope not in the allowlist is
|
||||||
|
dropped. If the env var is unset/empty, all discovered scopes are kept
|
||||||
|
(default behavior).
|
||||||
|
"""
|
||||||
|
if not MCP_OAUTH_ALLOWED_SCOPES:
|
||||||
|
return scopes
|
||||||
|
allowed = set(MCP_OAUTH_ALLOWED_SCOPES)
|
||||||
|
filtered = [s for s in scopes if s in allowed]
|
||||||
|
log.debug(f'Scope allowlist active: advertised {scopes} -> requesting {filtered}')
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ProtectedResourceMetadata:
|
class ProtectedResourceMetadata:
|
||||||
"""RFC 9728 Protected Resource Metadata fields relevant to OAuth flows."""
|
"""RFC 9728 Protected Resource Metadata fields relevant to OAuth flows."""
|
||||||
|
|
@ -449,7 +472,9 @@ async def get_oauth_client_info_with_dynamic_client_registration(
|
||||||
oauth_client_metadata.scope is None
|
oauth_client_metadata.scope is None
|
||||||
and oauth_server_metadata.scopes_supported is not None
|
and oauth_server_metadata.scopes_supported is not None
|
||||||
):
|
):
|
||||||
oauth_client_metadata.scope = ' '.join(oauth_server_metadata.scopes_supported)
|
_scopes = _filter_scopes(oauth_server_metadata.scopes_supported)
|
||||||
|
if _scopes:
|
||||||
|
oauth_client_metadata.scope = ' '.join(_scopes)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
oauth_server_metadata.token_endpoint_auth_methods_supported
|
oauth_server_metadata.token_endpoint_auth_methods_supported
|
||||||
|
|
@ -565,7 +590,8 @@ async def get_oauth_client_info_with_static_credentials(
|
||||||
# Unlike the Authorization Server's scopes_supported (which is a full catalog
|
# Unlike the Authorization Server's scopes_supported (which is a full catalog
|
||||||
# of every scope the server can grant), the PRM scopes_supported represents
|
# of every scope the server can grant), the PRM scopes_supported represents
|
||||||
# what this specific resource requires — making it safe to request them all.
|
# 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
|
_scopes = _filter_scopes(resource_metadata.scopes_supported)
|
||||||
|
scope = ' '.join(_scopes) if _scopes else None
|
||||||
|
|
||||||
# Determine token_endpoint_auth_method
|
# Determine token_endpoint_auth_method
|
||||||
token_endpoint_auth_method = 'client_secret_post'
|
token_endpoint_auth_method = 'client_secret_post'
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue