mcp-security-framework 1.1.0__py3-none-any.whl → 1.1.2__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/__init__.py +26 -15
- mcp_security_framework/cli/__init__.py +1 -1
- mcp_security_framework/cli/cert_cli.py +233 -197
- mcp_security_framework/cli/security_cli.py +324 -234
- mcp_security_framework/constants.py +21 -27
- mcp_security_framework/core/auth_manager.py +41 -22
- mcp_security_framework/core/cert_manager.py +210 -147
- mcp_security_framework/core/permission_manager.py +9 -9
- mcp_security_framework/core/rate_limiter.py +2 -2
- mcp_security_framework/core/security_manager.py +284 -229
- mcp_security_framework/examples/__init__.py +6 -0
- mcp_security_framework/examples/comprehensive_example.py +349 -279
- mcp_security_framework/examples/django_example.py +247 -206
- mcp_security_framework/examples/fastapi_example.py +315 -283
- mcp_security_framework/examples/flask_example.py +274 -203
- mcp_security_framework/examples/gateway_example.py +304 -237
- mcp_security_framework/examples/microservice_example.py +258 -189
- mcp_security_framework/examples/standalone_example.py +255 -230
- mcp_security_framework/examples/test_all_examples.py +151 -135
- mcp_security_framework/middleware/__init__.py +46 -55
- mcp_security_framework/middleware/auth_middleware.py +62 -63
- mcp_security_framework/middleware/fastapi_auth_middleware.py +119 -118
- mcp_security_framework/middleware/fastapi_middleware.py +156 -148
- mcp_security_framework/middleware/flask_auth_middleware.py +160 -147
- mcp_security_framework/middleware/flask_middleware.py +183 -157
- mcp_security_framework/middleware/mtls_middleware.py +106 -117
- mcp_security_framework/middleware/rate_limit_middleware.py +105 -101
- mcp_security_framework/middleware/security_middleware.py +109 -124
- mcp_security_framework/schemas/config.py +2 -1
- mcp_security_framework/schemas/models.py +18 -6
- mcp_security_framework/utils/cert_utils.py +14 -8
- mcp_security_framework/utils/datetime_compat.py +116 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/METADATA +4 -3
- mcp_security_framework-1.1.2.dist-info/RECORD +84 -0
- tests/conftest.py +63 -66
- tests/test_cli/test_cert_cli.py +184 -146
- tests/test_cli/test_security_cli.py +274 -247
- tests/test_core/test_cert_manager.py +24 -10
- tests/test_core/test_security_manager.py +2 -2
- tests/test_examples/test_comprehensive_example.py +190 -137
- tests/test_examples/test_fastapi_example.py +124 -101
- tests/test_examples/test_flask_example.py +124 -101
- tests/test_examples/test_standalone_example.py +73 -80
- tests/test_integration/test_auth_flow.py +213 -197
- tests/test_integration/test_certificate_flow.py +180 -149
- tests/test_integration/test_fastapi_integration.py +108 -111
- tests/test_integration/test_flask_integration.py +141 -140
- tests/test_integration/test_standalone_integration.py +290 -259
- tests/test_middleware/test_fastapi_auth_middleware.py +195 -174
- tests/test_middleware/test_fastapi_middleware.py +147 -132
- tests/test_middleware/test_flask_auth_middleware.py +260 -202
- tests/test_middleware/test_flask_middleware.py +201 -179
- tests/test_middleware/test_security_middleware.py +145 -130
- tests/test_utils/test_datetime_compat.py +147 -0
- mcp_security_framework-1.1.0.dist-info/RECORD +0 -82
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/WHEEL +0 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.2.dist-info}/top_level.txt +0 -0
@@ -11,23 +11,25 @@ License: MIT
|
|
11
11
|
"""
|
12
12
|
|
13
13
|
import json
|
14
|
-
import
|
15
|
-
from unittest.mock import Mock, patch
|
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,
|
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(
|
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,
|
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(
|
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
|
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
|
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
|
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
|
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(
|
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
|
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,
|
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,
|
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
|
-
|
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,
|
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
|
-
|
493
|
-
self.mock_auth_manager.authenticate_api_key.side_effect = Exception(
|
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,
|
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(
|
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(
|
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
|