Forward IdP auth errors to MCP client instead of showing HTML error page (#4293)

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
This commit is contained in:
Bobby Davidson 2026-06-24 17:09:09 +01:00 committed by GitHub
commit 7f2d034f4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 99 additions and 14 deletions

View file

@ -1,5 +1,8 @@
"""Tests for OAuth proxy initialization and configuration."""
import time
from urllib.parse import parse_qs, urlparse
import httpx
import pytest
from authlib.integrations.httpx_client import AsyncOAuth2Client
@ -7,6 +10,7 @@ from key_value.aio.stores.memory import MemoryStore
from starlette.applications import Starlette
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.oauth_proxy.models import OAuthTransaction
class TestOAuthProxyInitialization:
@ -303,3 +307,68 @@ class TestOptionalClientSecret:
signed = proxy._sign_cookie("test-payload")
assert proxy._verify_cookie(signed) == "test-payload"
assert proxy._verify_cookie("tampered.payload") is None
class TestIdpCallbackErrorForwarding:
"""Tests for error forwarding in the IdP callback."""
async def test_error_with_valid_transaction_redirects_to_client(self, oauth_proxy):
"""When the IdP returns an error and the transaction exists, the proxy
must forward the error to the client's redirect_uri rather than showing
an HTML error page."""
txn_id = "test-txn-123"
client_redirect_uri = "http://localhost:12345/callback"
client_state = "client-state-abc"
transaction = OAuthTransaction(
txn_id=txn_id,
client_id="test-client",
client_redirect_uri=client_redirect_uri,
client_state=client_state,
code_challenge=None,
code_challenge_method="S256",
scopes=["read"],
created_at=time.time(),
)
await oauth_proxy._transaction_store.put(key=txn_id, value=transaction)
app = Starlette(routes=oauth_proxy.get_routes())
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(
transport=transport,
base_url="https://myserver.com",
follow_redirects=False,
) as client:
response = await client.get(
f"/auth/callback?error=access_denied&error_description=User+denied+access&state={txn_id}"
)
assert response.status_code == 302
location = response.headers["location"]
parsed = urlparse(location)
assert (
parsed.scheme + "://" + parsed.netloc + parsed.path == client_redirect_uri
)
params = parse_qs(parsed.query)
assert params["error"] == ["access_denied"]
assert params["error_description"] == ["User denied access"]
assert params["state"] == [client_state]
async def test_error_with_missing_transaction_returns_html_error(self, oauth_proxy):
"""When the IdP returns an error but the transaction is missing or
expired, the proxy must return a local HTML error page there is no
trusted client redirect_uri to forward to."""
app = Starlette(routes=oauth_proxy.get_routes())
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(
transport=transport,
base_url="https://myserver.com",
follow_redirects=False,
) as client:
response = await client.get(
"/auth/callback?error=access_denied&state=nonexistent-txn"
)
assert response.status_code == 400