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,1011 @@
|
|
1
|
+
"""
|
2
|
+
Authentication Manager Module
|
3
|
+
|
4
|
+
This module provides comprehensive authentication management for the
|
5
|
+
MCP Security Framework. It handles multiple authentication methods,
|
6
|
+
JWT token management, and integration with permission management.
|
7
|
+
|
8
|
+
Key Features:
|
9
|
+
- Multiple authentication methods (API key, JWT, certificate)
|
10
|
+
- JWT token creation and validation
|
11
|
+
- Integration with PermissionManager for role extraction
|
12
|
+
- Secure credential validation
|
13
|
+
- Authentication result management
|
14
|
+
- Session management utilities
|
15
|
+
|
16
|
+
Classes:
|
17
|
+
AuthManager: Main authentication management class
|
18
|
+
AuthResult: Authentication result container
|
19
|
+
JWTManager: JWT token management utilities
|
20
|
+
|
21
|
+
Author: MCP Security Team
|
22
|
+
Version: 1.0.0
|
23
|
+
License: MIT
|
24
|
+
"""
|
25
|
+
|
26
|
+
import logging
|
27
|
+
import time
|
28
|
+
from datetime import datetime, timedelta, timezone
|
29
|
+
from typing import Dict, List, Optional, Union, Any
|
30
|
+
|
31
|
+
import jwt
|
32
|
+
from cryptography import x509
|
33
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
34
|
+
|
35
|
+
from ..core.permission_manager import PermissionManager
|
36
|
+
from ..schemas.config import AuthConfig
|
37
|
+
from ..schemas.models import AuthResult, AuthStatus, ValidationResult
|
38
|
+
from ..utils.cert_utils import (
|
39
|
+
extract_permissions_from_certificate,
|
40
|
+
extract_roles_from_certificate,
|
41
|
+
parse_certificate,
|
42
|
+
validate_certificate_chain,
|
43
|
+
)
|
44
|
+
from ..utils.crypto_utils import (
|
45
|
+
generate_secure_token,
|
46
|
+
hash_password,
|
47
|
+
validate_api_key_format,
|
48
|
+
verify_password,
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
class AuthManager:
|
53
|
+
"""
|
54
|
+
Authentication Manager Class
|
55
|
+
|
56
|
+
This class provides comprehensive authentication management including
|
57
|
+
multiple authentication methods, JWT token management, and integration
|
58
|
+
with permission management.
|
59
|
+
|
60
|
+
The AuthManager handles:
|
61
|
+
- API key authentication with secure validation
|
62
|
+
- JWT token creation, validation, and management
|
63
|
+
- Certificate-based authentication with role extraction
|
64
|
+
- Integration with PermissionManager for role validation
|
65
|
+
- Secure credential storage and validation
|
66
|
+
- Session management and token refresh
|
67
|
+
- Authentication result management with detailed status
|
68
|
+
|
69
|
+
Attributes:
|
70
|
+
config (AuthConfig): Authentication configuration settings
|
71
|
+
logger (Logger): Logger instance for authentication operations
|
72
|
+
permission_manager (PermissionManager): Permission manager instance
|
73
|
+
_api_keys (Dict): Stored API keys for validation
|
74
|
+
_jwt_secret (str): JWT signing secret
|
75
|
+
_token_cache (Dict): Cache for validated tokens
|
76
|
+
_session_store (Dict): Active session storage
|
77
|
+
|
78
|
+
Example:
|
79
|
+
>>> config = AuthConfig(api_keys={"user1": "key123"}, jwt_secret="secret")
|
80
|
+
>>> auth_manager = AuthManager(config, permission_manager)
|
81
|
+
>>> result = auth_manager.authenticate_api_key("key123")
|
82
|
+
|
83
|
+
Raises:
|
84
|
+
AuthenticationConfigurationError: When authentication configuration is invalid
|
85
|
+
AuthenticationError: When authentication fails
|
86
|
+
JWTValidationError: When JWT token validation fails
|
87
|
+
"""
|
88
|
+
|
89
|
+
def __init__(self, config: AuthConfig, permission_manager: PermissionManager):
|
90
|
+
"""
|
91
|
+
Initialize Authentication Manager.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
config (AuthConfig): Authentication configuration settings containing
|
95
|
+
API keys, JWT settings, and authentication parameters.
|
96
|
+
Must be a valid AuthConfig instance with proper authentication
|
97
|
+
settings and security parameters.
|
98
|
+
permission_manager (PermissionManager): Permission manager instance
|
99
|
+
for role and permission validation. Must be a valid
|
100
|
+
PermissionManager instance.
|
101
|
+
|
102
|
+
Raises:
|
103
|
+
AuthenticationConfigurationError: If configuration is invalid or
|
104
|
+
permission manager is not provided.
|
105
|
+
|
106
|
+
Example:
|
107
|
+
>>> config = AuthConfig(api_keys={"user1": "key123"})
|
108
|
+
>>> perm_manager = PermissionManager(perm_config)
|
109
|
+
>>> auth_manager = AuthManager(config, perm_manager)
|
110
|
+
"""
|
111
|
+
if not config:
|
112
|
+
raise AuthenticationConfigurationError(
|
113
|
+
"Authentication configuration is required"
|
114
|
+
)
|
115
|
+
|
116
|
+
if not permission_manager:
|
117
|
+
raise AuthenticationConfigurationError("Permission manager is required")
|
118
|
+
|
119
|
+
self.config = config
|
120
|
+
self.permission_manager = permission_manager
|
121
|
+
self.logger = logging.getLogger(__name__)
|
122
|
+
|
123
|
+
# Initialize storage
|
124
|
+
# Convert API keys from "key": "user" format to "user": "key" format
|
125
|
+
# or handle new format "key": {"username": "user", "roles": ["role1", "role2"]}
|
126
|
+
if config.api_keys:
|
127
|
+
self._api_keys = {}
|
128
|
+
self._api_key_metadata = {}
|
129
|
+
for key, value in config.api_keys.items():
|
130
|
+
if isinstance(value, str):
|
131
|
+
# Old format: "key": "user"
|
132
|
+
self._api_keys[value] = key
|
133
|
+
elif isinstance(value, dict):
|
134
|
+
# New format: "key": {"username": "user", "roles": ["role1", "role2"]}
|
135
|
+
username = value.get("username", key)
|
136
|
+
self._api_keys[username] = key
|
137
|
+
self._api_key_metadata[key] = value
|
138
|
+
else:
|
139
|
+
self.logger.warning(f"Invalid API key format for key {key}: {value}")
|
140
|
+
else:
|
141
|
+
self._api_keys = {}
|
142
|
+
self._api_key_metadata = {}
|
143
|
+
self._jwt_secret = (
|
144
|
+
config.jwt_secret.get_secret_value()
|
145
|
+
if config.jwt_secret
|
146
|
+
else generate_secure_token(32)
|
147
|
+
)
|
148
|
+
self._token_cache: Dict[str, Dict] = {}
|
149
|
+
self._session_store: Dict[str, Dict] = {}
|
150
|
+
|
151
|
+
# Validate configuration
|
152
|
+
self._validate_configuration()
|
153
|
+
|
154
|
+
self.logger.info(
|
155
|
+
"AuthManager initialized successfully",
|
156
|
+
extra={
|
157
|
+
"api_keys_count": len(self._api_keys),
|
158
|
+
"jwt_enabled": bool(self._jwt_secret),
|
159
|
+
"jwt_expiry_hours": config.jwt_expiry_hours,
|
160
|
+
},
|
161
|
+
)
|
162
|
+
|
163
|
+
def authenticate_api_key(self, api_key: str) -> AuthResult:
|
164
|
+
"""
|
165
|
+
Authenticate user using API key.
|
166
|
+
|
167
|
+
This method validates an API key against the configured key store and
|
168
|
+
returns authentication result with user information and permissions.
|
169
|
+
|
170
|
+
The method performs comprehensive API key validation including:
|
171
|
+
- Key format validation and sanitization
|
172
|
+
- Key existence verification in configured store
|
173
|
+
- User role and permission extraction
|
174
|
+
- Authentication result generation with metadata
|
175
|
+
|
176
|
+
Args:
|
177
|
+
api_key (str): API key string to validate. Must be a non-empty
|
178
|
+
string containing the API key value. The key should be
|
179
|
+
provided in the format expected by the authentication system.
|
180
|
+
Valid keys are typically 32-64 character alphanumeric strings.
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
AuthResult: Authentication result object containing:
|
184
|
+
- is_valid (bool): True if authentication succeeded
|
185
|
+
- username (str): Authenticated username if valid
|
186
|
+
- roles (List[str]): User roles and permissions
|
187
|
+
- auth_method (str): Authentication method used ("api_key")
|
188
|
+
- error_code (int): Error code if authentication failed
|
189
|
+
- error_message (str): Human-readable error message
|
190
|
+
|
191
|
+
Raises:
|
192
|
+
AuthenticationError: When API key is invalid, expired, or
|
193
|
+
authentication process fails
|
194
|
+
AuthenticationConfigurationError: When authentication configuration
|
195
|
+
is invalid or missing
|
196
|
+
ValueError: When API key parameter is empty or malformed
|
197
|
+
|
198
|
+
Example:
|
199
|
+
>>> auth_manager = AuthManager(config, permission_manager)
|
200
|
+
>>> result = auth_manager.authenticate_api_key("user_api_key_123")
|
201
|
+
>>> if result.is_valid:
|
202
|
+
... print(f"Authenticated user: {result.username}")
|
203
|
+
... print(f"User roles: {result.roles}")
|
204
|
+
>>> else:
|
205
|
+
... print(f"Authentication failed: {result.error_message}")
|
206
|
+
|
207
|
+
Note:
|
208
|
+
This method is thread-safe and can be called concurrently.
|
209
|
+
API keys are validated against the in-memory key store for
|
210
|
+
performance. For production use, consider implementing
|
211
|
+
persistent key storage with proper encryption.
|
212
|
+
|
213
|
+
See Also:
|
214
|
+
authenticate_jwt_token: JWT token authentication method
|
215
|
+
authenticate_certificate: Certificate-based authentication
|
216
|
+
"""
|
217
|
+
try:
|
218
|
+
# Validate input
|
219
|
+
if not api_key:
|
220
|
+
return AuthResult(
|
221
|
+
is_valid=False,
|
222
|
+
status=AuthStatus.INVALID,
|
223
|
+
auth_method="api_key",
|
224
|
+
error_code=-32001,
|
225
|
+
error_message="API key is required",
|
226
|
+
)
|
227
|
+
|
228
|
+
# Validate API key format
|
229
|
+
if not validate_api_key_format(api_key):
|
230
|
+
return AuthResult(
|
231
|
+
is_valid=False,
|
232
|
+
status=AuthStatus.INVALID,
|
233
|
+
auth_method="api_key",
|
234
|
+
error_code=-32002,
|
235
|
+
error_message="Invalid API key format",
|
236
|
+
)
|
237
|
+
|
238
|
+
# Find user by API key
|
239
|
+
username = None
|
240
|
+
user_roles = []
|
241
|
+
for user, api_key_in_config in self._api_keys.items():
|
242
|
+
if api_key_in_config == api_key:
|
243
|
+
username = user
|
244
|
+
# Check if we have metadata for this API key
|
245
|
+
if api_key in self._api_key_metadata:
|
246
|
+
metadata = self._api_key_metadata[api_key]
|
247
|
+
user_roles = metadata.get("roles", [])
|
248
|
+
break
|
249
|
+
|
250
|
+
if not username:
|
251
|
+
return AuthResult(
|
252
|
+
is_valid=False,
|
253
|
+
status=AuthStatus.INVALID,
|
254
|
+
auth_method="api_key",
|
255
|
+
error_code=-32003,
|
256
|
+
error_message="Invalid API key",
|
257
|
+
)
|
258
|
+
|
259
|
+
# If we don't have roles from metadata, get them from permission manager
|
260
|
+
if not user_roles:
|
261
|
+
try:
|
262
|
+
user_roles = self._get_user_roles(username)
|
263
|
+
except Exception as e:
|
264
|
+
self.logger.error(
|
265
|
+
"Failed to get user roles",
|
266
|
+
extra={"username": username, "error": str(e)},
|
267
|
+
)
|
268
|
+
return AuthResult(
|
269
|
+
is_valid=False,
|
270
|
+
status=AuthStatus.FAILED,
|
271
|
+
auth_method="api_key",
|
272
|
+
error_code=-32004,
|
273
|
+
error_message="Failed to retrieve user roles",
|
274
|
+
)
|
275
|
+
|
276
|
+
# Get user permissions from permission manager
|
277
|
+
user_permissions = set()
|
278
|
+
if self.permission_manager:
|
279
|
+
try:
|
280
|
+
permissions_result = self.permission_manager.get_effective_permissions(user_roles)
|
281
|
+
# Handle both set and mock objects
|
282
|
+
if hasattr(permissions_result, '__iter__') and not isinstance(permissions_result, str):
|
283
|
+
user_permissions = set(permissions_result)
|
284
|
+
else:
|
285
|
+
user_permissions = set()
|
286
|
+
except Exception as e:
|
287
|
+
self.logger.warning(
|
288
|
+
"Failed to get user permissions",
|
289
|
+
extra={"username": username, "roles": user_roles, "error": str(e)},
|
290
|
+
)
|
291
|
+
|
292
|
+
# Create successful authentication result
|
293
|
+
auth_result = AuthResult(
|
294
|
+
is_valid=True,
|
295
|
+
status=AuthStatus.SUCCESS,
|
296
|
+
username=username,
|
297
|
+
roles=user_roles,
|
298
|
+
permissions=user_permissions,
|
299
|
+
auth_method="api_key",
|
300
|
+
auth_timestamp=datetime.now(timezone.utc),
|
301
|
+
)
|
302
|
+
|
303
|
+
self.logger.info(
|
304
|
+
"API key authentication successful",
|
305
|
+
extra={
|
306
|
+
"username": username,
|
307
|
+
"roles": user_roles,
|
308
|
+
"auth_method": "api_key",
|
309
|
+
},
|
310
|
+
)
|
311
|
+
|
312
|
+
return auth_result
|
313
|
+
|
314
|
+
except Exception as e:
|
315
|
+
self.logger.error(
|
316
|
+
"API key authentication failed",
|
317
|
+
extra={
|
318
|
+
"api_key": api_key[:8] + "..." if api_key else None,
|
319
|
+
"error": str(e),
|
320
|
+
},
|
321
|
+
)
|
322
|
+
raise AuthenticationError(f"API key authentication failed: {str(e)}")
|
323
|
+
|
324
|
+
def authenticate_jwt_token(self, token: str) -> AuthResult:
|
325
|
+
"""
|
326
|
+
Authenticate user using JWT token.
|
327
|
+
|
328
|
+
This method validates a JWT token and extracts user information
|
329
|
+
and permissions from the token payload.
|
330
|
+
|
331
|
+
The method performs comprehensive JWT validation including:
|
332
|
+
- Token format and structure validation
|
333
|
+
- Signature verification using configured secret
|
334
|
+
- Token expiration and validity checks
|
335
|
+
- Payload extraction and validation
|
336
|
+
- User role and permission extraction
|
337
|
+
|
338
|
+
Args:
|
339
|
+
token (str): JWT token string to validate. Must be a valid
|
340
|
+
JWT token format with proper signature and payload.
|
341
|
+
The token should contain user information and roles.
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
AuthResult: Authentication result object containing:
|
345
|
+
- is_valid (bool): True if authentication succeeded
|
346
|
+
- username (str): Authenticated username if valid
|
347
|
+
- roles (List[str]): User roles from token
|
348
|
+
- auth_method (str): Authentication method used ("jwt")
|
349
|
+
- error_code (int): Error code if authentication failed
|
350
|
+
- error_message (str): Human-readable error message
|
351
|
+
|
352
|
+
Raises:
|
353
|
+
JWTValidationError: When JWT token is invalid, expired, or
|
354
|
+
validation process fails
|
355
|
+
AuthenticationError: When authentication process fails
|
356
|
+
ValueError: When token parameter is empty or malformed
|
357
|
+
|
358
|
+
Example:
|
359
|
+
>>> auth_manager = AuthManager(config, permission_manager)
|
360
|
+
>>> result = auth_manager.authenticate_jwt_token("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...")
|
361
|
+
>>> if result.is_valid:
|
362
|
+
... print(f"Authenticated user: {result.username}")
|
363
|
+
... print(f"Token expires: {result.expires_at}")
|
364
|
+
>>> else:
|
365
|
+
... print(f"JWT validation failed: {result.error_message}")
|
366
|
+
|
367
|
+
Note:
|
368
|
+
This method validates JWT tokens using the configured secret.
|
369
|
+
Token expiration is automatically checked. For production use,
|
370
|
+
consider implementing token refresh mechanisms.
|
371
|
+
|
372
|
+
See Also:
|
373
|
+
create_jwt_token: JWT token creation method
|
374
|
+
authenticate_api_key: API key authentication method
|
375
|
+
"""
|
376
|
+
try:
|
377
|
+
# Validate input
|
378
|
+
if not token:
|
379
|
+
return AuthResult(
|
380
|
+
is_valid=False,
|
381
|
+
status=AuthStatus.INVALID,
|
382
|
+
auth_method="jwt",
|
383
|
+
error_code=-32001,
|
384
|
+
error_message="JWT token is required",
|
385
|
+
)
|
386
|
+
|
387
|
+
# Check token cache
|
388
|
+
if token in self._token_cache:
|
389
|
+
cached_result = self._token_cache[token]
|
390
|
+
if not self._is_token_expired(cached_result):
|
391
|
+
return cached_result
|
392
|
+
else:
|
393
|
+
# Remove expired token from cache
|
394
|
+
del self._token_cache[token]
|
395
|
+
|
396
|
+
# Decode and validate JWT token
|
397
|
+
try:
|
398
|
+
payload = jwt.decode(
|
399
|
+
token,
|
400
|
+
self._jwt_secret,
|
401
|
+
algorithms=["HS256"],
|
402
|
+
options={
|
403
|
+
"verify_signature": True,
|
404
|
+
"verify_exp": True,
|
405
|
+
"verify_iat": True,
|
406
|
+
"verify_aud": False,
|
407
|
+
},
|
408
|
+
)
|
409
|
+
except jwt.ExpiredSignatureError:
|
410
|
+
return AuthResult(
|
411
|
+
is_valid=False,
|
412
|
+
status=AuthStatus.EXPIRED,
|
413
|
+
auth_method="jwt",
|
414
|
+
error_code=-32002,
|
415
|
+
error_message="JWT token has expired",
|
416
|
+
)
|
417
|
+
except jwt.InvalidTokenError as e:
|
418
|
+
return AuthResult(
|
419
|
+
is_valid=False,
|
420
|
+
status=AuthStatus.INVALID,
|
421
|
+
auth_method="jwt",
|
422
|
+
error_code=-32003,
|
423
|
+
error_message=f"Invalid JWT token: {str(e)}",
|
424
|
+
)
|
425
|
+
|
426
|
+
# Extract user information from payload
|
427
|
+
username = payload.get("username")
|
428
|
+
if not username:
|
429
|
+
return AuthResult(
|
430
|
+
is_valid=False,
|
431
|
+
status=AuthStatus.INVALID,
|
432
|
+
auth_method="jwt",
|
433
|
+
error_code=-32004,
|
434
|
+
error_message="JWT token missing username",
|
435
|
+
)
|
436
|
+
|
437
|
+
# Extract roles from payload
|
438
|
+
roles = payload.get("roles", [])
|
439
|
+
if not isinstance(roles, list):
|
440
|
+
roles = []
|
441
|
+
|
442
|
+
# Extract expiration time
|
443
|
+
expires_at = None
|
444
|
+
if "exp" in payload:
|
445
|
+
expires_at = datetime.fromtimestamp(payload["exp"], tz=timezone.utc)
|
446
|
+
|
447
|
+
# Get user permissions from permission manager
|
448
|
+
user_permissions = set()
|
449
|
+
if self.permission_manager:
|
450
|
+
try:
|
451
|
+
permissions_result = self.permission_manager.get_effective_permissions(roles)
|
452
|
+
# Handle both set and mock objects
|
453
|
+
if hasattr(permissions_result, '__iter__') and not isinstance(permissions_result, str):
|
454
|
+
user_permissions = set(permissions_result)
|
455
|
+
else:
|
456
|
+
user_permissions = set()
|
457
|
+
except Exception as e:
|
458
|
+
self.logger.warning(
|
459
|
+
"Failed to get user permissions",
|
460
|
+
extra={"username": username, "roles": roles, "error": str(e)},
|
461
|
+
)
|
462
|
+
|
463
|
+
# Create successful authentication result
|
464
|
+
auth_result = AuthResult(
|
465
|
+
is_valid=True,
|
466
|
+
status=AuthStatus.SUCCESS,
|
467
|
+
username=username,
|
468
|
+
roles=roles,
|
469
|
+
permissions=user_permissions,
|
470
|
+
auth_method="jwt",
|
471
|
+
auth_timestamp=datetime.now(timezone.utc),
|
472
|
+
token_expiry=expires_at,
|
473
|
+
)
|
474
|
+
|
475
|
+
# Cache the result
|
476
|
+
self._token_cache[token] = auth_result
|
477
|
+
|
478
|
+
self.logger.info(
|
479
|
+
"JWT token authentication successful",
|
480
|
+
extra={"username": username, "roles": roles, "auth_method": "jwt"},
|
481
|
+
)
|
482
|
+
|
483
|
+
return auth_result
|
484
|
+
|
485
|
+
except Exception as e:
|
486
|
+
self.logger.error(
|
487
|
+
"JWT token authentication failed",
|
488
|
+
extra={"token": token[:20] + "..." if token else None, "error": str(e)},
|
489
|
+
)
|
490
|
+
raise JWTValidationError(f"JWT token authentication failed: {str(e)}")
|
491
|
+
|
492
|
+
def authenticate_certificate(self, cert_pem: str) -> AuthResult:
|
493
|
+
"""
|
494
|
+
Authenticate user using X.509 certificate.
|
495
|
+
|
496
|
+
This method validates an X.509 certificate and extracts user
|
497
|
+
information and roles from certificate extensions.
|
498
|
+
|
499
|
+
The method performs comprehensive certificate validation including:
|
500
|
+
- Certificate format and structure validation
|
501
|
+
- Certificate chain validation against CA
|
502
|
+
- Certificate expiration and validity checks
|
503
|
+
- Role and permission extraction from certificate extensions
|
504
|
+
- Certificate fingerprint validation
|
505
|
+
|
506
|
+
Args:
|
507
|
+
cert_pem (str): X.509 certificate in PEM format. Must be a valid
|
508
|
+
PEM certificate with proper structure and extensions.
|
509
|
+
The certificate should contain user roles in extensions.
|
510
|
+
|
511
|
+
Returns:
|
512
|
+
AuthResult: Authentication result object containing:
|
513
|
+
- is_valid (bool): True if authentication succeeded
|
514
|
+
- username (str): Authenticated username from certificate
|
515
|
+
- roles (List[str]): User roles from certificate
|
516
|
+
- auth_method (str): Authentication method used ("certificate")
|
517
|
+
- error_code (int): Error code if authentication failed
|
518
|
+
- error_message (str): Human-readable error message
|
519
|
+
|
520
|
+
Raises:
|
521
|
+
CertificateValidationError: When certificate is invalid, expired, or
|
522
|
+
validation process fails
|
523
|
+
AuthenticationError: When authentication process fails
|
524
|
+
ValueError: When certificate parameter is empty or malformed
|
525
|
+
|
526
|
+
Example:
|
527
|
+
>>> auth_manager = AuthManager(config, permission_manager)
|
528
|
+
>>> with open("user.crt", "r") as f:
|
529
|
+
... cert_pem = f.read()
|
530
|
+
>>> result = auth_manager.authenticate_certificate(cert_pem)
|
531
|
+
>>> if result.is_valid:
|
532
|
+
... print(f"Authenticated user: {result.username}")
|
533
|
+
... print(f"Certificate roles: {result.roles}")
|
534
|
+
>>> else:
|
535
|
+
... print(f"Certificate validation failed: {result.error_message}")
|
536
|
+
|
537
|
+
Note:
|
538
|
+
This method validates X.509 certificates and extracts roles
|
539
|
+
from certificate extensions. Certificate chain validation
|
540
|
+
requires proper CA configuration.
|
541
|
+
|
542
|
+
See Also:
|
543
|
+
authenticate_api_key: API key authentication method
|
544
|
+
authenticate_jwt_token: JWT token authentication method
|
545
|
+
"""
|
546
|
+
try:
|
547
|
+
# Validate input
|
548
|
+
if not cert_pem:
|
549
|
+
return AuthResult(
|
550
|
+
is_valid=False,
|
551
|
+
status=AuthStatus.INVALID,
|
552
|
+
auth_method="certificate",
|
553
|
+
error_code=-32001,
|
554
|
+
error_message="Certificate is required",
|
555
|
+
)
|
556
|
+
|
557
|
+
# Parse certificate
|
558
|
+
try:
|
559
|
+
cert = parse_certificate(cert_pem)
|
560
|
+
except Exception as e:
|
561
|
+
return AuthResult(
|
562
|
+
is_valid=False,
|
563
|
+
status=AuthStatus.INVALID,
|
564
|
+
auth_method="certificate",
|
565
|
+
error_code=-32002,
|
566
|
+
error_message=f"Invalid certificate format: {str(e)}",
|
567
|
+
)
|
568
|
+
|
569
|
+
# Check certificate expiration
|
570
|
+
now = datetime.now(timezone.utc)
|
571
|
+
if cert.not_valid_after.replace(tzinfo=timezone.utc) < now:
|
572
|
+
return AuthResult(
|
573
|
+
is_valid=False,
|
574
|
+
status=AuthStatus.EXPIRED,
|
575
|
+
auth_method="certificate",
|
576
|
+
error_code=-32003,
|
577
|
+
error_message="Certificate has expired",
|
578
|
+
)
|
579
|
+
|
580
|
+
# Extract username from certificate subject
|
581
|
+
username = self._extract_username_from_certificate(cert)
|
582
|
+
if not username:
|
583
|
+
return AuthResult(
|
584
|
+
is_valid=False,
|
585
|
+
status=AuthStatus.INVALID,
|
586
|
+
auth_method="certificate",
|
587
|
+
error_code=-32004,
|
588
|
+
error_message="Certificate missing username in subject",
|
589
|
+
)
|
590
|
+
|
591
|
+
# Extract roles from certificate
|
592
|
+
try:
|
593
|
+
roles = extract_roles_from_certificate(cert_pem)
|
594
|
+
except Exception as e:
|
595
|
+
self.logger.warning(
|
596
|
+
"Failed to extract roles from certificate",
|
597
|
+
extra={"username": username, "error": str(e)},
|
598
|
+
)
|
599
|
+
roles = []
|
600
|
+
|
601
|
+
# Validate certificate chain if CA is configured
|
602
|
+
if self.config.ca_cert_file:
|
603
|
+
try:
|
604
|
+
is_valid_chain = validate_certificate_chain(
|
605
|
+
cert_pem, self.config.ca_cert_file
|
606
|
+
)
|
607
|
+
if not is_valid_chain:
|
608
|
+
return AuthResult(
|
609
|
+
is_valid=False,
|
610
|
+
status=AuthStatus.INVALID,
|
611
|
+
auth_method="certificate",
|
612
|
+
error_code=-32005,
|
613
|
+
error_message="Certificate chain validation failed",
|
614
|
+
)
|
615
|
+
except Exception as e:
|
616
|
+
self.logger.warning(
|
617
|
+
"Certificate chain validation failed",
|
618
|
+
extra={"username": username, "error": str(e)},
|
619
|
+
)
|
620
|
+
|
621
|
+
# Create successful authentication result
|
622
|
+
auth_result = AuthResult(
|
623
|
+
is_valid=True,
|
624
|
+
status=AuthStatus.SUCCESS,
|
625
|
+
username=username,
|
626
|
+
roles=roles,
|
627
|
+
auth_method="certificate",
|
628
|
+
auth_timestamp=datetime.now(timezone.utc),
|
629
|
+
token_expiry=cert.not_valid_after.replace(tzinfo=timezone.utc),
|
630
|
+
)
|
631
|
+
|
632
|
+
self.logger.info(
|
633
|
+
"Certificate authentication successful",
|
634
|
+
extra={
|
635
|
+
"username": username,
|
636
|
+
"roles": roles,
|
637
|
+
"auth_method": "certificate",
|
638
|
+
},
|
639
|
+
)
|
640
|
+
|
641
|
+
return auth_result
|
642
|
+
|
643
|
+
except Exception as e:
|
644
|
+
self.logger.error(
|
645
|
+
"Certificate authentication failed",
|
646
|
+
extra={
|
647
|
+
"cert_pem": cert_pem[:100] + "..." if cert_pem else None,
|
648
|
+
"error": str(e),
|
649
|
+
},
|
650
|
+
)
|
651
|
+
raise CertificateValidationError(
|
652
|
+
f"Certificate authentication failed: {str(e)}"
|
653
|
+
)
|
654
|
+
|
655
|
+
def create_jwt_token(self, user_data: Dict) -> str:
|
656
|
+
"""
|
657
|
+
Create JWT token for user.
|
658
|
+
|
659
|
+
This method creates a JWT token containing user information
|
660
|
+
and roles for authentication purposes.
|
661
|
+
|
662
|
+
The method creates a secure JWT token with:
|
663
|
+
- User information in payload
|
664
|
+
- Role and permission data
|
665
|
+
- Proper expiration time
|
666
|
+
- Secure signing with configured secret
|
667
|
+
- Standard JWT claims (iat, exp, sub)
|
668
|
+
|
669
|
+
Args:
|
670
|
+
user_data (Dict): User data dictionary containing:
|
671
|
+
- username (str): User username (required)
|
672
|
+
- roles (List[str]): User roles (optional)
|
673
|
+
- permissions (List[str]): User permissions (optional)
|
674
|
+
- additional_data (Dict): Additional user data (optional)
|
675
|
+
Must contain at least username field.
|
676
|
+
|
677
|
+
Returns:
|
678
|
+
str: JWT token string containing user information and claims.
|
679
|
+
The token is signed with the configured secret and includes
|
680
|
+
standard JWT claims and user-specific data.
|
681
|
+
|
682
|
+
Raises:
|
683
|
+
ValueError: When user_data is invalid or missing required fields
|
684
|
+
JWTValidationError: When JWT token creation fails
|
685
|
+
AuthenticationConfigurationError: When JWT configuration is invalid
|
686
|
+
|
687
|
+
Example:
|
688
|
+
>>> auth_manager = AuthManager(config, permission_manager)
|
689
|
+
>>> user_data = {
|
690
|
+
... "username": "john_doe",
|
691
|
+
... "roles": ["user", "admin"],
|
692
|
+
... "permissions": ["read:users", "write:posts"]
|
693
|
+
... }
|
694
|
+
>>> token = auth_manager.create_jwt_token(user_data)
|
695
|
+
>>> print(f"Created JWT token: {token}")
|
696
|
+
|
697
|
+
Note:
|
698
|
+
This method creates JWT tokens with configurable expiration.
|
699
|
+
Tokens are signed with the configured secret for security.
|
700
|
+
For production use, consider implementing token refresh mechanisms.
|
701
|
+
|
702
|
+
See Also:
|
703
|
+
authenticate_jwt_token: JWT token validation method
|
704
|
+
validate_jwt_token: JWT token validation utility
|
705
|
+
"""
|
706
|
+
try:
|
707
|
+
# Validate input
|
708
|
+
if user_data is None:
|
709
|
+
raise ValueError("User data must be provided")
|
710
|
+
|
711
|
+
if not isinstance(user_data, dict):
|
712
|
+
raise ValueError("User data must be a dictionary")
|
713
|
+
|
714
|
+
if not user_data: # Check if dict is empty
|
715
|
+
raise ValueError("User data dictionary cannot be empty")
|
716
|
+
|
717
|
+
username = user_data.get("username")
|
718
|
+
if not username:
|
719
|
+
raise ValueError("Username is required in user data")
|
720
|
+
|
721
|
+
# Prepare JWT payload
|
722
|
+
now = datetime.now(timezone.utc)
|
723
|
+
payload = {
|
724
|
+
"username": username,
|
725
|
+
"roles": user_data.get("roles", []),
|
726
|
+
"permissions": user_data.get("permissions", []),
|
727
|
+
"iat": int(now.timestamp()),
|
728
|
+
"exp": int(
|
729
|
+
(now + timedelta(hours=self.config.jwt_expiry_hours)).timestamp()
|
730
|
+
),
|
731
|
+
"sub": username,
|
732
|
+
"iss": "mcp_security_framework",
|
733
|
+
}
|
734
|
+
|
735
|
+
# Add additional user data if provided
|
736
|
+
additional_data = user_data.get("additional_data", {})
|
737
|
+
if additional_data:
|
738
|
+
payload["user_data"] = additional_data
|
739
|
+
|
740
|
+
# Create JWT token
|
741
|
+
token = jwt.encode(payload, self._jwt_secret, algorithm="HS256")
|
742
|
+
|
743
|
+
self.logger.info(
|
744
|
+
"JWT token created successfully",
|
745
|
+
extra={
|
746
|
+
"username": username,
|
747
|
+
"roles": payload["roles"],
|
748
|
+
"expires_in_hours": self.config.jwt_expiry_hours,
|
749
|
+
},
|
750
|
+
)
|
751
|
+
|
752
|
+
return token
|
753
|
+
|
754
|
+
except Exception as e:
|
755
|
+
self.logger.error(
|
756
|
+
"Failed to create JWT token",
|
757
|
+
extra={"user_data": user_data, "error": str(e)},
|
758
|
+
)
|
759
|
+
raise JWTValidationError(f"Failed to create JWT token: {str(e)}")
|
760
|
+
|
761
|
+
def validate_jwt_token(self, token: str) -> bool:
|
762
|
+
"""
|
763
|
+
Validate JWT token without full authentication.
|
764
|
+
|
765
|
+
This method performs basic JWT token validation including
|
766
|
+
signature verification and expiration checks.
|
767
|
+
|
768
|
+
Args:
|
769
|
+
token (str): JWT token to validate. Must be a valid
|
770
|
+
JWT token format with proper signature.
|
771
|
+
|
772
|
+
Returns:
|
773
|
+
bool: True if token is valid, False otherwise.
|
774
|
+
Returns True when token has valid signature and
|
775
|
+
is not expired.
|
776
|
+
|
777
|
+
Example:
|
778
|
+
>>> auth_manager = AuthManager(config, permission_manager)
|
779
|
+
>>> is_valid = auth_manager.validate_jwt_token("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...")
|
780
|
+
>>> if is_valid:
|
781
|
+
... print("Token is valid")
|
782
|
+
>>> else:
|
783
|
+
... print("Token is invalid")
|
784
|
+
"""
|
785
|
+
try:
|
786
|
+
if not token:
|
787
|
+
return False
|
788
|
+
|
789
|
+
# Decode token to check signature and expiration
|
790
|
+
jwt.decode(
|
791
|
+
token,
|
792
|
+
self._jwt_secret,
|
793
|
+
algorithms=["HS256"],
|
794
|
+
options={
|
795
|
+
"verify_signature": True,
|
796
|
+
"verify_exp": True,
|
797
|
+
"verify_aud": False,
|
798
|
+
},
|
799
|
+
)
|
800
|
+
|
801
|
+
return True
|
802
|
+
|
803
|
+
except jwt.InvalidTokenError:
|
804
|
+
return False
|
805
|
+
except Exception:
|
806
|
+
return False
|
807
|
+
|
808
|
+
def add_api_key(self, username: str, api_key: str) -> bool:
|
809
|
+
"""
|
810
|
+
Add API key for user.
|
811
|
+
|
812
|
+
This method adds an API key to the authentication store
|
813
|
+
for the specified user.
|
814
|
+
|
815
|
+
Args:
|
816
|
+
username (str): Username to associate with API key
|
817
|
+
api_key (str): API key to add
|
818
|
+
|
819
|
+
Returns:
|
820
|
+
bool: True if API key was added successfully, False otherwise
|
821
|
+
|
822
|
+
Example:
|
823
|
+
>>> auth_manager = AuthManager(config, permission_manager)
|
824
|
+
>>> success = auth_manager.add_api_key("john_doe", "new_api_key_123")
|
825
|
+
>>> if success:
|
826
|
+
... print("API key added successfully")
|
827
|
+
"""
|
828
|
+
try:
|
829
|
+
if not username or not api_key:
|
830
|
+
return False
|
831
|
+
|
832
|
+
if not validate_api_key_format(api_key):
|
833
|
+
return False
|
834
|
+
|
835
|
+
self._api_keys[username] = api_key
|
836
|
+
|
837
|
+
self.logger.info("API key added for user", extra={"username": username})
|
838
|
+
|
839
|
+
return True
|
840
|
+
|
841
|
+
except Exception as e:
|
842
|
+
self.logger.error(
|
843
|
+
"Failed to add API key", extra={"username": username, "error": str(e)}
|
844
|
+
)
|
845
|
+
return False
|
846
|
+
|
847
|
+
def remove_api_key(self, username: str) -> bool:
|
848
|
+
"""
|
849
|
+
Remove API key for user.
|
850
|
+
|
851
|
+
Args:
|
852
|
+
username (str): Username whose API key to remove
|
853
|
+
|
854
|
+
Returns:
|
855
|
+
bool: True if API key was removed successfully, False otherwise
|
856
|
+
"""
|
857
|
+
try:
|
858
|
+
if username in self._api_keys:
|
859
|
+
del self._api_keys[username]
|
860
|
+
|
861
|
+
self.logger.info(
|
862
|
+
"API key removed for user", extra={"username": username}
|
863
|
+
)
|
864
|
+
|
865
|
+
return True
|
866
|
+
|
867
|
+
return False
|
868
|
+
|
869
|
+
except Exception as e:
|
870
|
+
self.logger.error(
|
871
|
+
"Failed to remove API key",
|
872
|
+
extra={"username": username, "error": str(e)},
|
873
|
+
)
|
874
|
+
return False
|
875
|
+
|
876
|
+
def clear_token_cache(self) -> None:
|
877
|
+
"""Clear JWT token cache."""
|
878
|
+
self._token_cache.clear()
|
879
|
+
self.logger.info("JWT token cache cleared")
|
880
|
+
|
881
|
+
def clear_session_store(self) -> None:
|
882
|
+
"""Clear session store."""
|
883
|
+
self._session_store.clear()
|
884
|
+
self.logger.info("Session store cleared")
|
885
|
+
|
886
|
+
def _validate_configuration(self) -> None:
|
887
|
+
"""Validate authentication configuration."""
|
888
|
+
if not self._jwt_secret:
|
889
|
+
raise AuthenticationConfigurationError("JWT secret is required")
|
890
|
+
|
891
|
+
if len(self._jwt_secret) < 16:
|
892
|
+
raise AuthenticationConfigurationError(
|
893
|
+
"JWT secret must be at least 16 characters"
|
894
|
+
)
|
895
|
+
|
896
|
+
def _get_user_roles(self, username: str) -> List[str]:
|
897
|
+
"""
|
898
|
+
Get user roles from configuration.
|
899
|
+
|
900
|
+
This method retrieves user roles from the authentication configuration.
|
901
|
+
It checks both the user_roles mapping and the permission manager.
|
902
|
+
|
903
|
+
Args:
|
904
|
+
username (str): Username to get roles for
|
905
|
+
|
906
|
+
Returns:
|
907
|
+
List[str]: List of user roles
|
908
|
+
"""
|
909
|
+
# Check user_roles mapping first
|
910
|
+
if self.config.user_roles and username in self.config.user_roles:
|
911
|
+
return self.config.user_roles[username]
|
912
|
+
|
913
|
+
# Fallback to permission manager
|
914
|
+
if self.permission_manager:
|
915
|
+
return self.permission_manager.get_user_roles(username)
|
916
|
+
|
917
|
+
return []
|
918
|
+
|
919
|
+
def _validate_external_user(self, username: str, credentials: Dict[str, Any]) -> bool:
|
920
|
+
"""
|
921
|
+
Validate user against external authentication system.
|
922
|
+
|
923
|
+
This method provides a hook for integrating with external
|
924
|
+
authentication systems (LDAP, Active Directory, etc.).
|
925
|
+
|
926
|
+
Args:
|
927
|
+
username (str): Username to validate
|
928
|
+
credentials (Dict[str, Any]): User credentials
|
929
|
+
|
930
|
+
Returns:
|
931
|
+
bool: True if user is valid in external system
|
932
|
+
"""
|
933
|
+
# This is a placeholder for external authentication integration
|
934
|
+
# In a real implementation, this would connect to external auth systems
|
935
|
+
self.logger.debug(
|
936
|
+
"External user validation called",
|
937
|
+
extra={"username": username, "method": "external"}
|
938
|
+
)
|
939
|
+
|
940
|
+
# For now, return False to indicate external validation not implemented
|
941
|
+
return False
|
942
|
+
|
943
|
+
def _extract_username_from_certificate(
|
944
|
+
self, cert: x509.Certificate
|
945
|
+
) -> Optional[str]:
|
946
|
+
"""Extract username from certificate subject."""
|
947
|
+
try:
|
948
|
+
# Try to get username from Common Name
|
949
|
+
cn_attr = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
|
950
|
+
if cn_attr:
|
951
|
+
return cn_attr[0].value
|
952
|
+
|
953
|
+
# Try to get username from Subject Alternative Name
|
954
|
+
try:
|
955
|
+
san = cert.extensions.get_extension_for_oid(
|
956
|
+
x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
957
|
+
)
|
958
|
+
if san and san.value:
|
959
|
+
for name in san.value:
|
960
|
+
if isinstance(name, x509.DNSName):
|
961
|
+
return name.value
|
962
|
+
except x509.ExtensionNotFound:
|
963
|
+
pass
|
964
|
+
|
965
|
+
return None
|
966
|
+
|
967
|
+
except Exception:
|
968
|
+
return None
|
969
|
+
|
970
|
+
def _is_token_expired(self, auth_result: AuthResult) -> bool:
|
971
|
+
"""Check if authentication result is expired."""
|
972
|
+
if not auth_result.token_expiry:
|
973
|
+
return False
|
974
|
+
|
975
|
+
return datetime.now(timezone.utc) > auth_result.token_expiry
|
976
|
+
|
977
|
+
|
978
|
+
class AuthenticationConfigurationError(Exception):
|
979
|
+
"""Raised when authentication configuration is invalid."""
|
980
|
+
|
981
|
+
def __init__(self, message: str, error_code: int = -32001):
|
982
|
+
self.message = message
|
983
|
+
self.error_code = error_code
|
984
|
+
super().__init__(self.message)
|
985
|
+
|
986
|
+
|
987
|
+
class AuthenticationError(Exception):
|
988
|
+
"""Raised when authentication fails."""
|
989
|
+
|
990
|
+
def __init__(self, message: str, error_code: int = -32002):
|
991
|
+
self.message = message
|
992
|
+
self.error_code = error_code
|
993
|
+
super().__init__(self.message)
|
994
|
+
|
995
|
+
|
996
|
+
class JWTValidationError(Exception):
|
997
|
+
"""Raised when JWT token validation fails."""
|
998
|
+
|
999
|
+
def __init__(self, message: str, error_code: int = -32003):
|
1000
|
+
self.message = message
|
1001
|
+
self.error_code = error_code
|
1002
|
+
super().__init__(self.message)
|
1003
|
+
|
1004
|
+
|
1005
|
+
class CertificateValidationError(Exception):
|
1006
|
+
"""Raised when certificate validation fails."""
|
1007
|
+
|
1008
|
+
def __init__(self, message: str, error_code: int = -32004):
|
1009
|
+
self.message = message
|
1010
|
+
self.error_code = error_code
|
1011
|
+
super().__init__(self.message)
|