mcp-security-framework 0.1.0__py3-none-any.whl → 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_security_framework/core/auth_manager.py +12 -2
- mcp_security_framework/core/cert_manager.py +247 -16
- mcp_security_framework/core/permission_manager.py +4 -0
- mcp_security_framework/core/rate_limiter.py +10 -0
- mcp_security_framework/core/security_manager.py +2 -0
- mcp_security_framework/examples/comprehensive_example.py +884 -0
- mcp_security_framework/examples/django_example.py +45 -12
- mcp_security_framework/examples/fastapi_example.py +826 -354
- mcp_security_framework/examples/flask_example.py +51 -11
- mcp_security_framework/examples/gateway_example.py +109 -17
- mcp_security_framework/examples/microservice_example.py +112 -16
- mcp_security_framework/examples/standalone_example.py +646 -430
- mcp_security_framework/examples/test_all_examples.py +556 -0
- mcp_security_framework/middleware/auth_middleware.py +1 -1
- mcp_security_framework/middleware/fastapi_auth_middleware.py +82 -14
- mcp_security_framework/middleware/flask_auth_middleware.py +154 -7
- mcp_security_framework/schemas/models.py +1 -0
- mcp_security_framework/utils/cert_utils.py +5 -5
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/METADATA +1 -1
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/RECORD +38 -32
- tests/conftest.py +306 -0
- tests/test_cli/test_cert_cli.py +13 -31
- tests/test_core/test_cert_manager.py +12 -12
- tests/test_examples/test_comprehensive_example.py +560 -0
- tests/test_examples/test_fastapi_example.py +214 -116
- tests/test_examples/test_flask_example.py +250 -131
- tests/test_examples/test_standalone_example.py +44 -99
- tests/test_integration/test_auth_flow.py +4 -4
- tests/test_integration/test_certificate_flow.py +1 -1
- tests/test_integration/test_fastapi_integration.py +39 -45
- tests/test_integration/test_flask_integration.py +4 -2
- tests/test_integration/test_standalone_integration.py +48 -48
- tests/test_middleware/test_fastapi_auth_middleware.py +724 -0
- tests/test_middleware/test_flask_auth_middleware.py +638 -0
- tests/test_middleware/test_security_middleware.py +9 -3
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/WHEEL +0 -0
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,724 @@
|
|
1
|
+
"""
|
2
|
+
FastAPI Authentication Middleware Tests
|
3
|
+
|
4
|
+
This module contains comprehensive tests for the FastAPI Authentication Middleware
|
5
|
+
implementation of the MCP Security Framework.
|
6
|
+
|
7
|
+
Author: Vasiliy Zdanovskiy
|
8
|
+
email: vasilyvz@gmail.com
|
9
|
+
Version: 1.0.0
|
10
|
+
License: MIT
|
11
|
+
"""
|
12
|
+
|
13
|
+
import json
|
14
|
+
import pytest
|
15
|
+
from unittest.mock import Mock, patch, AsyncMock
|
16
|
+
from typing import Dict, Any
|
17
|
+
|
18
|
+
from fastapi import Request, Response, status
|
19
|
+
from fastapi.responses import JSONResponse
|
20
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
21
|
+
|
22
|
+
from mcp_security_framework.middleware.fastapi_auth_middleware import FastAPIAuthMiddleware
|
23
|
+
from mcp_security_framework.schemas.config import SecurityConfig, AuthConfig
|
24
|
+
from mcp_security_framework.schemas.models import AuthResult, AuthStatus, AuthMethod
|
25
|
+
from mcp_security_framework.middleware.auth_middleware import AuthMiddlewareError
|
26
|
+
|
27
|
+
|
28
|
+
class TestFastAPIAuthMiddleware:
|
29
|
+
"""Test suite for FastAPI Authentication Middleware."""
|
30
|
+
|
31
|
+
def setup_method(self):
|
32
|
+
"""Set up test fixtures before each test method."""
|
33
|
+
# Create test configuration
|
34
|
+
self.config = SecurityConfig(
|
35
|
+
auth=AuthConfig(
|
36
|
+
enabled=True,
|
37
|
+
methods=["api_key", "jwt"],
|
38
|
+
api_keys={
|
39
|
+
"test_key_123": {"username": "testuser", "roles": ["user"]}
|
40
|
+
},
|
41
|
+
jwt_secret="test-jwt-secret-key-for-testing",
|
42
|
+
jwt_algorithm="HS256",
|
43
|
+
jwt_expiry_hours=24,
|
44
|
+
public_paths=["/health", "/metrics"]
|
45
|
+
)
|
46
|
+
)
|
47
|
+
|
48
|
+
# Create mock security manager
|
49
|
+
from mcp_security_framework.core.security_manager import SecurityManager
|
50
|
+
self.mock_security_manager = Mock(spec=SecurityManager)
|
51
|
+
self.mock_security_manager.authenticate_user = Mock()
|
52
|
+
self.mock_security_manager.config = self.config
|
53
|
+
|
54
|
+
# Create mock auth manager
|
55
|
+
self.mock_auth_manager = Mock()
|
56
|
+
self.mock_auth_manager.authenticate_api_key = Mock()
|
57
|
+
self.mock_auth_manager.authenticate_jwt_token = Mock()
|
58
|
+
self.mock_auth_manager.authenticate_certificate = Mock()
|
59
|
+
self.mock_security_manager.auth_manager = self.mock_auth_manager
|
60
|
+
|
61
|
+
# Create middleware instance
|
62
|
+
self.middleware = FastAPIAuthMiddleware(self.mock_security_manager)
|
63
|
+
|
64
|
+
def test_fastapi_auth_middleware_initialization(self):
|
65
|
+
"""Test FastAPI Authentication Middleware initialization."""
|
66
|
+
assert self.middleware is not None
|
67
|
+
assert isinstance(self.middleware, FastAPIAuthMiddleware)
|
68
|
+
assert self.middleware.config == self.config
|
69
|
+
assert self.middleware.security_manager == self.mock_security_manager
|
70
|
+
assert hasattr(self.middleware, 'logger')
|
71
|
+
|
72
|
+
@pytest.mark.asyncio
|
73
|
+
async def test_fastapi_auth_middleware_public_path_bypass(self):
|
74
|
+
"""Test that public paths bypass authentication."""
|
75
|
+
# Create mock request for public path
|
76
|
+
mock_request = Mock(spec=Request)
|
77
|
+
mock_request.url.path = "/health"
|
78
|
+
mock_request.headers = {}
|
79
|
+
|
80
|
+
# Create mock call_next
|
81
|
+
mock_call_next = AsyncMock()
|
82
|
+
mock_response = Mock(spec=Response)
|
83
|
+
mock_call_next.return_value = mock_response
|
84
|
+
|
85
|
+
# Process request
|
86
|
+
response = await self.middleware(mock_request, mock_call_next)
|
87
|
+
|
88
|
+
# Assertions
|
89
|
+
assert response == mock_response
|
90
|
+
mock_call_next.assert_called_once_with(mock_request)
|
91
|
+
# Security manager should not be called for public paths
|
92
|
+
self.mock_security_manager.authenticate_user.assert_not_called()
|
93
|
+
|
94
|
+
@pytest.mark.asyncio
|
95
|
+
async def test_fastapi_auth_middleware_api_key_authentication_success(self):
|
96
|
+
"""Test successful API key authentication."""
|
97
|
+
# Create mock request with API key
|
98
|
+
mock_request = Mock(spec=Request)
|
99
|
+
mock_request.url.path = "/api/v1/users/me"
|
100
|
+
mock_request.headers = {"X-API-Key": "test_key_123"}
|
101
|
+
|
102
|
+
# Mock successful authentication
|
103
|
+
auth_result = AuthResult(
|
104
|
+
is_valid=True,
|
105
|
+
status=AuthStatus.SUCCESS,
|
106
|
+
username="testuser",
|
107
|
+
roles=["user"],
|
108
|
+
auth_method=AuthMethod.API_KEY
|
109
|
+
)
|
110
|
+
self.mock_auth_manager.authenticate_api_key.return_value = auth_result
|
111
|
+
|
112
|
+
# Create mock call_next
|
113
|
+
mock_call_next = AsyncMock()
|
114
|
+
mock_response = Mock(spec=Response)
|
115
|
+
mock_call_next.return_value = mock_response
|
116
|
+
|
117
|
+
# Process request
|
118
|
+
response = await self.middleware(mock_request, mock_call_next)
|
119
|
+
|
120
|
+
# Assertions
|
121
|
+
assert response == mock_response
|
122
|
+
mock_call_next.assert_called_once_with(mock_request)
|
123
|
+
self.mock_auth_manager.authenticate_api_key.assert_called_once_with("test_key_123")
|
124
|
+
|
125
|
+
# Check that user info was added to request state
|
126
|
+
assert hasattr(mock_request.state, 'auth_result')
|
127
|
+
assert mock_request.state.auth_result.username == "testuser"
|
128
|
+
assert mock_request.state.auth_result.roles == ["user"]
|
129
|
+
|
130
|
+
@pytest.mark.asyncio
|
131
|
+
async def test_fastapi_auth_middleware_jwt_authentication_success(self):
|
132
|
+
"""Test successful JWT authentication."""
|
133
|
+
# Create mock request with JWT token
|
134
|
+
mock_request = Mock(spec=Request)
|
135
|
+
mock_request.url.path = "/api/v1/users/me"
|
136
|
+
mock_request.headers = {"Authorization": "Bearer test_jwt_token"}
|
137
|
+
|
138
|
+
# Mock failed API key authentication first
|
139
|
+
failed_api_key_result = AuthResult(
|
140
|
+
is_valid=False,
|
141
|
+
status=AuthStatus.FAILED,
|
142
|
+
username=None,
|
143
|
+
roles=[],
|
144
|
+
auth_method=AuthMethod.API_KEY,
|
145
|
+
error_code=-32012,
|
146
|
+
error_message="API key not found in request"
|
147
|
+
)
|
148
|
+
self.mock_auth_manager.authenticate_api_key.return_value = failed_api_key_result
|
149
|
+
|
150
|
+
# Mock successful JWT authentication
|
151
|
+
auth_result = AuthResult(
|
152
|
+
is_valid=True,
|
153
|
+
status=AuthStatus.SUCCESS,
|
154
|
+
username="testuser",
|
155
|
+
roles=["user"],
|
156
|
+
auth_method=AuthMethod.JWT
|
157
|
+
)
|
158
|
+
self.mock_auth_manager.authenticate_jwt_token.return_value = auth_result
|
159
|
+
|
160
|
+
# Create mock call_next
|
161
|
+
mock_call_next = AsyncMock()
|
162
|
+
mock_response = Mock(spec=Response)
|
163
|
+
mock_call_next.return_value = mock_response
|
164
|
+
|
165
|
+
# Process request
|
166
|
+
response = await self.middleware(mock_request, mock_call_next)
|
167
|
+
|
168
|
+
# Assertions
|
169
|
+
assert response == mock_response
|
170
|
+
mock_call_next.assert_called_once_with(mock_request)
|
171
|
+
self.mock_auth_manager.authenticate_jwt_token.assert_called_once_with("test_jwt_token")
|
172
|
+
|
173
|
+
@pytest.mark.asyncio
|
174
|
+
async def test_fastapi_auth_middleware_certificate_authentication_success(self):
|
175
|
+
"""Test successful certificate authentication."""
|
176
|
+
# Create mock request with certificate
|
177
|
+
mock_request = Mock(spec=Request)
|
178
|
+
mock_request.url.path = "/api/v1/users/me"
|
179
|
+
mock_request.headers = {"X-Client-Cert": "test_certificate_data"}
|
180
|
+
|
181
|
+
# Mock failed API key authentication first
|
182
|
+
failed_api_key_result = AuthResult(
|
183
|
+
is_valid=False,
|
184
|
+
status=AuthStatus.FAILED,
|
185
|
+
username=None,
|
186
|
+
roles=[],
|
187
|
+
auth_method=AuthMethod.API_KEY,
|
188
|
+
error_code=-32012,
|
189
|
+
error_message="API key not found in request"
|
190
|
+
)
|
191
|
+
self.mock_auth_manager.authenticate_api_key.return_value = failed_api_key_result
|
192
|
+
|
193
|
+
# Mock failed JWT authentication
|
194
|
+
failed_jwt_result = AuthResult(
|
195
|
+
is_valid=False,
|
196
|
+
status=AuthStatus.FAILED,
|
197
|
+
username=None,
|
198
|
+
roles=[],
|
199
|
+
auth_method=AuthMethod.JWT,
|
200
|
+
error_code=-32013,
|
201
|
+
error_message="JWT token not found in Authorization header"
|
202
|
+
)
|
203
|
+
self.mock_auth_manager.authenticate_jwt_token.return_value = failed_jwt_result
|
204
|
+
|
205
|
+
# Certificate authentication is not implemented, so it will fail
|
206
|
+
# We expect the middleware to return an error response
|
207
|
+
|
208
|
+
# Create mock call_next
|
209
|
+
mock_call_next = AsyncMock()
|
210
|
+
mock_response = Mock(spec=Response)
|
211
|
+
mock_call_next.return_value = mock_response
|
212
|
+
|
213
|
+
# Process request
|
214
|
+
response = await self.middleware(mock_request, mock_call_next)
|
215
|
+
|
216
|
+
# Assertions - certificate auth should fail
|
217
|
+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
218
|
+
response_data = json.loads(response.body.decode())
|
219
|
+
assert response_data["error"] == "Authentication failed"
|
220
|
+
assert response_data["error_code"] == -32033 # All authentication methods failed
|
221
|
+
|
222
|
+
# call_next should not be called for failed authentication
|
223
|
+
mock_call_next.assert_not_called()
|
224
|
+
|
225
|
+
@pytest.mark.asyncio
|
226
|
+
async def test_fastapi_auth_middleware_authentication_failure(self):
|
227
|
+
"""Test authentication failure handling."""
|
228
|
+
# Create mock request without authentication
|
229
|
+
mock_request = Mock(spec=Request)
|
230
|
+
mock_request.url.path = "/api/v1/users/me"
|
231
|
+
mock_request.headers = {}
|
232
|
+
|
233
|
+
# Mock failed authentication - this test expects all methods to fail
|
234
|
+
failed_auth_result = AuthResult(
|
235
|
+
is_valid=False,
|
236
|
+
status=AuthStatus.INVALID,
|
237
|
+
auth_method=AuthMethod.UNKNOWN,
|
238
|
+
error_code=-32001,
|
239
|
+
error_message="No authentication credentials provided"
|
240
|
+
)
|
241
|
+
self.mock_auth_manager.authenticate_api_key.return_value = failed_auth_result
|
242
|
+
|
243
|
+
# Create mock call_next
|
244
|
+
mock_call_next = AsyncMock()
|
245
|
+
|
246
|
+
# Process request
|
247
|
+
response = await self.middleware(mock_request, mock_call_next)
|
248
|
+
|
249
|
+
# Assertions
|
250
|
+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
251
|
+
response_data = json.loads(response.body.decode())
|
252
|
+
assert response_data["error"] == "Authentication failed"
|
253
|
+
assert response_data["error_code"] == -32033 # All authentication methods failed
|
254
|
+
assert response_data["error_message"] == "All authentication methods failed"
|
255
|
+
|
256
|
+
# call_next should not be called for failed authentication
|
257
|
+
mock_call_next.assert_not_called()
|
258
|
+
|
259
|
+
@pytest.mark.asyncio
|
260
|
+
async def test_fastapi_auth_middleware_invalid_api_key(self):
|
261
|
+
"""Test handling of invalid API key."""
|
262
|
+
# Create mock request with invalid API key
|
263
|
+
mock_request = Mock(spec=Request)
|
264
|
+
mock_request.url.path = "/api/v1/users/me"
|
265
|
+
mock_request.headers = {"X-API-Key": "invalid_key"}
|
266
|
+
|
267
|
+
# Mock failed authentication
|
268
|
+
auth_result = AuthResult(
|
269
|
+
is_valid=False,
|
270
|
+
status=AuthStatus.INVALID,
|
271
|
+
auth_method=AuthMethod.API_KEY,
|
272
|
+
error_code=-32002,
|
273
|
+
error_message="Invalid API key"
|
274
|
+
)
|
275
|
+
self.mock_auth_manager.authenticate_api_key.return_value = auth_result
|
276
|
+
|
277
|
+
# Create mock call_next
|
278
|
+
mock_call_next = AsyncMock()
|
279
|
+
|
280
|
+
# Process request
|
281
|
+
response = await self.middleware(mock_request, mock_call_next)
|
282
|
+
|
283
|
+
# Assertions
|
284
|
+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
285
|
+
response_data = json.loads(response.body.decode())
|
286
|
+
assert response_data["error"] == "Authentication failed"
|
287
|
+
assert response_data["error_code"] == -32033 # All authentication methods failed
|
288
|
+
|
289
|
+
@pytest.mark.asyncio
|
290
|
+
async def test_fastapi_auth_middleware_invalid_jwt_token(self):
|
291
|
+
"""Test handling of invalid JWT token."""
|
292
|
+
# Create mock request with invalid JWT token
|
293
|
+
mock_request = Mock(spec=Request)
|
294
|
+
mock_request.url.path = "/api/v1/users/me"
|
295
|
+
mock_request.headers = {"Authorization": "Bearer invalid_token"}
|
296
|
+
|
297
|
+
# Mock failed API key authentication first
|
298
|
+
failed_api_key_result = AuthResult(
|
299
|
+
is_valid=False,
|
300
|
+
status=AuthStatus.FAILED,
|
301
|
+
username=None,
|
302
|
+
roles=[],
|
303
|
+
auth_method=AuthMethod.API_KEY,
|
304
|
+
error_code=-32012,
|
305
|
+
error_message="API key not found in request"
|
306
|
+
)
|
307
|
+
self.mock_auth_manager.authenticate_api_key.return_value = failed_api_key_result
|
308
|
+
|
309
|
+
# Mock failed JWT authentication
|
310
|
+
auth_result = AuthResult(
|
311
|
+
is_valid=False,
|
312
|
+
status=AuthStatus.INVALID,
|
313
|
+
auth_method=AuthMethod.JWT,
|
314
|
+
error_code=-32003,
|
315
|
+
error_message="Invalid JWT token"
|
316
|
+
)
|
317
|
+
self.mock_auth_manager.authenticate_jwt_token.return_value = auth_result
|
318
|
+
|
319
|
+
# Create mock call_next
|
320
|
+
mock_call_next = AsyncMock()
|
321
|
+
|
322
|
+
# Process request
|
323
|
+
response = await self.middleware(mock_request, mock_call_next)
|
324
|
+
|
325
|
+
# Assertions
|
326
|
+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
327
|
+
response_data = json.loads(response.body.decode())
|
328
|
+
assert response_data["error"] == "Authentication failed"
|
329
|
+
assert response_data["error_code"] == -32033 # All authentication methods failed
|
330
|
+
|
331
|
+
@pytest.mark.asyncio
|
332
|
+
async def test_fastapi_auth_middleware_exception_handling(self):
|
333
|
+
"""Test exception handling in middleware."""
|
334
|
+
# Create mock request
|
335
|
+
mock_request = Mock(spec=Request)
|
336
|
+
mock_request.url.path = "/api/v1/users/me"
|
337
|
+
mock_request.headers = {"X-API-Key": "test_key_123"}
|
338
|
+
|
339
|
+
# Mock auth manager to raise exception
|
340
|
+
self.mock_auth_manager.authenticate_api_key.side_effect = Exception("Authentication error")
|
341
|
+
|
342
|
+
# Create mock call_next
|
343
|
+
mock_call_next = AsyncMock()
|
344
|
+
|
345
|
+
# Process request
|
346
|
+
response = await self.middleware(mock_request, mock_call_next)
|
347
|
+
|
348
|
+
# Assertions
|
349
|
+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
350
|
+
response_data = json.loads(response.body.decode())
|
351
|
+
assert response_data["error"] == "Authentication failed"
|
352
|
+
assert response_data["error_code"] == -32033 # All authentication methods failed
|
353
|
+
|
354
|
+
@pytest.mark.asyncio
|
355
|
+
async def test_fastapi_auth_middleware_user_info_structure(self):
|
356
|
+
"""Test that user info is properly structured in request state."""
|
357
|
+
# Create mock request with API key
|
358
|
+
mock_request = Mock(spec=Request)
|
359
|
+
mock_request.url.path = "/api/v1/users/me"
|
360
|
+
mock_request.headers = {"X-API-Key": "test_key_123"}
|
361
|
+
|
362
|
+
# Mock successful authentication
|
363
|
+
auth_result = AuthResult(
|
364
|
+
is_valid=True,
|
365
|
+
status=AuthStatus.SUCCESS,
|
366
|
+
username="testuser",
|
367
|
+
roles=["user", "admin"],
|
368
|
+
permissions=["read:own", "write:own"],
|
369
|
+
auth_method=AuthMethod.API_KEY
|
370
|
+
)
|
371
|
+
self.mock_auth_manager.authenticate_api_key.return_value = auth_result
|
372
|
+
|
373
|
+
# Create mock call_next
|
374
|
+
mock_call_next = AsyncMock()
|
375
|
+
mock_response = Mock(spec=Response)
|
376
|
+
mock_call_next.return_value = mock_response
|
377
|
+
|
378
|
+
# Process request
|
379
|
+
await self.middleware(mock_request, mock_call_next)
|
380
|
+
|
381
|
+
# Check user info structure
|
382
|
+
assert hasattr(mock_request.state, 'auth_result')
|
383
|
+
assert mock_request.state.auth_result.username == "testuser"
|
384
|
+
assert mock_request.state.auth_result.roles == ["user", "admin"]
|
385
|
+
assert mock_request.state.auth_result.permissions == {"read:own", "write:own"}
|
386
|
+
assert mock_request.state.auth_result.auth_method == AuthMethod.API_KEY
|
387
|
+
|
388
|
+
@pytest.mark.asyncio
|
389
|
+
async def test_fastapi_auth_middleware_multiple_public_paths(self):
|
390
|
+
"""Test that multiple public paths are handled correctly."""
|
391
|
+
public_paths = ["/health", "/metrics", "/docs", "/openapi.json"]
|
392
|
+
|
393
|
+
for path in public_paths:
|
394
|
+
# Create mock request for public path
|
395
|
+
mock_request = Mock(spec=Request)
|
396
|
+
mock_request.url.path = path
|
397
|
+
mock_request.headers = {}
|
398
|
+
|
399
|
+
# Create mock call_next
|
400
|
+
mock_call_next = AsyncMock()
|
401
|
+
mock_response = Mock(spec=Response)
|
402
|
+
mock_call_next.return_value = mock_response
|
403
|
+
|
404
|
+
# Process request
|
405
|
+
response = await self.middleware(mock_request, mock_call_next)
|
406
|
+
|
407
|
+
# Assertions
|
408
|
+
assert response == mock_response
|
409
|
+
mock_call_next.assert_called_once_with(mock_request)
|
410
|
+
|
411
|
+
# Reset mock for next iteration
|
412
|
+
mock_call_next.reset_mock()
|
413
|
+
|
414
|
+
@pytest.mark.asyncio
|
415
|
+
async def test_fastapi_auth_middleware_disabled_authentication(self):
|
416
|
+
"""Test middleware behavior when authentication is disabled."""
|
417
|
+
# Create config with disabled authentication
|
418
|
+
disabled_config = SecurityConfig(
|
419
|
+
auth=AuthConfig(
|
420
|
+
enabled=False,
|
421
|
+
methods=["api_key"],
|
422
|
+
api_keys={},
|
423
|
+
public_paths=[]
|
424
|
+
)
|
425
|
+
)
|
426
|
+
|
427
|
+
# Create middleware with disabled auth
|
428
|
+
from mcp_security_framework.core.security_manager import SecurityManager
|
429
|
+
disabled_security_manager = Mock(spec=SecurityManager)
|
430
|
+
disabled_security_manager.config = disabled_config
|
431
|
+
disabled_auth_manager = Mock()
|
432
|
+
disabled_security_manager.auth_manager = disabled_auth_manager
|
433
|
+
disabled_middleware = FastAPIAuthMiddleware(disabled_security_manager)
|
434
|
+
|
435
|
+
# Create mock request
|
436
|
+
mock_request = Mock(spec=Request)
|
437
|
+
mock_request.url.path = "/api/v1/users/me"
|
438
|
+
mock_request.headers = {}
|
439
|
+
|
440
|
+
# Create mock call_next
|
441
|
+
mock_call_next = AsyncMock()
|
442
|
+
mock_response = Mock(spec=Response)
|
443
|
+
mock_call_next.return_value = mock_response
|
444
|
+
|
445
|
+
# Process request
|
446
|
+
response = await disabled_middleware(mock_request, mock_call_next)
|
447
|
+
|
448
|
+
# Assertions - should bypass authentication when disabled
|
449
|
+
assert response == mock_response
|
450
|
+
mock_call_next.assert_called_once_with(mock_request)
|
451
|
+
self.mock_security_manager.authenticate_user.assert_not_called()
|
452
|
+
|
453
|
+
@pytest.mark.asyncio
|
454
|
+
async def test_fastapi_auth_middleware_logging(self):
|
455
|
+
"""Test that authentication events are logged."""
|
456
|
+
with patch.object(self.middleware.logger, 'info') as mock_logger:
|
457
|
+
# Create mock request with API key
|
458
|
+
mock_request = Mock(spec=Request)
|
459
|
+
mock_request.url.path = "/api/v1/users/me"
|
460
|
+
mock_request.headers = {"X-API-Key": "test_key_123"}
|
461
|
+
|
462
|
+
# Mock successful authentication
|
463
|
+
auth_result = AuthResult(
|
464
|
+
is_valid=True,
|
465
|
+
status=AuthStatus.SUCCESS,
|
466
|
+
username="testuser",
|
467
|
+
roles=["user"],
|
468
|
+
auth_method=AuthMethod.API_KEY
|
469
|
+
)
|
470
|
+
self.mock_auth_manager.authenticate_api_key.return_value = auth_result
|
471
|
+
|
472
|
+
# Create mock call_next
|
473
|
+
mock_call_next = AsyncMock()
|
474
|
+
mock_response = Mock(spec=Response)
|
475
|
+
mock_call_next.return_value = mock_response
|
476
|
+
|
477
|
+
# Process request
|
478
|
+
await self.middleware(mock_request, mock_call_next)
|
479
|
+
|
480
|
+
# Assertions - should log authentication success
|
481
|
+
mock_logger.assert_called()
|
482
|
+
|
483
|
+
@pytest.mark.asyncio
|
484
|
+
async def test_fastapi_auth_middleware_error_logging(self):
|
485
|
+
"""Test that authentication errors are logged."""
|
486
|
+
with patch.object(self.middleware.logger, 'error') as mock_logger:
|
487
|
+
# Create mock request
|
488
|
+
mock_request = Mock(spec=Request)
|
489
|
+
mock_request.url.path = "/api/v1/users/me"
|
490
|
+
mock_request.headers = {"X-API-Key": "test_key_123"}
|
491
|
+
|
492
|
+
# Mock auth manager to raise exception
|
493
|
+
self.mock_auth_manager.authenticate_api_key.side_effect = Exception("Authentication error")
|
494
|
+
|
495
|
+
# Create mock call_next
|
496
|
+
mock_call_next = AsyncMock()
|
497
|
+
|
498
|
+
# Process request
|
499
|
+
await self.middleware(mock_request, mock_call_next)
|
500
|
+
|
501
|
+
# Assertions - should log authentication error
|
502
|
+
mock_logger.assert_called()
|
503
|
+
|
504
|
+
def test_fastapi_auth_middleware_inheritance(self):
|
505
|
+
"""Test that FastAPIAuthMiddleware properly inherits from AuthMiddleware."""
|
506
|
+
from mcp_security_framework.middleware.auth_middleware import AuthMiddleware
|
507
|
+
|
508
|
+
assert issubclass(FastAPIAuthMiddleware, AuthMiddleware)
|
509
|
+
assert isinstance(self.middleware, AuthMiddleware)
|
510
|
+
|
511
|
+
def test_fastapi_auth_middleware_config_validation(self):
|
512
|
+
"""Test that middleware validates configuration properly."""
|
513
|
+
# Test with valid config
|
514
|
+
assert self.middleware.config is not None
|
515
|
+
assert self.middleware.config.auth.enabled is True
|
516
|
+
assert "api_key" in self.middleware.config.auth.methods
|
517
|
+
|
518
|
+
# Test with invalid config (should not raise during init)
|
519
|
+
try:
|
520
|
+
invalid_config = SecurityConfig(
|
521
|
+
auth=AuthConfig(
|
522
|
+
enabled=True,
|
523
|
+
methods=["invalid_method"],
|
524
|
+
api_keys={},
|
525
|
+
public_paths=[]
|
526
|
+
)
|
527
|
+
)
|
528
|
+
# If we get here, the config was accepted (which is wrong)
|
529
|
+
assert False, "Invalid config should have raised validation error"
|
530
|
+
except Exception as e:
|
531
|
+
# This is expected - invalid config should raise validation error
|
532
|
+
assert "invalid_method" in str(e)
|
533
|
+
# Don't try to create middleware with invalid config
|
534
|
+
# The middleware should be valid since we're testing the existing one
|
535
|
+
assert self.middleware is not None
|
536
|
+
|
537
|
+
@pytest.mark.asyncio
|
538
|
+
async def test_fastapi_auth_middleware_exception_in_call(self):
|
539
|
+
"""Test exception handling in __call__ method."""
|
540
|
+
# Create mock request
|
541
|
+
mock_request = Mock(spec=Request)
|
542
|
+
mock_request.url.path = "/api/v1/users/me"
|
543
|
+
mock_request.headers = {"X-API-Key": "test_key_123"}
|
544
|
+
|
545
|
+
# Mock call_next to raise exception
|
546
|
+
mock_call_next = AsyncMock()
|
547
|
+
mock_call_next.side_effect = Exception("Test exception")
|
548
|
+
|
549
|
+
# Process request - should handle exception gracefully
|
550
|
+
with pytest.raises(Exception) as exc_info:
|
551
|
+
await self.middleware(mock_request, mock_call_next)
|
552
|
+
|
553
|
+
assert "Authentication processing failed" in str(exc_info.value)
|
554
|
+
assert hasattr(exc_info.value, 'error_code')
|
555
|
+
assert exc_info.value.error_code == -32035
|
556
|
+
|
557
|
+
def test_fastapi_auth_middleware_get_client_ip_x_forwarded_for(self):
|
558
|
+
"""Test getting client IP from X-Forwarded-For header."""
|
559
|
+
mock_request = Mock(spec=Request)
|
560
|
+
mock_request.headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
561
|
+
|
562
|
+
ip = self.middleware._get_client_ip(mock_request)
|
563
|
+
assert ip == "192.168.1.1"
|
564
|
+
|
565
|
+
def test_fastapi_auth_middleware_get_client_ip_x_real_ip(self):
|
566
|
+
"""Test getting client IP from X-Real-IP header."""
|
567
|
+
mock_request = Mock(spec=Request)
|
568
|
+
mock_request.headers = {"X-Real-IP": "192.168.1.2"}
|
569
|
+
|
570
|
+
ip = self.middleware._get_client_ip(mock_request)
|
571
|
+
assert ip == "192.168.1.2"
|
572
|
+
|
573
|
+
def test_fastapi_auth_middleware_get_client_ip_client_host(self):
|
574
|
+
"""Test getting client IP from client.host."""
|
575
|
+
mock_request = Mock(spec=Request)
|
576
|
+
mock_request.headers = {}
|
577
|
+
mock_request.client = Mock()
|
578
|
+
mock_request.client.host = "192.168.1.3"
|
579
|
+
|
580
|
+
ip = self.middleware._get_client_ip(mock_request)
|
581
|
+
assert ip == "192.168.1.3"
|
582
|
+
|
583
|
+
def test_fastapi_auth_middleware_get_client_ip_no_client(self):
|
584
|
+
"""Test getting client IP when client is None."""
|
585
|
+
mock_request = Mock(spec=Request)
|
586
|
+
mock_request.headers = {}
|
587
|
+
mock_request.client = None
|
588
|
+
|
589
|
+
ip = self.middleware._get_client_ip(mock_request)
|
590
|
+
assert ip == "127.0.0.1" # Default fallback from constants
|
591
|
+
|
592
|
+
def test_fastapi_auth_middleware_get_client_ip_with_default_ip_config(self):
|
593
|
+
"""Test getting client IP with default_ip in config."""
|
594
|
+
mock_request = Mock(spec=Request)
|
595
|
+
mock_request.headers = {}
|
596
|
+
mock_request.client = None
|
597
|
+
|
598
|
+
# Mock the config to have default_client_ip attribute
|
599
|
+
self.middleware.config = Mock()
|
600
|
+
self.middleware.config.default_client_ip = "192.168.0.1"
|
601
|
+
|
602
|
+
ip = self.middleware._get_client_ip(mock_request)
|
603
|
+
assert ip == "192.168.0.1"
|
604
|
+
|
605
|
+
def test_fastapi_auth_middleware_get_cache_key(self):
|
606
|
+
"""Test cache key generation."""
|
607
|
+
mock_request = Mock(spec=Request)
|
608
|
+
mock_request.headers = {"User-Agent": "test-browser/1.0"}
|
609
|
+
mock_request.client = Mock()
|
610
|
+
mock_request.client.host = "192.168.1.1"
|
611
|
+
|
612
|
+
cache_key = self.middleware._get_cache_key(mock_request)
|
613
|
+
assert cache_key.startswith("auth:192.168.1.1:")
|
614
|
+
assert isinstance(cache_key, str)
|
615
|
+
assert len(cache_key) > 0
|
616
|
+
|
617
|
+
def test_fastapi_auth_middleware_unsupported_auth_method(self):
|
618
|
+
"""Test handling of unsupported authentication method."""
|
619
|
+
mock_request = Mock(spec=Request)
|
620
|
+
|
621
|
+
result = self.middleware._try_auth_method(mock_request, "unsupported_method")
|
622
|
+
|
623
|
+
assert result.is_valid is False
|
624
|
+
assert result.error_code == -32022
|
625
|
+
assert "Unsupported authentication method" in result.error_message
|
626
|
+
|
627
|
+
def test_fastapi_auth_middleware_auth_method_exception(self):
|
628
|
+
"""Test exception handling in _try_auth_method."""
|
629
|
+
mock_request = Mock(spec=Request)
|
630
|
+
|
631
|
+
# Mock authenticate_api_key to raise exception
|
632
|
+
self.mock_auth_manager.authenticate_api_key.side_effect = Exception("Test auth error")
|
633
|
+
|
634
|
+
result = self.middleware._try_auth_method(mock_request, "api_key")
|
635
|
+
|
636
|
+
assert result.is_valid is False
|
637
|
+
assert result.error_code == -32023
|
638
|
+
assert "Authentication method api_key failed" in result.error_message
|
639
|
+
|
640
|
+
def test_fastapi_auth_middleware_api_key_from_authorization_header(self):
|
641
|
+
"""Test API key authentication using Authorization header."""
|
642
|
+
mock_request = Mock(spec=Request)
|
643
|
+
mock_request.headers = {"Authorization": "Bearer api_key_123"}
|
644
|
+
|
645
|
+
# Mock successful authentication
|
646
|
+
auth_result = AuthResult(
|
647
|
+
is_valid=True,
|
648
|
+
status=AuthStatus.SUCCESS,
|
649
|
+
username="testuser",
|
650
|
+
roles=["user"],
|
651
|
+
auth_method=AuthMethod.API_KEY
|
652
|
+
)
|
653
|
+
self.mock_auth_manager.authenticate_api_key.return_value = auth_result
|
654
|
+
|
655
|
+
result = self.middleware._try_api_key_auth(mock_request)
|
656
|
+
|
657
|
+
assert result.is_valid is True
|
658
|
+
self.mock_auth_manager.authenticate_api_key.assert_called_once_with("api_key_123")
|
659
|
+
|
660
|
+
def test_fastapi_auth_middleware_api_key_no_key(self):
|
661
|
+
"""Test API key authentication when no key is provided."""
|
662
|
+
mock_request = Mock(spec=Request)
|
663
|
+
mock_request.headers = {}
|
664
|
+
|
665
|
+
result = self.middleware._try_api_key_auth(mock_request)
|
666
|
+
|
667
|
+
assert result.is_valid is False
|
668
|
+
assert result.error_code == -32012
|
669
|
+
assert "API key not found in request" in result.error_message
|
670
|
+
|
671
|
+
def test_fastapi_auth_middleware_jwt_no_token(self):
|
672
|
+
"""Test JWT authentication when no token is provided."""
|
673
|
+
mock_request = Mock(spec=Request)
|
674
|
+
mock_request.headers = {}
|
675
|
+
|
676
|
+
result = self.middleware._try_jwt_auth(mock_request)
|
677
|
+
|
678
|
+
assert result.is_valid is False
|
679
|
+
assert result.error_code == -32013
|
680
|
+
assert "JWT token not found in Authorization header" in result.error_message
|
681
|
+
|
682
|
+
def test_fastapi_auth_middleware_jwt_wrong_format(self):
|
683
|
+
"""Test JWT authentication with wrong Authorization header format."""
|
684
|
+
mock_request = Mock(spec=Request)
|
685
|
+
mock_request.headers = {"Authorization": "Basic dGVzdDp0ZXN0"}
|
686
|
+
|
687
|
+
result = self.middleware._try_jwt_auth(mock_request)
|
688
|
+
|
689
|
+
assert result.is_valid is False
|
690
|
+
assert result.error_code == -32013
|
691
|
+
assert "JWT token not found in Authorization header" in result.error_message
|
692
|
+
|
693
|
+
def test_fastapi_auth_middleware_basic_auth_not_implemented(self):
|
694
|
+
"""Test basic authentication (not implemented)."""
|
695
|
+
mock_request = Mock(spec=Request)
|
696
|
+
mock_request.headers = {"Authorization": "Basic dGVzdDp0ZXN0"}
|
697
|
+
|
698
|
+
result = self.middleware._try_basic_auth(mock_request)
|
699
|
+
|
700
|
+
assert result.is_valid is False
|
701
|
+
assert result.error_code == -32016
|
702
|
+
assert "Basic authentication not implemented" in result.error_message
|
703
|
+
|
704
|
+
def test_fastapi_auth_middleware_basic_auth_no_credentials(self):
|
705
|
+
"""Test basic authentication when no credentials are provided."""
|
706
|
+
mock_request = Mock(spec=Request)
|
707
|
+
mock_request.headers = {}
|
708
|
+
|
709
|
+
result = self.middleware._try_basic_auth(mock_request)
|
710
|
+
|
711
|
+
assert result.is_valid is False
|
712
|
+
assert result.error_code == -32015
|
713
|
+
assert "Basic authentication credentials not found" in result.error_message
|
714
|
+
|
715
|
+
def test_fastapi_auth_middleware_basic_auth_wrong_format(self):
|
716
|
+
"""Test basic authentication with wrong Authorization header format."""
|
717
|
+
mock_request = Mock(spec=Request)
|
718
|
+
mock_request.headers = {"Authorization": "Bearer token123"}
|
719
|
+
|
720
|
+
result = self.middleware._try_basic_auth(mock_request)
|
721
|
+
|
722
|
+
assert result.is_valid is False
|
723
|
+
assert result.error_code == -32015
|
724
|
+
assert "Basic authentication credentials not found" in result.error_message
|