Source code for api.saml.metadata.federations.validator
import datetime
import logging
from abc import ABCMeta
import six
from defusedxml.lxml import fromstring
from onelogin.saml2.utils import OneLogin_Saml2_Utils
from core.exceptions import BaseError
from core.util.datetime_helpers import from_timestamp, utc_now
[docs]class SAMLFederatedMetadataValidationError(BaseError):
"""Raised in the case of any errors happened during SAML metadata validation."""
[docs]@six.add_metaclass(ABCMeta)
class SAMLFederatedMetadataValidator(object):
"""Base class for all validators checking correctness of SAML federated metadata."""
[docs] def validate(self, federation, metadata):
"""Validate SAML federated metadata.
:param federation: SAML federation
:type federation: api.saml.metadata.federations.model.SAMLFederation
:param metadata: SAML federation's aggregated metadata
:type metadata: str
:raises SAMLFederatedMetadataValidationError: in the case of validation errors
"""
raise NotImplementedError
[docs]class SAMLFederatedMetadataValidatorChain(SAMLFederatedMetadataValidator):
def __init__(self, validators):
"""Initialize a new instance of SAMLFederatedMetadataValidatorChain class.
:param validators: List of validators
:type validators: List[SAMLFederatedMetadataValidator]
"""
if not validators or not isinstance(validators, list):
raise ValueError("Argument 'validators' must be a non-empty list")
for validator in validators:
if not isinstance(validator, SAMLFederatedMetadataValidator):
raise ValueError(
"Argument 'validators' must contain only instances of {0} class".format(
SAMLFederatedMetadataValidator
)
)
self._validators = validators
[docs] def validate(self, federation, metadata):
"""Validate SAML federated metadata using a chain of inner validators.
:param federation: SAML federation
:type federation: api.saml.metadata.federations.model.SAMLFederation
:param metadata: SAML federation's aggregated metadata
:type metadata: str
:raises SAMLFederatedMetadataValidationError: in the case of validation errors
"""
for validator in self._validators:
validator.validate(federation, metadata)
[docs]class SAMLFederatedMetadataExpirationValidator(SAMLFederatedMetadataValidator):
"""Verifies that federated SAML metadata has not expired."""
# We allow the metadata's expiration time to be only 5 minutes behind.
MAX_CLOCK_SKEW = datetime.timedelta(minutes=5)
# We allow the metadata's expiration time to be only 4 week ahead.
MAX_VALID_TIME = datetime.timedelta(weeks=4)
def __init__(self):
"""Initialize a new instance of SAMLFederatedMetadataExpirationValidator class."""
self._logger = logging.getLogger(__name__)
@staticmethod
def _parse_saml_date_time(saml_date_time):
"""Parse the string containing date & time information in the SAML format into datetime object.
:param saml_date_time: String containing date & time information in the SAML format
:type saml_date_time: str
"""
unix_timestamp = OneLogin_Saml2_Utils.parse_SAML_to_time(saml_date_time)
parsed_date_time = from_timestamp(unix_timestamp)
return parsed_date_time
[docs] def validate(self, federation, metadata):
"""Verify that federated SAML metadata has not expired.
:param federation: SAML federation
:type federation: api.saml.metadata.federations.model.SAMLFederation
:param metadata: SAML federation's aggregated metadata
:type metadata: str
:raises SAMLFederatedMetadataValidationError: in the case of validation errors
"""
self._logger.info(
"Started validating the expiration time of the metadata belonging to {0}".format(
federation
)
)
try:
root = fromstring(metadata.encode("utf-8"))
except Exception as exception:
raise SAMLFederatedMetadataValidationError(
"Metadata's XML is not valid", str(exception)
)
if "EntitiesDescriptor" not in root.tag:
raise SAMLFederatedMetadataValidationError(
'Metadata\'s root element is not "EntitiesDescriptor"'
)
valid_until = root.get("validUntil", None)
if not valid_until:
raise SAMLFederatedMetadataValidationError(
'Metadata does not contain "validUntil" attribute'
)
valid_until = self._parse_saml_date_time(valid_until)
now = utc_now()
if valid_until < now and (now - valid_until) > self.MAX_CLOCK_SKEW:
raise SAMLFederatedMetadataValidationError(
"Metadata has already expired. "
'"validUntil" is {0} while the current time is {1}'.format(
valid_until, now
)
)
if valid_until > now and (valid_until - now) > self.MAX_VALID_TIME:
raise SAMLFederatedMetadataValidationError(
"Expiration time is unexpectedly far into the future. "
'"validUntil" is {0} while the current time is {1}'.format(
valid_until, now
)
)
self._logger.info(
"Finished validating the expiration time of the metadata belonging to {0}".format(
federation
)
)
[docs]class SAMLMetadataSignatureValidator(SAMLFederatedMetadataValidator):
"""Verifies the validity of federated SAML metadata's signature."""
def __init__(self):
"""Initialize a new instance of SAMLMetadataSignatureValidator class."""
self._logger = logging.getLogger(__name__)
[docs] def validate(self, federation, metadata):
"""Verify the validity of the SAML federated metadata's signature.
:param federation: SAML federation
:type federation: api.saml.metadata.federations.model.SAMLFederation
:param metadata: SAML federation's aggregated metadata
:type metadata: str
:raises SAMLFederatedMetadataValidationError: in the case of validation errors
"""
self._logger.info(
"Started verifying the validity of the metadata's signature belonging to {0}".format(
federation
)
)
try:
OneLogin_Saml2_Utils.validate_metadata_sign(
metadata, federation.certificate, raise_exceptions=True
)
except Exception as exception:
raise SAMLFederatedMetadataValidationError(
six.ensure_text(str(exception)), exception
)
self._logger.info(
"Finished verifying the validity of the metadata's signature belonging to {0}".format(
federation
)
)