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