"""Tests for OAuth proxy UI and error page rendering."""
from unittest.mock import Mock
from key_value.aio.stores.memory import MemoryStore
from starlette.requests import Request
from starlette.responses import HTMLResponse
from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.oauth_proxy.ui import create_consent_html, create_error_html
from fastmcp.server.auth.providers.jwt import JWTVerifier
class TestErrorPageRendering:
"""Test error page rendering for OAuth callback errors."""
def test_create_error_html_basic(self):
"""Test basic error page generation."""
html = create_error_html(
error_title="Test Error",
error_message="This is a test error message",
)
# Verify it's valid HTML
assert "" in html
assert "
Test Error" in html
assert "This is a test error message" in html
assert 'class="info-box error"' in html
def test_create_error_html_with_details(self):
"""Test error page with error details."""
html = create_error_html(
error_title="OAuth Error",
error_message="Authentication failed",
error_details={
"Error Code": "invalid_scope",
"Description": "Requested scope does not exist",
},
)
# Verify error details are included
assert "Error Details" in html
assert "Error Code" in html
assert "invalid_scope" in html
assert "Description" in html
assert "Requested scope does not exist" in html
def test_create_error_html_escapes_user_input(self):
"""Test that error page properly escapes HTML in user input."""
html = create_error_html(
error_title="Error ",
error_message="Message with HTML tags",
error_details={"Key" not in html
assert "<script>" in html
assert "HTML" not in html
assert "<b>HTML</b>" in html
async def test_callback_error_returns_html_page(self):
"""Test that OAuth callback errors return styled HTML instead of data: URLs."""
# Create a minimal OAuth proxy
provider = OAuthProxy(
upstream_authorization_endpoint="https://idp.example.com/authorize",
upstream_token_endpoint="https://idp.example.com/token",
upstream_client_id="test-client",
upstream_client_secret="test-secret",
token_verifier=JWTVerifier(
jwks_uri="https://idp.example.com/.well-known/jwks.json",
issuer="https://idp.example.com",
audience="test-client",
),
base_url="http://localhost:8000",
jwt_signing_key="test-signing-key",
client_storage=MemoryStore(),
)
# Mock a request with an error from the IdP
mock_request = Mock(spec=Request)
mock_request.query_params = {
"error": "invalid_scope",
"error_description": "The application asked for scope 'read' that doesn't exist",
"state": "test-state",
}
# Call the callback handler
response = await provider._handle_idp_callback(mock_request)
# Verify we get an HTMLResponse, not a RedirectResponse
assert isinstance(response, HTMLResponse)
assert response.status_code == 400
# Verify the response contains the error message
assert b"invalid_scope" in response.body
assert b"doesn't exist" in response.body # HTML-escaped apostrophe
assert b"OAuth Error" in response.body
class TestConsentPageRendering:
"""Test consent page rendering and escaping."""
def test_create_consent_html_escapes_client_id_in_details(self):
"""Test that Application ID is escaped in advanced details."""
html = create_consent_html(
client_id='evil
',
redirect_uri="https://example.com/callback",
scopes=["read"],
txn_id="txn",
csrf_token="csrf",
)
assert 'evil
' not in html
assert "evil<img src=x onerror=alert("xss")>" in html