mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-28 10:18:08 +02:00
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:
parent
ea63d06241
commit
7f2d034f4d
2 changed files with 99 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue