mcp-security-framework 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/PKG-INFO +1 -1
  2. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/__init__.py +1 -1
  3. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/audit_logger.py +28 -20
  4. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/auth_manager.py +271 -9
  5. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/ssl_manager.py +24 -0
  6. mcp_security_framework-2.3.0/mcp_security_framework/examples/auth_methods_example.py +321 -0
  7. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/cert_inspection_example.py +67 -38
  8. mcp_security_framework-2.3.0/mcp_security_framework/examples/certificate_roles_example.py +393 -0
  9. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/crl_retrieval_example.py +42 -19
  10. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/protected_keys_example.py +29 -12
  11. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/fastapi_auth_middleware.py +120 -27
  12. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/fastapi_middleware.py +121 -29
  13. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/flask_auth_middleware.py +83 -27
  14. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/flask_middleware.py +82 -29
  15. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/config.py +32 -6
  16. mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/__init__.py +80 -0
  17. mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/chain.py +183 -0
  18. mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/errors.py +28 -0
  19. mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/inspection.py +335 -0
  20. mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/parsing.py +255 -0
  21. mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/revocation.py +206 -0
  22. mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/roles.py +412 -0
  23. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/PKG-INFO +1 -1
  24. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/SOURCES.txt +10 -2
  25. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/pyproject.toml +1 -1
  26. mcp_security_framework-2.2.0/mcp_security_framework/utils/cert_utils.py +0 -1353
  27. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/LICENSE +0 -0
  28. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/README.md +0 -0
  29. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/cli/__init__.py +0 -0
  30. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/cli/cert_cli.py +0 -0
  31. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/cli/security_cli.py +0 -0
  32. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/constants.py +0 -0
  33. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/__init__.py +0 -0
  34. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/adapter_wrapper.py +0 -0
  35. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/cert_manager.py +0 -0
  36. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/permission_manager.py +0 -0
  37. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/rate_limiter.py +0 -0
  38. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/security_adapter.py +0 -0
  39. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/security_manager.py +0 -0
  40. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/__init__.py +0 -0
  41. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/adapter_example.py +0 -0
  42. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/comprehensive_example.py +0 -0
  43. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/fastapi_example.py +0 -0
  44. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/flask_example.py +0 -0
  45. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/gateway_example.py +0 -0
  46. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/microservice_example.py +0 -0
  47. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/standalone_example.py +0 -0
  48. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/__init__.py +0 -0
  49. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/auth_middleware.py +0 -0
  50. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/mtls_middleware.py +0 -0
  51. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/rate_limit_middleware.py +0 -0
  52. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/security_middleware.py +0 -0
  53. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/__init__.py +0 -0
  54. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/models.py +0 -0
  55. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/operation_context.py +0 -0
  56. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/responses.py +0 -0
  57. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/__init__.py +0 -0
  58. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/crl_client.py +0 -0
  59. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/crypto_utils.py +0 -0
  60. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/datetime_compat.py +0 -0
  61. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/validation_utils.py +0 -0
  62. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/dependency_links.txt +0 -0
  63. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/entry_points.txt +0 -0
  64. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/requires.txt +0 -0
  65. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/top_level.txt +0 -0
  66. {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-security-framework
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Added extensible security adapter system for custom operation validation, structured audit logging, and operation context support. Comprehensive test coverage with 1104+ tests. Fully backward compatible. Requires cryptography>=42.0.0 for certificate operations.
5
5
  Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
6
  Maintainer-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
@@ -79,7 +79,7 @@ from mcp_security_framework.schemas.responses import (
79
79
  )
80
80
 
81
81
  # Version information
82
- __version__ = "2.2.0"
82
+ __version__ = "2.3.0"
83
83
  __author__ = "Vasiliy Zdanovskiy"
84
84
  __email__ = "vasilyvz@gmail.com"
85
85
  __license__ = "MIT"
@@ -33,6 +33,22 @@ from typing import Any, Dict, List, Optional
33
33
  from ..schemas.operation_context import OperationContext
34
34
 
35
35
 
36
+ class AuditLoggerConfigurationError(ValueError):
37
+ """
38
+ Raised when the audit logger is misconfigured or an unsupported
39
+ backend is requested.
40
+
41
+ This error indicates that the logger cannot be initialized with the
42
+ provided configuration, typically because a requested backend is not
43
+ supported or required configuration is missing.
44
+
45
+ Inherits from ValueError to maintain compatibility with existing
46
+ code that catches ValueError for configuration errors.
47
+ """
48
+
49
+ pass
50
+
51
+
36
52
  class AuditStatus(str, Enum):
37
53
  """
38
54
  Audit Status Enumeration
@@ -224,9 +240,10 @@ class AuditLogger:
224
240
  ... )
225
241
 
226
242
  Note:
227
- Database and elasticsearch backends are currently placeholders
228
- and will raise NotImplementedError if used. File and console
229
- backends are fully implemented.
243
+ Only file and console backends are supported. Requesting any other
244
+ backend will raise AuditLoggerConfigurationError at initialization time
245
+ to prevent silent loss of audit records. This is not an extension point;
246
+ audit logs are security-critical and must never silently fail.
230
247
  """
231
248
 
232
249
  def __init__(
@@ -239,15 +256,16 @@ class AuditLogger:
239
256
 
240
257
  Args:
241
258
  backend (str): Backend type for audit logging. Supported values:
242
- "file", "console", "database", "elasticsearch". Defaults
243
- to "console" if not specified.
259
+ "file" or "console". Defaults to "console" if not specified.
244
260
  config (Optional[Dict[str, Any]]): Configuration dictionary for
245
261
  the selected backend. For file backend, should contain
246
262
  "log_file" key with file path. For console backend, can
247
263
  be empty or None. Defaults to None.
248
264
 
249
265
  Raises:
250
- ValueError: If backend type is not supported.
266
+ AuditLoggerConfigurationError: If backend type is not supported.
267
+ This is a fail-loud check at initialization time to prevent
268
+ silent loss of audit records.
251
269
  FileNotFoundError: If file backend is used and log file
252
270
  directory does not exist.
253
271
 
@@ -266,11 +284,11 @@ class AuditLogger:
266
284
  self.logger = logging.getLogger(f"{__name__}.{self.backend}")
267
285
 
268
286
  # Validate backend type
269
- supported_backends = ["file", "console", "database", "elasticsearch"]
270
- if self.backend not in supported_backends:
271
- raise ValueError(
287
+ implemented_backends = ["file", "console"]
288
+ if self.backend not in implemented_backends:
289
+ raise AuditLoggerConfigurationError(
272
290
  f"Unsupported backend: {self.backend}. "
273
- f"Supported backends: {supported_backends}"
291
+ f"Supported backends: {implemented_backends}"
274
292
  )
275
293
 
276
294
  # Initialize backend-specific configuration
@@ -310,8 +328,6 @@ class AuditLogger:
310
328
  AuditEvent instance with all required fields.
311
329
 
312
330
  Raises:
313
- NotImplementedError: If database or elasticsearch backend
314
- is used (not yet implemented).
315
331
  IOError: If file backend is used and log file cannot be written.
316
332
 
317
333
  Example:
@@ -340,14 +356,6 @@ class AuditLogger:
340
356
  log_message = json.dumps(event_dict, default=str)
341
357
  self.logger.info(log_message)
342
358
 
343
- elif self.backend == "database":
344
- # Placeholder for database backend
345
- raise NotImplementedError("Database backend not yet implemented")
346
-
347
- elif self.backend == "elasticsearch":
348
- # Placeholder for elasticsearch backend
349
- raise NotImplementedError("Elasticsearch backend not yet implemented")
350
-
351
359
  def log_operation(
352
360
  self,
353
361
  operation: str,
@@ -702,6 +702,246 @@ class AuthManager:
702
702
  error_code=-32004,
703
703
  )
704
704
 
705
+ def authenticate_basic(self, authorization_header: str) -> AuthResult:
706
+ """
707
+ Authenticate user using HTTP Basic Authentication.
708
+
709
+ This method validates an HTTP Basic Authentication header per RFC 7617,
710
+ parsing the base64-encoded credentials and verifying them against the
711
+ configured user store.
712
+
713
+ The method performs comprehensive basic auth validation including:
714
+ - Authorization header presence and format verification
715
+ - Base64 decoding of credentials
716
+ - Username and password extraction (split on first colon)
717
+ - Credential verification against stored password hashes
718
+ - User role and permission extraction
719
+
720
+ Args:
721
+ authorization_header (str): Full Authorization header value
722
+ (e.g., "Basic dXNlcm5hbWU6cGFzc3dvcmQ="). Must be in the format
723
+ "Basic <base64-encoded-credentials>" where credentials are
724
+ "username:password". Case-insensitive scheme matching.
725
+
726
+ Returns:
727
+ AuthResult: Authentication result object containing:
728
+ - is_valid (bool): True if authentication succeeded
729
+ - username (str): Authenticated username if valid
730
+ - roles (List[str]): User roles from user_roles mapping
731
+ - auth_method (str): Authentication method used ("basic")
732
+ - error_code (int): Error code if authentication failed
733
+ - error_message (str): Human-readable error message
734
+
735
+ Note on Error Indistinguishability:
736
+ An unknown username and a wrong password return identical status and
737
+ error messages. This prevents username enumeration attacks by making
738
+ it impossible for an attacker to determine whether a username is
739
+ valid or only its password is wrong. Both cases return
740
+ status=INVALID and a generic "Invalid credentials" message.
741
+
742
+ Raises:
743
+ AuthenticationError: When authentication process fails unexpectedly
744
+
745
+ Example:
746
+ >>> auth_manager = AuthManager(config, permission_manager)
747
+ >>> result = auth_manager.authenticate_basic("Basic dXNlcjpwYXNz")
748
+ >>> if result.is_valid:
749
+ ... print(f"Authenticated user: {result.username}")
750
+ ... print(f"User roles: {result.roles}")
751
+ >>> else:
752
+ ... print(f"Authentication failed: {result.error_message}")
753
+
754
+ See Also:
755
+ authenticate_api_key: API key authentication method
756
+ authenticate_jwt_token: JWT token authentication method
757
+ authenticate_certificate: Certificate-based authentication
758
+ """
759
+ try:
760
+ from ..utils.crypto_utils import verify_password
761
+
762
+ # Validate input
763
+ if not authorization_header:
764
+ return AuthResult(
765
+ is_valid=False,
766
+ status=AuthStatus.INVALID,
767
+ auth_method="basic",
768
+ error_code=-32001,
769
+ error_message="Authorization header is required",
770
+ )
771
+
772
+ # Parse Authorization header: "Basic <base64>"
773
+ parts = authorization_header.split(maxsplit=1)
774
+ if len(parts) != 2:
775
+ return AuthResult(
776
+ is_valid=False,
777
+ status=AuthStatus.INVALID,
778
+ auth_method="basic",
779
+ error_code=-32002,
780
+ error_message="Invalid Authorization header format",
781
+ )
782
+
783
+ scheme, credentials_b64 = parts
784
+ if scheme.lower() != "basic":
785
+ return AuthResult(
786
+ is_valid=False,
787
+ status=AuthStatus.INVALID,
788
+ auth_method="basic",
789
+ error_code=-32003,
790
+ error_message="Invalid Authorization header format",
791
+ )
792
+
793
+ # Decode base64
794
+ try:
795
+ credentials_bytes = __import__("base64").b64decode(credentials_b64)
796
+ credentials_str = credentials_bytes.decode("utf-8")
797
+ except Exception:
798
+ return AuthResult(
799
+ is_valid=False,
800
+ status=AuthStatus.INVALID,
801
+ auth_method="basic",
802
+ error_code=-32004,
803
+ error_message="Invalid credentials encoding",
804
+ )
805
+
806
+ # Split on first colon only (password may contain colons)
807
+ if ":" not in credentials_str:
808
+ return AuthResult(
809
+ is_valid=False,
810
+ status=AuthStatus.INVALID,
811
+ auth_method="basic",
812
+ error_code=-32005,
813
+ error_message="Invalid credentials format",
814
+ )
815
+
816
+ colon_idx = credentials_str.index(":")
817
+ username = credentials_str[:colon_idx]
818
+ password = credentials_str[colon_idx + 1 :]
819
+
820
+ # Validate username is provided
821
+ if not username:
822
+ return AuthResult(
823
+ is_valid=False,
824
+ status=AuthStatus.INVALID,
825
+ auth_method="basic",
826
+ error_code=-32006,
827
+ error_message="Invalid credentials",
828
+ )
829
+
830
+ # Check if user exists in basic_auth_users
831
+ if (
832
+ not self.config.basic_auth_users
833
+ or username not in self.config.basic_auth_users
834
+ ):
835
+ # User not found - return same error as wrong password
836
+ return AuthResult(
837
+ is_valid=False,
838
+ status=AuthStatus.INVALID,
839
+ auth_method="basic",
840
+ error_code=-32007,
841
+ error_message="Invalid credentials",
842
+ )
843
+
844
+ # Get stored credentials
845
+ stored_credentials = self.config.basic_auth_users[username]
846
+ stored_hash = stored_credentials.get("hash")
847
+ stored_salt = stored_credentials.get("salt")
848
+
849
+ if not stored_hash or not stored_salt:
850
+ return AuthResult(
851
+ is_valid=False,
852
+ status=AuthStatus.INVALID,
853
+ auth_method="basic",
854
+ error_code=-32008,
855
+ error_message="Invalid credentials",
856
+ )
857
+
858
+ # Verify password
859
+ try:
860
+ if not verify_password(password, stored_hash, stored_salt):
861
+ # Wrong password - return same error as unknown user
862
+ return AuthResult(
863
+ is_valid=False,
864
+ status=AuthStatus.INVALID,
865
+ auth_method="basic",
866
+ error_code=-32009,
867
+ error_message="Invalid credentials",
868
+ )
869
+ except Exception as e:
870
+ self.logger.error(
871
+ "Password verification failed",
872
+ extra={"username": username, "error": str(e)},
873
+ )
874
+ return AuthResult(
875
+ is_valid=False,
876
+ status=AuthStatus.INVALID,
877
+ auth_method="basic",
878
+ error_code=-32010,
879
+ error_message="Invalid credentials",
880
+ )
881
+
882
+ # Get user roles
883
+ user_roles = self._get_user_roles(username)
884
+
885
+ # Get user permissions from permission manager
886
+ user_permissions = set()
887
+ if self.permission_manager:
888
+ try:
889
+ permissions_result = (
890
+ self.permission_manager.get_effective_permissions(user_roles)
891
+ )
892
+ # Handle both set and mock objects
893
+ if hasattr(permissions_result, "__iter__") and not isinstance(
894
+ permissions_result, str
895
+ ):
896
+ user_permissions = set(permissions_result)
897
+ else:
898
+ user_permissions = set()
899
+ except Exception as e:
900
+ self.logger.warning(
901
+ "Failed to get user permissions",
902
+ extra={
903
+ "username": username,
904
+ "roles": user_roles,
905
+ "error": str(e),
906
+ },
907
+ )
908
+
909
+ # Create successful authentication result
910
+ auth_result = AuthResult(
911
+ is_valid=True,
912
+ status=AuthStatus.SUCCESS,
913
+ username=username,
914
+ roles=user_roles,
915
+ permissions=user_permissions,
916
+ auth_method="basic",
917
+ auth_timestamp=datetime.now(timezone.utc),
918
+ )
919
+
920
+ self.logger.info(
921
+ "Basic authentication successful",
922
+ extra={
923
+ "username": username,
924
+ "roles": user_roles,
925
+ "auth_method": "basic",
926
+ },
927
+ )
928
+
929
+ return auth_result
930
+
931
+ except Exception as e:
932
+ self.logger.error(
933
+ "Basic authentication failed",
934
+ extra={
935
+ "auth_header": (
936
+ authorization_header[:20] + "..."
937
+ if authorization_header
938
+ else None
939
+ ),
940
+ "error": str(e),
941
+ },
942
+ )
943
+ raise AuthenticationError(f"Basic authentication failed: {str(e)}")
944
+
705
945
  def create_jwt_token(self, user_data: Dict) -> str:
706
946
  """
707
947
  Create JWT token for user.
@@ -978,26 +1218,48 @@ class AuthManager:
978
1218
  self, username: str, credentials: Dict[str, Any]
979
1219
  ) -> bool:
980
1220
  """
981
- Validate user against external authentication system.
1221
+ Extension point for integrating with external authentication systems.
1222
+
1223
+ This method is provided as an extension point for frameworks and
1224
+ applications that need to integrate with external authentication
1225
+ systems such as LDAP, Active Directory, OAuth2 providers, SAML
1226
+ identity providers, or other enterprise authentication backends.
982
1227
 
983
- This method provides a hook for integrating with external
984
- authentication systems (LDAP, Active Directory, etc.).
1228
+ This base implementation always returns False, indicating that the
1229
+ extension point is not configured. Applications that wish to use
1230
+ external authentication must either:
1231
+ 1. Subclass AuthManager and override this method
1232
+ 2. Call this base method as a fallback only after checking for
1233
+ configuration that enables external authentication integration
985
1234
 
986
1235
  Args:
987
1236
  username (str): Username to validate
988
- credentials (Dict[str, Any]): User credentials
1237
+ credentials (Dict[str, Any]): User credentials dictionary
989
1238
 
990
1239
  Returns:
991
- bool: True if user is valid in external system
1240
+ bool: True if user is valid in the external system, False if
1241
+ not configured or validation failed.
1242
+
1243
+ Example:
1244
+ To integrate with an external system, subclass AuthManager::
1245
+
1246
+ class LDAPAuthManager(AuthManager):
1247
+ def _validate_external_user(self, username, credentials):
1248
+ if not self.config.ldap_enabled:
1249
+ return False
1250
+ # Connect to LDAP server and validate credentials
1251
+ return self.ldap_client.verify(username, credentials)
1252
+
1253
+ See Also:
1254
+ authenticate_api_key: API key authentication
1255
+ authenticate_basic: HTTP Basic Authentication
1256
+ authenticate_certificate: Certificate-based authentication
992
1257
  """
993
- # This is a placeholder for external authentication integration
994
- # In a real implementation, this would connect to external auth systems
995
1258
  self.logger.debug(
996
- "External user validation called",
1259
+ "External user validation called (not configured)",
997
1260
  extra={"username": username, "method": "external"},
998
1261
  )
999
1262
 
1000
- # For now, return False to indicate external validation not implemented
1001
1263
  return False
1002
1264
 
1003
1265
  def _extract_username_from_certificate(
@@ -235,10 +235,22 @@ class SSLManager:
235
235
 
236
236
  # Validate required files or data
237
237
  if cert_data is None and not cert_file:
238
+ if self.config.material_from_memory:
239
+ raise SSLConfigurationError(
240
+ "material_from_memory=True but cert_data was not supplied; "
241
+ "provide cert_data as PEM bytes or "
242
+ "set material_from_memory=False"
243
+ )
238
244
  raise SSLConfigurationError(
239
245
  "Server certificate file or data is required"
240
246
  )
241
247
  if key_data is None and not key_file:
248
+ if self.config.material_from_memory:
249
+ raise SSLConfigurationError(
250
+ "material_from_memory=True but key_data was not supplied; "
251
+ "provide key_data as PEM bytes or "
252
+ "set material_from_memory=False"
253
+ )
242
254
  raise SSLConfigurationError(
243
255
  "Server private key file or data is required"
244
256
  )
@@ -423,6 +435,18 @@ class SSLManager:
423
435
  verify_mode = verify_mode or self.config.verify_mode
424
436
  min_version = min_version or self.config.min_tls_version
425
437
 
438
+ # Validate that material_from_memory promises are kept
439
+ if cert_data is None and self.config.material_from_memory:
440
+ raise SSLConfigurationError(
441
+ "material_from_memory=True but cert_data was not supplied; "
442
+ "provide cert_data as PEM bytes or set material_from_memory=False"
443
+ )
444
+ if key_data is None and self.config.material_from_memory:
445
+ raise SSLConfigurationError(
446
+ "material_from_memory=True but key_data was not supplied; "
447
+ "provide key_data as PEM bytes or set material_from_memory=False"
448
+ )
449
+
426
450
  # Check cache (only for file-based contexts)
427
451
  cache_key = None
428
452
  if cert_data is None and key_data is None: