mcp-security-framework 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_security_framework/__init__.py +96 -0
- mcp_security_framework/cli/__init__.py +18 -0
- mcp_security_framework/cli/cert_cli.py +511 -0
- mcp_security_framework/cli/security_cli.py +791 -0
- mcp_security_framework/constants.py +209 -0
- mcp_security_framework/core/__init__.py +61 -0
- mcp_security_framework/core/auth_manager.py +1011 -0
- mcp_security_framework/core/cert_manager.py +1663 -0
- mcp_security_framework/core/permission_manager.py +735 -0
- mcp_security_framework/core/rate_limiter.py +602 -0
- mcp_security_framework/core/security_manager.py +943 -0
- mcp_security_framework/core/ssl_manager.py +735 -0
- mcp_security_framework/examples/__init__.py +75 -0
- mcp_security_framework/examples/django_example.py +615 -0
- mcp_security_framework/examples/fastapi_example.py +472 -0
- mcp_security_framework/examples/flask_example.py +506 -0
- mcp_security_framework/examples/gateway_example.py +803 -0
- mcp_security_framework/examples/microservice_example.py +690 -0
- mcp_security_framework/examples/standalone_example.py +576 -0
- mcp_security_framework/middleware/__init__.py +250 -0
- mcp_security_framework/middleware/auth_middleware.py +292 -0
- mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
- mcp_security_framework/middleware/fastapi_middleware.py +757 -0
- mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
- mcp_security_framework/middleware/flask_middleware.py +591 -0
- mcp_security_framework/middleware/mtls_middleware.py +439 -0
- mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
- mcp_security_framework/middleware/security_middleware.py +507 -0
- mcp_security_framework/schemas/__init__.py +109 -0
- mcp_security_framework/schemas/config.py +694 -0
- mcp_security_framework/schemas/models.py +709 -0
- mcp_security_framework/schemas/responses.py +686 -0
- mcp_security_framework/tests/__init__.py +0 -0
- mcp_security_framework/utils/__init__.py +121 -0
- mcp_security_framework/utils/cert_utils.py +525 -0
- mcp_security_framework/utils/crypto_utils.py +475 -0
- mcp_security_framework/utils/validation_utils.py +571 -0
- mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
- mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
- mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
- mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_cli/__init__.py +0 -0
- tests/test_cli/test_cert_cli.py +379 -0
- tests/test_cli/test_security_cli.py +657 -0
- tests/test_core/__init__.py +0 -0
- tests/test_core/test_auth_manager.py +582 -0
- tests/test_core/test_cert_manager.py +795 -0
- tests/test_core/test_permission_manager.py +395 -0
- tests/test_core/test_rate_limiter.py +626 -0
- tests/test_core/test_security_manager.py +841 -0
- tests/test_core/test_ssl_manager.py +532 -0
- tests/test_examples/__init__.py +8 -0
- tests/test_examples/test_fastapi_example.py +264 -0
- tests/test_examples/test_flask_example.py +238 -0
- tests/test_examples/test_standalone_example.py +292 -0
- tests/test_integration/__init__.py +0 -0
- tests/test_integration/test_auth_flow.py +502 -0
- tests/test_integration/test_certificate_flow.py +527 -0
- tests/test_integration/test_fastapi_integration.py +341 -0
- tests/test_integration/test_flask_integration.py +398 -0
- tests/test_integration/test_standalone_integration.py +493 -0
- tests/test_middleware/__init__.py +0 -0
- tests/test_middleware/test_fastapi_middleware.py +523 -0
- tests/test_middleware/test_flask_middleware.py +582 -0
- tests/test_middleware/test_security_middleware.py +493 -0
- tests/test_schemas/__init__.py +0 -0
- tests/test_schemas/test_config.py +811 -0
- tests/test_schemas/test_models.py +879 -0
- tests/test_schemas/test_responses.py +1054 -0
- tests/test_schemas/test_serialization.py +493 -0
- tests/test_utils/__init__.py +0 -0
- tests/test_utils/test_cert_utils.py +510 -0
- tests/test_utils/test_crypto_utils.py +603 -0
- tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,465 @@
|
|
1
|
+
"""
|
2
|
+
Flask Authentication Middleware Module
|
3
|
+
|
4
|
+
This module provides Flask-specific authentication middleware implementation
|
5
|
+
for the MCP Security Framework. It handles authentication-only processing
|
6
|
+
for Flask applications.
|
7
|
+
|
8
|
+
Key Features:
|
9
|
+
- Flask-specific request/response handling
|
10
|
+
- Authentication caching and optimization
|
11
|
+
- Framework-specific error responses
|
12
|
+
- Security event logging
|
13
|
+
|
14
|
+
Classes:
|
15
|
+
FlaskAuthMiddleware: Flask authentication middleware implementation
|
16
|
+
|
17
|
+
Dependencies:
|
18
|
+
- flask: For Flask request/response handling
|
19
|
+
- werkzeug: For WSGI utilities
|
20
|
+
|
21
|
+
Author: MCP Security Team
|
22
|
+
Version: 1.0.0
|
23
|
+
License: MIT
|
24
|
+
Created: 2024-01-15
|
25
|
+
Last Modified: 2024-01-20
|
26
|
+
"""
|
27
|
+
|
28
|
+
import json
|
29
|
+
import logging
|
30
|
+
from typing import Any, Dict, Optional
|
31
|
+
|
32
|
+
from flask import Request, Response, jsonify, current_app
|
33
|
+
from werkzeug.exceptions import Unauthorized
|
34
|
+
|
35
|
+
from mcp_security_framework.middleware.auth_middleware import AuthMiddleware
|
36
|
+
from mcp_security_framework.schemas.models import AuthResult, AuthStatus, AuthMethod
|
37
|
+
from mcp_security_framework.schemas.config import SecurityConfig
|
38
|
+
|
39
|
+
|
40
|
+
class FlaskAuthMiddleware(AuthMiddleware):
|
41
|
+
"""
|
42
|
+
Flask Authentication Middleware Implementation
|
43
|
+
|
44
|
+
This class provides Flask-specific authentication middleware that
|
45
|
+
handles authentication-only processing for Flask applications.
|
46
|
+
|
47
|
+
The middleware implements:
|
48
|
+
- Flask request/response handling
|
49
|
+
- Authentication caching and optimization
|
50
|
+
- Framework-specific error responses
|
51
|
+
- Security event logging
|
52
|
+
|
53
|
+
Attributes:
|
54
|
+
config (SecurityConfig): Security configuration settings
|
55
|
+
security_manager: Security manager instance
|
56
|
+
logger (Logger): Logger instance for authentication operations
|
57
|
+
_auth_cache (Dict): Authentication result cache
|
58
|
+
|
59
|
+
Example:
|
60
|
+
>>> from mcp_security_framework.middleware import create_auth_middleware
|
61
|
+
>>> middleware = create_auth_middleware(config, framework="flask")
|
62
|
+
>>> app.wsgi_app = middleware(app.wsgi_app)
|
63
|
+
|
64
|
+
Raises:
|
65
|
+
AuthMiddlewareError: When authentication processing fails
|
66
|
+
"""
|
67
|
+
|
68
|
+
def __init__(self, config: SecurityConfig, security_manager: Any):
|
69
|
+
"""
|
70
|
+
Initialize Flask Authentication Middleware.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
config (SecurityConfig): Security configuration settings
|
74
|
+
security_manager: Security manager instance
|
75
|
+
"""
|
76
|
+
super().__init__(config, security_manager)
|
77
|
+
self.logger = logging.getLogger(__name__)
|
78
|
+
|
79
|
+
def __call__(self, environ: Dict[str, Any], start_response: Any) -> Any:
|
80
|
+
"""
|
81
|
+
Process Flask request through authentication middleware.
|
82
|
+
|
83
|
+
This method implements the authentication-only processing
|
84
|
+
pipeline for Flask requests, focusing solely on user
|
85
|
+
authentication without authorization checks.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
environ (Dict[str, Any]): WSGI environment
|
89
|
+
start_response: WSGI start_response function
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Any: WSGI response
|
93
|
+
|
94
|
+
Raises:
|
95
|
+
AuthMiddlewareError: If authentication processing fails
|
96
|
+
"""
|
97
|
+
try:
|
98
|
+
# Create Flask request object
|
99
|
+
request = Request(environ)
|
100
|
+
|
101
|
+
# Check if path is public (bypasses authentication)
|
102
|
+
if self._is_public_path(request):
|
103
|
+
return self._call_next(environ, start_response)
|
104
|
+
|
105
|
+
# Perform authentication
|
106
|
+
auth_result = self._authenticate_only(request)
|
107
|
+
|
108
|
+
if not auth_result.is_valid:
|
109
|
+
return self._auth_error_response(auth_result, start_response)
|
110
|
+
|
111
|
+
# Add authentication info to request context
|
112
|
+
request.auth_result = auth_result
|
113
|
+
request.username = auth_result.username
|
114
|
+
request.user_roles = auth_result.roles
|
115
|
+
request.auth_method = auth_result.auth_method
|
116
|
+
|
117
|
+
# Process request
|
118
|
+
response = self._call_next(environ, start_response)
|
119
|
+
|
120
|
+
# Log successful authentication
|
121
|
+
self._log_auth_event("authentication_successful", {
|
122
|
+
"ip_address": self._get_client_ip(request),
|
123
|
+
"username": auth_result.username,
|
124
|
+
"path": request.path,
|
125
|
+
"method": request.method,
|
126
|
+
"auth_method": auth_result.auth_method
|
127
|
+
})
|
128
|
+
|
129
|
+
return response
|
130
|
+
|
131
|
+
except Exception as e:
|
132
|
+
self.logger.error(
|
133
|
+
"Flask authentication middleware processing failed",
|
134
|
+
extra={"error": str(e)},
|
135
|
+
exc_info=True
|
136
|
+
)
|
137
|
+
raise AuthMiddlewareError(
|
138
|
+
f"Authentication processing failed: {str(e)}",
|
139
|
+
error_code=-32035
|
140
|
+
)
|
141
|
+
|
142
|
+
def _call_next(self, environ: Dict[str, Any], start_response: Any) -> Any:
|
143
|
+
"""
|
144
|
+
Call the next WSGI application in the chain.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
environ (Dict[str, Any]): WSGI environment
|
148
|
+
start_response: WSGI start_response function
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
Any: WSGI response
|
152
|
+
"""
|
153
|
+
# This would typically call the Flask app
|
154
|
+
# For now, return a simple response
|
155
|
+
status = '200 OK'
|
156
|
+
headers = [('Content-Type', 'application/json')]
|
157
|
+
start_response(status, headers)
|
158
|
+
return [json.dumps({"message": "OK"}).encode()]
|
159
|
+
|
160
|
+
def _is_public_path(self, request: Request) -> bool:
|
161
|
+
"""
|
162
|
+
Check if the request path is public (bypasses authentication).
|
163
|
+
|
164
|
+
Args:
|
165
|
+
request (Request): Flask request object
|
166
|
+
|
167
|
+
Returns:
|
168
|
+
bool: True if path is public, False otherwise
|
169
|
+
"""
|
170
|
+
path = request.path
|
171
|
+
|
172
|
+
# Check configured public paths
|
173
|
+
if hasattr(self.config.auth, 'public_paths'):
|
174
|
+
for public_path in self.config.auth.public_paths:
|
175
|
+
if path.startswith(public_path):
|
176
|
+
return True
|
177
|
+
|
178
|
+
# Check common public paths
|
179
|
+
public_paths = ["/health", "/status", "/metrics", "/docs", "/openapi.json"]
|
180
|
+
return any(path.startswith(public_path) for public_path in public_paths)
|
181
|
+
|
182
|
+
def _get_client_ip(self, request: Request) -> str:
|
183
|
+
"""
|
184
|
+
Get client IP address from Flask request.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
request (Request): Flask request object
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
str: Client IP address
|
191
|
+
"""
|
192
|
+
# Try X-Forwarded-For header
|
193
|
+
forwarded_for = request.headers.get("X-Forwarded-For")
|
194
|
+
if forwarded_for:
|
195
|
+
return forwarded_for.split(",")[0].strip()
|
196
|
+
|
197
|
+
# Try X-Real-IP header
|
198
|
+
real_ip = request.headers.get("X-Real-IP")
|
199
|
+
if real_ip:
|
200
|
+
return real_ip
|
201
|
+
|
202
|
+
# Use remote address
|
203
|
+
if request.remote_addr:
|
204
|
+
return request.remote_addr
|
205
|
+
|
206
|
+
# Fallback to default IP from config or environment
|
207
|
+
default_ip = getattr(self.config, 'default_client_ip', None)
|
208
|
+
if default_ip:
|
209
|
+
return default_ip
|
210
|
+
|
211
|
+
# Use environment variable or default
|
212
|
+
import os
|
213
|
+
from ..constants import DEFAULT_CLIENT_IP
|
214
|
+
return os.environ.get('DEFAULT_CLIENT_IP', DEFAULT_CLIENT_IP)
|
215
|
+
|
216
|
+
def _get_cache_key(self, request: Request) -> str:
|
217
|
+
"""
|
218
|
+
Generate cache key for authentication result.
|
219
|
+
|
220
|
+
Args:
|
221
|
+
request (Request): Flask request object
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
str: Cache key
|
225
|
+
"""
|
226
|
+
# Use combination of IP and user agent for cache key
|
227
|
+
ip = self._get_client_ip(request)
|
228
|
+
user_agent = request.headers.get("User-Agent", "")
|
229
|
+
return f"auth:{ip}:{hash(user_agent)}"
|
230
|
+
|
231
|
+
def _try_auth_method(self, request: Request, method: str) -> AuthResult:
|
232
|
+
"""
|
233
|
+
Try authentication using specific method with Flask request.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
request (Request): Flask request object
|
237
|
+
method (str): Authentication method to try
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
AuthResult: Authentication result
|
241
|
+
"""
|
242
|
+
try:
|
243
|
+
if method == "api_key":
|
244
|
+
return self._try_api_key_auth(request)
|
245
|
+
elif method == "jwt":
|
246
|
+
return self._try_jwt_auth(request)
|
247
|
+
elif method == "certificate":
|
248
|
+
return self._try_certificate_auth(request)
|
249
|
+
elif method == "basic":
|
250
|
+
return self._try_basic_auth(request)
|
251
|
+
else:
|
252
|
+
return AuthResult(
|
253
|
+
is_valid=False,
|
254
|
+
status=AuthStatus.FAILED,
|
255
|
+
username=None,
|
256
|
+
roles=[],
|
257
|
+
auth_method=None,
|
258
|
+
error_code=-32022,
|
259
|
+
error_message=f"Unsupported authentication method: {method}"
|
260
|
+
)
|
261
|
+
except Exception as e:
|
262
|
+
self.logger.error(
|
263
|
+
f"Authentication method {method} failed",
|
264
|
+
extra={"error": str(e)},
|
265
|
+
exc_info=True
|
266
|
+
)
|
267
|
+
return AuthResult(
|
268
|
+
is_valid=False,
|
269
|
+
status=AuthStatus.FAILED,
|
270
|
+
username=None,
|
271
|
+
roles=[],
|
272
|
+
auth_method=None,
|
273
|
+
error_code=-32023,
|
274
|
+
error_message=f"Authentication method {method} failed: {str(e)}"
|
275
|
+
)
|
276
|
+
|
277
|
+
def _try_api_key_auth(self, request: Request) -> AuthResult:
|
278
|
+
"""
|
279
|
+
Try API key authentication with Flask request.
|
280
|
+
|
281
|
+
Args:
|
282
|
+
request (Request): Flask request object
|
283
|
+
|
284
|
+
Returns:
|
285
|
+
AuthResult: Authentication result
|
286
|
+
"""
|
287
|
+
# Try to get API key from headers
|
288
|
+
api_key = request.headers.get("X-API-Key")
|
289
|
+
if not api_key:
|
290
|
+
# Try Authorization header
|
291
|
+
auth_header = request.headers.get("Authorization")
|
292
|
+
if auth_header and auth_header.startswith("Bearer "):
|
293
|
+
api_key = auth_header[7:] # Remove "Bearer " prefix
|
294
|
+
|
295
|
+
if not api_key:
|
296
|
+
return AuthResult(
|
297
|
+
is_valid=False,
|
298
|
+
status=AuthStatus.FAILED,
|
299
|
+
username=None,
|
300
|
+
roles=[],
|
301
|
+
auth_method=AuthMethod.API_KEY,
|
302
|
+
error_code=-32012,
|
303
|
+
error_message="API key not found in request"
|
304
|
+
)
|
305
|
+
|
306
|
+
# Authenticate using security manager
|
307
|
+
return self.security_manager.auth_manager.authenticate_api_key(api_key)
|
308
|
+
|
309
|
+
def _try_jwt_auth(self, request: Request) -> AuthResult:
|
310
|
+
"""
|
311
|
+
Try JWT authentication with Flask request.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
request (Request): Flask request object
|
315
|
+
|
316
|
+
Returns:
|
317
|
+
AuthResult: Authentication result
|
318
|
+
"""
|
319
|
+
# Try to get JWT token from Authorization header
|
320
|
+
auth_header = request.headers.get("Authorization")
|
321
|
+
if not auth_header or not auth_header.startswith("Bearer "):
|
322
|
+
return AuthResult(
|
323
|
+
is_valid=False,
|
324
|
+
status=AuthStatus.FAILED,
|
325
|
+
username=None,
|
326
|
+
roles=[],
|
327
|
+
auth_method=AuthMethod.JWT,
|
328
|
+
error_code=-32013,
|
329
|
+
error_message="JWT token not found in Authorization header"
|
330
|
+
)
|
331
|
+
|
332
|
+
token = auth_header[7:] # Remove "Bearer " prefix
|
333
|
+
|
334
|
+
# Authenticate using security manager
|
335
|
+
return self.security_manager.auth_manager.authenticate_jwt_token(token)
|
336
|
+
|
337
|
+
def _try_certificate_auth(self, request: Request) -> AuthResult:
|
338
|
+
"""
|
339
|
+
Try certificate authentication with Flask request.
|
340
|
+
|
341
|
+
Args:
|
342
|
+
request (Request): Flask request object
|
343
|
+
|
344
|
+
Returns:
|
345
|
+
AuthResult: Authentication result
|
346
|
+
"""
|
347
|
+
# Certificate authentication is typically handled at the SSL/TLS level
|
348
|
+
# This method would extract certificate information from the request
|
349
|
+
# For now, return not implemented
|
350
|
+
return AuthResult(
|
351
|
+
is_valid=False,
|
352
|
+
status=AuthStatus.FAILED,
|
353
|
+
username=None,
|
354
|
+
roles=[],
|
355
|
+
auth_method=AuthMethod.CERTIFICATE,
|
356
|
+
error_code=-32014,
|
357
|
+
error_message="Certificate authentication not implemented"
|
358
|
+
)
|
359
|
+
|
360
|
+
def _try_basic_auth(self, request: Request) -> AuthResult:
|
361
|
+
"""
|
362
|
+
Try basic authentication with Flask request.
|
363
|
+
|
364
|
+
Args:
|
365
|
+
request (Request): Flask request object
|
366
|
+
|
367
|
+
Returns:
|
368
|
+
AuthResult: Authentication result
|
369
|
+
"""
|
370
|
+
# Try to get basic auth credentials from Authorization header
|
371
|
+
auth_header = request.headers.get("Authorization")
|
372
|
+
if not auth_header or not auth_header.startswith("Basic "):
|
373
|
+
return AuthResult(
|
374
|
+
is_valid=False,
|
375
|
+
status=AuthStatus.FAILED,
|
376
|
+
username=None,
|
377
|
+
roles=[],
|
378
|
+
auth_method=AuthMethod.BASIC,
|
379
|
+
error_code=-32015,
|
380
|
+
error_message="Basic authentication credentials not found"
|
381
|
+
)
|
382
|
+
|
383
|
+
# Basic auth is not implemented in this version
|
384
|
+
return AuthResult(
|
385
|
+
is_valid=False,
|
386
|
+
status=AuthStatus.FAILED,
|
387
|
+
username=None,
|
388
|
+
roles=[],
|
389
|
+
auth_method=AuthMethod.BASIC,
|
390
|
+
error_code=-32016,
|
391
|
+
error_message="Basic authentication not implemented"
|
392
|
+
)
|
393
|
+
|
394
|
+
def _auth_error_response(self, auth_result: AuthResult, start_response: Any) -> Any:
|
395
|
+
"""
|
396
|
+
Create authentication error response for Flask.
|
397
|
+
|
398
|
+
Args:
|
399
|
+
auth_result (AuthResult): Authentication result with error
|
400
|
+
start_response: WSGI start_response function
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
Any: WSGI error response
|
404
|
+
"""
|
405
|
+
error_data = {
|
406
|
+
"error": "Authentication failed",
|
407
|
+
"error_code": auth_result.error_code,
|
408
|
+
"error_message": auth_result.error_message,
|
409
|
+
"auth_method": auth_result.auth_method.value if auth_result.auth_method else None
|
410
|
+
}
|
411
|
+
|
412
|
+
status = '401 Unauthorized'
|
413
|
+
headers = [
|
414
|
+
('Content-Type', 'application/json'),
|
415
|
+
('WWW-Authenticate', 'Bearer')
|
416
|
+
]
|
417
|
+
|
418
|
+
start_response(status, headers)
|
419
|
+
return [json.dumps(error_data).encode()]
|
420
|
+
|
421
|
+
def _log_auth_event(self, event_type: str, details: Dict[str, Any]) -> None:
|
422
|
+
"""
|
423
|
+
Log authentication event.
|
424
|
+
|
425
|
+
Args:
|
426
|
+
event_type (str): Type of authentication event
|
427
|
+
details (Dict[str, Any]): Event details
|
428
|
+
"""
|
429
|
+
try:
|
430
|
+
self.logger.info(
|
431
|
+
f"Authentication event: {event_type}",
|
432
|
+
extra={
|
433
|
+
"event_type": event_type,
|
434
|
+
"timestamp": details.get("timestamp"),
|
435
|
+
"ip_address": details.get("ip_address"),
|
436
|
+
"username": details.get("username"),
|
437
|
+
"path": details.get("path"),
|
438
|
+
"method": details.get("method"),
|
439
|
+
"auth_method": details.get("auth_method"),
|
440
|
+
**details
|
441
|
+
}
|
442
|
+
)
|
443
|
+
except Exception as e:
|
444
|
+
self.logger.error(
|
445
|
+
"Failed to log authentication event",
|
446
|
+
extra={"error": str(e)},
|
447
|
+
exc_info=True
|
448
|
+
)
|
449
|
+
|
450
|
+
|
451
|
+
class AuthMiddlewareError(Exception):
|
452
|
+
"""
|
453
|
+
Authentication Middleware Error
|
454
|
+
|
455
|
+
This exception is raised when authentication middleware processing fails.
|
456
|
+
|
457
|
+
Attributes:
|
458
|
+
message (str): Error message
|
459
|
+
error_code (int): Error code for programmatic handling
|
460
|
+
"""
|
461
|
+
|
462
|
+
def __init__(self, message: str, error_code: int = -32030):
|
463
|
+
self.message = message
|
464
|
+
self.error_code = error_code
|
465
|
+
super().__init__(self.message)
|