diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py
index af570af0af..962a76daaf 100644
--- a/backend/open_webui/main.py
+++ b/backend/open_webui/main.py
@@ -2503,7 +2503,9 @@ async def register_client(request, client_id: str) -> bool:
server_url = connection.get('url')
auth_type = connection.get('auth_type', 'none')
- oauth_server_key = (connection.get('config') or {}).get('oauth_server_key')
+ config = connection.get('config') or {}
+ oauth_server_key = config.get('oauth_server_key')
+ oauth_server_url = config.get('oauth_server_url')
try:
if auth_type == 'oauth_2.1_static':
@@ -2526,6 +2528,7 @@ async def register_client(request, client_id: str) -> bool:
server_url,
oauth_client_id=oauth_client_id,
oauth_client_secret=oauth_client_secret,
+ authorization_server_url=oauth_server_url,
)
else:
oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration(
@@ -2533,6 +2536,7 @@ async def register_client(request, client_id: str) -> bool:
client_id,
server_url,
oauth_server_key,
+ authorization_server_url=oauth_server_url,
)
except Exception as e:
log.error(f'OAuth client re-registration failed for {client_id}: {e}')
diff --git a/backend/open_webui/routers/configs.py b/backend/open_webui/routers/configs.py
index 02b16d8e5b..7fa8136aae 100644
--- a/backend/open_webui/routers/configs.py
+++ b/backend/open_webui/routers/configs.py
@@ -102,6 +102,7 @@ class OAuthClientRegistrationForm(BaseModel):
client_id: str
client_name: Optional[str] = None
client_secret: Optional[str] = None
+ oauth_server_url: Optional[str] = None
@router.post('/oauth/clients/register')
@@ -124,10 +125,11 @@ async def register_oauth_client(
form_data.url,
oauth_client_id=form_data.client_id,
oauth_client_secret=form_data.client_secret,
+ authorization_server_url=form_data.oauth_server_url,
)
else:
oauth_client_info = await get_oauth_client_info_with_dynamic_client_registration(
- request, oauth_client_id, form_data.url
+ request, oauth_client_id, form_data.url, authorization_server_url=form_data.oauth_server_url
)
return {
'status': True,
@@ -368,7 +370,8 @@ async def verify_tool_servers_config(request: Request, form_data: ToolServerConn
try:
if form_data.type == 'mcp':
if form_data.auth_type in ('oauth_2.1', 'oauth_2.1_static'):
- discovery_urls = await get_discovery_urls(form_data.url)
+ oauth_server_url = (form_data.config or {}).get('oauth_server_url')
+ discovery_urls = await get_discovery_urls(form_data.url, oauth_server_url)
for discovery_url in discovery_urls:
log.debug(f'Trying to fetch OAuth 2.1 discovery document from {discovery_url}')
async with aiohttp.ClientSession(
diff --git a/backend/open_webui/test/utils/test_oauth.py b/backend/open_webui/test/utils/test_oauth.py
new file mode 100644
index 0000000000..53030cc9b2
--- /dev/null
+++ b/backend/open_webui/test/utils/test_oauth.py
@@ -0,0 +1,47 @@
+import pytest
+from open_webui.utils.oauth import get_discovery_urls
+
+
+@pytest.mark.asyncio
+async def test_get_discovery_urls_prefers_authorization_server_url():
+ urls = await get_discovery_urls(
+ 'https://gmailmcp.googleapis.com/mcp/v1',
+ 'https://accounts.google.com',
+ )
+
+ assert urls == [
+ 'https://accounts.google.com/.well-known/oauth-authorization-server',
+ 'https://accounts.google.com/.well-known/openid-configuration',
+ ]
+
+
+@pytest.mark.asyncio
+async def test_get_discovery_urls_accepts_explicit_discovery_url():
+ urls = await get_discovery_urls(
+ 'https://gmailmcp.googleapis.com/mcp/v1',
+ 'https://accounts.google.com/.well-known/openid-configuration',
+ )
+
+ assert urls == ['https://accounts.google.com/.well-known/openid-configuration']
+
+
+@pytest.mark.asyncio
+async def test_get_discovery_urls_preserves_current_fallback_without_override(monkeypatch):
+ async def mock_authorization_server_discovery(server_url: str) -> list[str]:
+ assert server_url == 'https://gmailmcp.googleapis.com/mcp/v1'
+ return []
+
+ monkeypatch.setattr(
+ 'open_webui.utils.oauth.get_authorization_server_discovery_urls',
+ mock_authorization_server_discovery,
+ )
+
+ urls = await get_discovery_urls('https://gmailmcp.googleapis.com/mcp/v1')
+
+ assert urls == [
+ 'https://gmailmcp.googleapis.com/.well-known/oauth-authorization-server/mcp/v1',
+ 'https://gmailmcp.googleapis.com/.well-known/openid-configuration/mcp/v1',
+ 'https://gmailmcp.googleapis.com/mcp/v1/.well-known/openid-configuration',
+ 'https://gmailmcp.googleapis.com/.well-known/oauth-authorization-server',
+ 'https://gmailmcp.googleapis.com/.well-known/openid-configuration',
+ ]
diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py
index 4a7d79d87c..15acf916a1 100644
--- a/backend/open_webui/utils/oauth.py
+++ b/backend/open_webui/utils/oauth.py
@@ -356,10 +356,15 @@ async def get_authorization_server_discovery_urls(server_url: str) -> list[str]:
def _build_well_known_urls(server_url: str) -> list[str]:
"""Build RFC 8414 / OIDC Discovery well-known URLs for a server URL."""
parsed, base_url = get_parsed_and_base_url(server_url)
+ path = parsed.path.rstrip('/')
+ if path.startswith('/.well-known/oauth-authorization-server') or path.startswith(
+ '/.well-known/openid-configuration'
+ ):
+ return [server_url.rstrip('/')]
+
urls = []
if parsed.path and parsed.path != '/':
- path = parsed.path.rstrip('/')
urls.extend(
[
urllib.parse.urljoin(base_url, f'/.well-known/oauth-authorization-server{path}'),
@@ -378,7 +383,10 @@ def _build_well_known_urls(server_url: str) -> list[str]:
return urls
-async def get_discovery_urls(server_url) -> list[str]:
+async def get_discovery_urls(server_url: str, authorization_server_url: str | None = None) -> list[str]:
+ if authorization_server_url:
+ return _build_well_known_urls(authorization_server_url)
+
urls = await get_authorization_server_discovery_urls(server_url)
urls.extend(_build_well_known_urls(server_url))
return urls
@@ -391,6 +399,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
client_id: str,
oauth_server_url: str,
oauth_server_key: Optional[str] = None,
+ authorization_server_url: Optional[str] = None,
) -> OAuthClientInformationFull:
try:
oauth_server_metadata = None
@@ -406,7 +415,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
)
# Attempt to fetch OAuth server metadata to get registration endpoint & scopes
- discovery_urls = await get_discovery_urls(oauth_server_url)
+ discovery_urls = await get_discovery_urls(oauth_server_url, authorization_server_url)
for url in discovery_urls:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as oauth_server_metadata_response:
@@ -441,7 +450,7 @@ async def get_oauth_client_info_with_dynamic_client_registration(
if oauth_server_metadata and oauth_server_metadata.registration_endpoint:
registration_url = str(oauth_server_metadata.registration_endpoint)
else:
- _, base_url = get_parsed_and_base_url(oauth_server_url)
+ _, base_url = get_parsed_and_base_url(authorization_server_url or oauth_server_url)
registration_url = urllib.parse.urljoin(base_url, '/register')
registration_data = oauth_client_metadata.model_dump(
@@ -502,6 +511,7 @@ async def get_oauth_client_info_with_static_credentials(
oauth_server_url: str,
oauth_client_id: str,
oauth_client_secret: str,
+ authorization_server_url: Optional[str] = None,
) -> OAuthClientInformationFull:
"""
Build an OAuthClientInformationFull from user-provided static credentials.
@@ -516,7 +526,7 @@ async def get_oauth_client_info_with_static_credentials(
redirect_uri = f'{redirect_base_url}/oauth/clients/{client_id}/callback'
# Discover server metadata (authorization endpoint, token endpoint, scopes, etc.)
- discovery_urls = await get_discovery_urls(oauth_server_url)
+ discovery_urls = await get_discovery_urls(oauth_server_url, authorization_server_url)
for url in discovery_urls:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(url, ssl=AIOHTTP_CLIENT_SESSION_SSL) as resp:
diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts
index 6b7bf6f47b..7859a4e787 100644
--- a/src/lib/apis/configs/index.ts
+++ b/src/lib/apis/configs/index.ts
@@ -378,6 +378,7 @@ type RegisterOAuthClientForm = {
client_id: string;
client_name?: string;
client_secret?: string;
+ oauth_server_url?: string;
};
export const registerOAuthClient = async (
diff --git a/src/lib/components/AddToolServerModal.svelte b/src/lib/components/AddToolServerModal.svelte
index 74571c3086..df0561ca4e 100644
--- a/src/lib/components/AddToolServerModal.svelte
+++ b/src/lib/components/AddToolServerModal.svelte
@@ -60,6 +60,7 @@
let oauthClientId = '';
let oauthClientSecret = '';
+ let oauthServerUrl = '';
let enable = true;
let loading = false;
@@ -67,6 +68,13 @@
let showAccessControlModal = false;
let showDeleteConfirmDialog = false;
+ const getOAuthServerUrlConfig = () => {
+ const trimmedOAuthServerUrl = oauthServerUrl.trim();
+ return auth_type === 'oauth_2.1_static' && trimmedOAuthServerUrl
+ ? { oauth_server_url: trimmedOAuthServerUrl }
+ : {};
+ };
+
const registerOAuthClientHandler = async () => {
if (url === '') {
toast.error($i18n.t('Please enter a valid URL'));
@@ -86,10 +94,16 @@
// client_id is the tool server ID (used as the internal lookup key for both flows).
// For static, client_secret signals the backend to use the static credential path.
// The actual OAuth client_id/secret come from the connection info at save time.
- const formData: { url: string; client_id: string; client_secret?: string } = {
+ const formData: {
+ url: string;
+ client_id: string;
+ client_secret?: string;
+ oauth_server_url?: string;
+ } = {
url: url,
client_id: id,
- ...(auth_type === 'oauth_2.1_static' ? { client_secret: oauthClientSecret } : {})
+ ...(auth_type === 'oauth_2.1_static' ? { client_secret: oauthClientSecret } : {}),
+ ...getOAuthServerUrlConfig()
};
const res = await registerOAuthClient(localStorage.token, formData, 'mcp').catch((err) => {
@@ -164,7 +178,8 @@
key,
config: {
enable: enable,
- access_grants: accessGrants
+ access_grants: accessGrants,
+ ...getOAuthServerUrlConfig()
},
info: {
id,
@@ -222,6 +237,7 @@
if (data.config) {
enable = data.config.enable ?? true;
accessGrants = data.config.access_grants ?? [];
+ oauthServerUrl = data.config.oauth_server_url ?? '';
}
toast.success($i18n.t('Import successful'));
@@ -246,6 +262,9 @@
auth_type,
headers: headers ? JSON.parse(headers) : undefined,
key,
+ ...(Object.keys(getOAuthServerUrlConfig()).length
+ ? { config: getOAuthServerUrlConfig() }
+ : {}),
info: {
id: id,
@@ -328,7 +347,8 @@
config: {
enable: enable,
function_name_filter_list: functionNameFilterList,
- access_grants: accessGrants
+ access_grants: accessGrants,
+ ...getOAuthServerUrlConfig()
},
info: {
id: id,
@@ -364,6 +384,7 @@
oauthClientInfo = null;
oauthClientId = '';
oauthClientSecret = '';
+ oauthServerUrl = '';
enable = true;
functionNameFilterList = '';
@@ -390,6 +411,7 @@
oauthClientInfo = connection.info?.oauth_client_info ?? null;
oauthClientId = connection.info?.oauth_client_id ?? '';
oauthClientSecret = connection.info?.oauth_client_secret ?? '';
+ oauthServerUrl = connection.config?.oauth_server_url ?? '';
enable = connection.config?.enable ?? true;
functionNameFilterList = connection.config?.function_name_filter_list ?? '';
@@ -730,6 +752,17 @@
placeholder={$i18n.t('Client Secret')}
required={false}
/>
+
+
{/if}