Jeremiah Lowin commited on
Commit
54ec17c
·
unverified ·
1 Parent(s): a4ec518

Add remote auth provider tests (#1351)

Browse files
tests/server/auth/test_remote_auth_provider.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ import pytest
3
+ from mcp.server.auth.provider import AccessToken
4
+ from pydantic import AnyHttpUrl
5
+
6
+ from fastmcp import FastMCP
7
+ from fastmcp.server.auth.auth import RemoteAuthProvider, TokenVerifier
8
+
9
+
10
+ class SimpleTokenVerifier(TokenVerifier):
11
+ """Simple token verifier for testing."""
12
+
13
+ def __init__(self, valid_tokens: dict[str, AccessToken] | None = None):
14
+ super().__init__()
15
+ self.valid_tokens = valid_tokens or {}
16
+
17
+ async def verify_token(self, token: str) -> AccessToken | None:
18
+ return self.valid_tokens.get(token)
19
+
20
+
21
+ class TestRemoteAuthProvider:
22
+ """Test suite for RemoteAuthProvider."""
23
+
24
+ def test_init(self):
25
+ """Test RemoteAuthProvider initialization."""
26
+ token_verifier = SimpleTokenVerifier()
27
+ auth_servers = [AnyHttpUrl("https://auth.example.com")]
28
+
29
+ provider = RemoteAuthProvider(
30
+ token_verifier=token_verifier,
31
+ authorization_servers=auth_servers,
32
+ resource_server_url="https://api.example.com",
33
+ )
34
+
35
+ assert provider.token_verifier is token_verifier
36
+ assert provider.authorization_servers == auth_servers
37
+ assert provider.resource_server_url == AnyHttpUrl("https://api.example.com")
38
+
39
+ async def test_verify_token_delegates_to_verifier(self):
40
+ """Test that verify_token delegates to the token verifier."""
41
+ access_token = AccessToken(
42
+ token="valid_token", client_id="test-client", scopes=[]
43
+ )
44
+ token_verifier = SimpleTokenVerifier({"valid_token": access_token})
45
+
46
+ provider = RemoteAuthProvider(
47
+ token_verifier=token_verifier,
48
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
49
+ resource_server_url="https://api.example.com",
50
+ )
51
+
52
+ # Valid token
53
+ result = await provider.verify_token("valid_token")
54
+ assert result is access_token
55
+
56
+ # Invalid token
57
+ result = await provider.verify_token("invalid_token")
58
+ assert result is None
59
+
60
+ def test_get_routes_creates_protected_resource_routes(self):
61
+ """Test that get_routes creates protected resource routes."""
62
+ token_verifier = SimpleTokenVerifier()
63
+ auth_servers = [AnyHttpUrl("https://auth.example.com")]
64
+
65
+ provider = RemoteAuthProvider(
66
+ token_verifier=token_verifier,
67
+ authorization_servers=auth_servers,
68
+ resource_server_url="https://api.example.com",
69
+ )
70
+
71
+ routes = provider.get_routes()
72
+ assert len(routes) == 1
73
+
74
+ # Check that the route is the OAuth protected resource metadata endpoint
75
+ route = routes[0]
76
+ assert route.path == "/.well-known/oauth-protected-resource"
77
+ assert route.methods is not None
78
+ assert "GET" in route.methods
79
+
80
+ def test_get_resource_metadata_url(self):
81
+ """Test get_resource_metadata_url returns correct URL."""
82
+ provider = RemoteAuthProvider(
83
+ token_verifier=SimpleTokenVerifier(),
84
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
85
+ resource_server_url="https://api.example.com",
86
+ )
87
+
88
+ metadata_url = provider.get_resource_metadata_url()
89
+ assert metadata_url == AnyHttpUrl(
90
+ "https://api.example.com/.well-known/oauth-protected-resource"
91
+ )
92
+
93
+ def test_get_resource_metadata_url_handles_trailing_slash(self):
94
+ """Test get_resource_metadata_url handles trailing slash correctly."""
95
+ provider = RemoteAuthProvider(
96
+ token_verifier=SimpleTokenVerifier(),
97
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
98
+ resource_server_url="https://api.example.com/",
99
+ )
100
+
101
+ metadata_url = provider.get_resource_metadata_url()
102
+ assert metadata_url == AnyHttpUrl(
103
+ "https://api.example.com/.well-known/oauth-protected-resource"
104
+ )
105
+
106
+
107
+ class TestRemoteAuthProviderIntegration:
108
+ """Integration tests for RemoteAuthProvider with FastMCP server."""
109
+
110
+ async def test_protected_resource_metadata_endpoint_status_code(self):
111
+ """Test that the protected resource metadata endpoint returns 200."""
112
+ token_verifier = SimpleTokenVerifier()
113
+ auth_provider = RemoteAuthProvider(
114
+ token_verifier=token_verifier,
115
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
116
+ resource_server_url="https://api.example.com/mcp",
117
+ )
118
+
119
+ mcp = FastMCP("test-server", auth=auth_provider)
120
+ mcp_http_app = mcp.http_app()
121
+
122
+ async with httpx.AsyncClient(
123
+ transport=httpx.ASGITransport(app=mcp_http_app),
124
+ base_url="https://api.example.com",
125
+ ) as client:
126
+ response = await client.get("/.well-known/oauth-protected-resource")
127
+ assert response.status_code == 200
128
+
129
+ async def test_protected_resource_metadata_endpoint_resource_field(self):
130
+ """Test that the protected resource metadata endpoint returns correct resource field."""
131
+ token_verifier = SimpleTokenVerifier()
132
+ auth_provider = RemoteAuthProvider(
133
+ token_verifier=token_verifier,
134
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
135
+ resource_server_url="https://api.example.com/mcp",
136
+ )
137
+
138
+ mcp = FastMCP("test-server", auth=auth_provider)
139
+ mcp_http_app = mcp.http_app()
140
+
141
+ async with httpx.AsyncClient(
142
+ transport=httpx.ASGITransport(app=mcp_http_app),
143
+ base_url="https://api.example.com",
144
+ ) as client:
145
+ response = await client.get("/.well-known/oauth-protected-resource")
146
+ data = response.json()
147
+
148
+ # This is the key test - ensure resource field contains the full MCP URL
149
+ assert data["resource"] == "https://api.example.com/mcp"
150
+
151
+ async def test_protected_resource_metadata_endpoint_authorization_servers_field(
152
+ self,
153
+ ):
154
+ """Test that the protected resource metadata endpoint returns correct authorization_servers field."""
155
+ token_verifier = SimpleTokenVerifier()
156
+ auth_provider = RemoteAuthProvider(
157
+ token_verifier=token_verifier,
158
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
159
+ resource_server_url="https://api.example.com/mcp",
160
+ )
161
+
162
+ mcp = FastMCP("test-server", auth=auth_provider)
163
+ mcp_http_app = mcp.http_app()
164
+
165
+ async with httpx.AsyncClient(
166
+ transport=httpx.ASGITransport(app=mcp_http_app),
167
+ base_url="https://api.example.com",
168
+ ) as client:
169
+ response = await client.get("/.well-known/oauth-protected-resource")
170
+ data = response.json()
171
+
172
+ assert data["authorization_servers"] == ["https://auth.example.com/"]
173
+
174
+ @pytest.mark.parametrize(
175
+ "resource_server_url,expected_resource",
176
+ [
177
+ ("https://api.example.com", "https://api.example.com/"),
178
+ ("https://api.example.com/", "https://api.example.com/"),
179
+ ("https://api.example.com/mcp", "https://api.example.com/mcp"),
180
+ ("https://api.example.com/mcp/", "https://api.example.com/mcp/"),
181
+ ],
182
+ )
183
+ async def test_resource_server_url_configurations(
184
+ self, resource_server_url: str, expected_resource: str
185
+ ):
186
+ """Test different resource_server_url configurations."""
187
+ token_verifier = SimpleTokenVerifier()
188
+ auth_provider = RemoteAuthProvider(
189
+ token_verifier=token_verifier,
190
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
191
+ resource_server_url=resource_server_url,
192
+ )
193
+ mcp = FastMCP("test-server", auth=auth_provider)
194
+ mcp_http_app = mcp.http_app()
195
+
196
+ async with httpx.AsyncClient(
197
+ transport=httpx.ASGITransport(app=mcp_http_app),
198
+ base_url="https://test.example.com",
199
+ ) as client:
200
+ response = await client.get("/.well-known/oauth-protected-resource")
201
+
202
+ assert response.status_code == 200
203
+ data = response.json()
204
+ assert data["resource"] == expected_resource
205
+
206
+ async def test_multiple_authorization_servers_resource_field(self):
207
+ """Test resource field with multiple authorization servers."""
208
+ token_verifier = SimpleTokenVerifier()
209
+ auth_servers = [
210
+ AnyHttpUrl("https://auth1.example.com"),
211
+ AnyHttpUrl("https://auth2.example.com"),
212
+ ]
213
+
214
+ auth_provider = RemoteAuthProvider(
215
+ token_verifier=token_verifier,
216
+ authorization_servers=auth_servers,
217
+ resource_server_url="https://api.example.com/mcp",
218
+ )
219
+
220
+ mcp = FastMCP("test-server", auth=auth_provider)
221
+ mcp_http_app = mcp.http_app()
222
+
223
+ async with httpx.AsyncClient(
224
+ transport=httpx.ASGITransport(app=mcp_http_app),
225
+ base_url="https://api.example.com",
226
+ ) as client:
227
+ response = await client.get("/.well-known/oauth-protected-resource")
228
+
229
+ data = response.json()
230
+ assert data["resource"] == "https://api.example.com/mcp"
231
+
232
+ async def test_multiple_authorization_servers_list(self):
233
+ """Test authorization_servers field with multiple authorization servers."""
234
+ token_verifier = SimpleTokenVerifier()
235
+ auth_servers = [
236
+ AnyHttpUrl("https://auth1.example.com"),
237
+ AnyHttpUrl("https://auth2.example.com"),
238
+ ]
239
+
240
+ auth_provider = RemoteAuthProvider(
241
+ token_verifier=token_verifier,
242
+ authorization_servers=auth_servers,
243
+ resource_server_url="https://api.example.com/mcp",
244
+ )
245
+
246
+ mcp = FastMCP("test-server", auth=auth_provider)
247
+ mcp_http_app = mcp.http_app()
248
+
249
+ async with httpx.AsyncClient(
250
+ transport=httpx.ASGITransport(app=mcp_http_app),
251
+ base_url="https://api.example.com",
252
+ ) as client:
253
+ response = await client.get("/.well-known/oauth-protected-resource")
254
+
255
+ data = response.json()
256
+ assert set(data["authorization_servers"]) == {
257
+ "https://auth1.example.com/",
258
+ "https://auth2.example.com/",
259
+ }
260
+
261
+ async def test_token_verification_with_valid_auth_succeeds(self):
262
+ """Test that requests with valid auth token succeed."""
263
+ # Note: This test focuses on HTTP-level authentication behavior
264
+ # For the RemoteAuthProvider, the key test is that the OAuth discovery
265
+ # endpoint correctly reports the resource server URL, which is tested above
266
+
267
+ # This is primarily testing that the token verifier integration works
268
+ access_token = AccessToken(
269
+ token="valid_token", client_id="test-client", scopes=[]
270
+ )
271
+ token_verifier = SimpleTokenVerifier({"valid_token": access_token})
272
+
273
+ provider = RemoteAuthProvider(
274
+ token_verifier=token_verifier,
275
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
276
+ resource_server_url="https://api.example.com/mcp",
277
+ )
278
+
279
+ # Test that the provider correctly delegates to the token verifier
280
+ result = await provider.verify_token("valid_token")
281
+ assert result is access_token
282
+
283
+ result = await provider.verify_token("invalid_token")
284
+ assert result is None
285
+
286
+ async def test_token_verification_with_invalid_auth_fails(self):
287
+ """Test that the provider correctly rejects invalid tokens."""
288
+ access_token = AccessToken(
289
+ token="valid_token", client_id="test-client", scopes=[]
290
+ )
291
+ token_verifier = SimpleTokenVerifier({"valid_token": access_token})
292
+
293
+ provider = RemoteAuthProvider(
294
+ token_verifier=token_verifier,
295
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
296
+ resource_server_url="https://api.example.com/mcp",
297
+ )
298
+
299
+ # Test that invalid tokens are rejected
300
+ result = await provider.verify_token("invalid_token")
301
+ assert result is None
302
+
303
+ async def test_issue_1348_oauth_discovery_returns_correct_url(self):
304
+ """Test that RemoteAuthProvider correctly returns the full MCP endpoint URL.
305
+
306
+ This test confirms that RemoteAuthProvider works correctly and returns
307
+ the exact resource_server_url specified, including full paths like /mcp/.
308
+ """
309
+ token_verifier = SimpleTokenVerifier()
310
+ auth_provider = RemoteAuthProvider(
311
+ token_verifier=token_verifier,
312
+ authorization_servers=[AnyHttpUrl("https://accounts.google.com")],
313
+ resource_server_url="https://my-server.com/mcp/",
314
+ )
315
+
316
+ mcp = FastMCP("test-server", auth=auth_provider)
317
+ mcp_http_app = mcp.http_app()
318
+
319
+ async with httpx.AsyncClient(
320
+ transport=httpx.ASGITransport(app=mcp_http_app),
321
+ base_url="https://my-server.com",
322
+ ) as client:
323
+ response = await client.get("/.well-known/oauth-protected-resource")
324
+
325
+ assert response.status_code == 200
326
+ data = response.json()
327
+
328
+ # The RemoteAuthProvider correctly returns the full MCP endpoint URL
329
+ assert data["resource"] == "https://my-server.com/mcp/"
330
+ assert data["authorization_servers"] == ["https://accounts.google.com/"]