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