Skip to content

Commit

Permalink
Adapt IdP XML metadata parser to take care of multiple IdP certtifica…
Browse files Browse the repository at this point in the history
…tes and be able to inject the data obtained on the settings.
  • Loading branch information
pitbulk committed May 15, 2017
1 parent 701d65e commit 650d2b9
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 83 deletions.
170 changes: 87 additions & 83 deletions src/onelogin/saml2/idp_metadata_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.xml_utils import OneLogin_Saml2_XML
from onelogin.saml2.utils import OneLogin_Saml2_Utils


class OneLogin_Saml2_IdPMetadataParser(object):
Expand Down Expand Up @@ -89,8 +88,7 @@ def parse(
idp_metadata,
required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
entity_id=None,
index=0):
entity_id=None):
"""
Parses the Identity Provider metadata and return a dict with extracted data.
Expand Down Expand Up @@ -121,99 +119,97 @@ def parse(
that contains multiple EntityDescriptor.
:type entity_id: string
:param index: If the metadata contains more than 1 certificate, use index to get the right certificate.
:type index: number
:returns: settings dict with extracted data
:rtype: dict
"""
data = {}

dom = OneLogin_Saml2_XML.to_etree(idp_metadata)
idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = idp_x509_cert = None
idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None

entity_desc_path = '//md:EntityDescriptor'
if entity_id:
entity_desc_path += "[@entityID='%s']" % entity_id
entity_descriptor_nodes = OneLogin_Saml2_XML.query(dom, entity_desc_path)

if len(entity_descriptor_nodes) > 0:
for entity_descriptor_node in entity_descriptor_nodes:
idp_descriptor_nodes = OneLogin_Saml2_XML.query(entity_descriptor_node, './md:IDPSSODescriptor')
if len(idp_descriptor_nodes) > 0:
idp_descriptor_node = idp_descriptor_nodes[0]

idp_entity_id = entity_descriptor_node.get('entityID', None)

want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None)

name_id_format_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, './md:NameIDFormat')
if len(name_id_format_nodes) > 0:
idp_name_id_format = name_id_format_nodes[0].text

sso_nodes = OneLogin_Saml2_XML.query(
idp_descriptor_node,
"./md:SingleSignOnService[@Binding='%s']" % required_sso_binding
)

if len(sso_nodes) > 0:
idp_sso_url = sso_nodes[0].get('Location', None)

slo_nodes = OneLogin_Saml2_XML.query(
idp_descriptor_node,
"./md:SingleLogoutService[@Binding='%s']" % required_slo_binding
)

if len(slo_nodes) > 0:
idp_slo_url = slo_nodes[0].get('Location', None)

# Attempt to extract the cert/public key to be used for
# verifying signatures (as opposed to extracing a key to be
# used for encryption), by specifying `use=signing` in the
# XPath expression. If that does not yield a cert, retry
# using a more relaxed XPath expression (the `use` attribute
# is optional according to the saml-metadata-2.0-os spec).
cert_nodes = OneLogin_Saml2_XML.query(
idp_descriptor_node,
"./md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate"
)

if not cert_nodes:
cert_nodes = OneLogin_Saml2_XML.query(
idp_descriptor_node,
"./md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate"
)

if len(cert_nodes) > 0:
idp_x509_cert = OneLogin_Saml2_Utils.format_cert(cert_nodes[index].text, False)

data['idp'] = {}

if idp_entity_id is not None:
data['idp']['entityId'] = idp_entity_id

if idp_sso_url is not None:
data['idp']['singleSignOnService'] = {}
data['idp']['singleSignOnService']['url'] = idp_sso_url
data['idp']['singleSignOnService']['binding'] = required_sso_binding

if idp_slo_url is not None:
data['idp']['singleLogoutService'] = {}
data['idp']['singleLogoutService']['url'] = idp_slo_url
data['idp']['singleLogoutService']['binding'] = required_slo_binding

if idp_x509_cert is not None:
data['idp']['x509cert'] = idp_x509_cert

if want_authn_requests_signed is not None:
data['security'] = {}
data['security']['authnRequestsSigned'] = want_authn_requests_signed

if idp_name_id_format:
data['sp'] = {}
data['sp']['NameIDFormat'] = idp_name_id_format

break
entity_descriptor_node = entity_descriptor_nodes[0]
idp_descriptor_nodes = OneLogin_Saml2_XML.query(entity_descriptor_node, './md:IDPSSODescriptor')
if len(idp_descriptor_nodes) > 0:
idp_descriptor_node = idp_descriptor_nodes[0]

idp_entity_id = entity_descriptor_node.get('entityID', None)

want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None)

name_id_format_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, './md:NameIDFormat')
if len(name_id_format_nodes) > 0:
idp_name_id_format = name_id_format_nodes[0].text

sso_nodes = OneLogin_Saml2_XML.query(
idp_descriptor_node,
"./md:SingleSignOnService[@Binding='%s']" % required_sso_binding
)

if len(sso_nodes) > 0:
idp_sso_url = sso_nodes[0].get('Location', None)

slo_nodes = OneLogin_Saml2_XML.query(
idp_descriptor_node,
"./md:SingleLogoutService[@Binding='%s']" % required_slo_binding
)

if len(slo_nodes) > 0:
idp_slo_url = slo_nodes[0].get('Location', None)

signing_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
encryption_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")

if len(signing_nodes) > 0 or len(encryption_nodes) > 0:
certs = {}
if len(signing_nodes) > 0:
certs['signing'] = []
for cert_node in signing_nodes:
certs['signing'].append(''.join(cert_node.text.split()))
if len(encryption_nodes) > 0:
certs['encryption'] = []
for cert_node in encryption_nodes:
certs['encryption'].append(''.join(cert_node.text.split()))

data['idp'] = {}

if idp_entity_id is not None:
data['idp']['entityId'] = idp_entity_id

if idp_sso_url is not None:
data['idp']['singleSignOnService'] = {}
data['idp']['singleSignOnService']['url'] = idp_sso_url
data['idp']['singleSignOnService']['binding'] = required_sso_binding

if idp_slo_url is not None:
data['idp']['singleLogoutService'] = {}
data['idp']['singleLogoutService']['url'] = idp_slo_url
data['idp']['singleLogoutService']['binding'] = required_slo_binding

if want_authn_requests_signed is not None:
data['security'] = {}
data['security']['authnRequestsSigned'] = want_authn_requests_signed

if idp_name_id_format:
data['sp'] = {}
data['sp']['NameIDFormat'] = idp_name_id_format

if certs is not None:
if len(certs) == 1 or \
(('signing' in certs and len(certs['signing']) == 1) and
('encryption' in certs and len(certs['encryption']) == 1 and
certs['signing'][0] == certs['encryption'][0])):
if 'signing' in certs:
data['idp']['x509cert'] = certs['signing'][0]
else:
data['idp']['x509cert'] = certs['encryption'][0]
else:
data['idp']['x509certMulti'] = certs
return data

@staticmethod
Expand All @@ -234,6 +230,14 @@ def merge_settings(settings, new_metadata_settings):
# Guarantee to not modify original data (`settings.copy()` would not
# be sufficient, as it's just a shallow copy).
result_settings = deepcopy(settings)

# previously I will take care of cert stuff
if 'idp' in new_metadata_settings and 'idp' in result_settings:
if new_metadata_settings['idp'].get('x509cert', None) and result_settings['idp'].get('x509certMulti', None):
del result_settings['idp']['x509certMulti']
if new_metadata_settings['idp'].get('x509certMulti', None) and result_settings['idp'].get('x509cert', None):
del result_settings['idp']['x509cert']

# Merge `new_metadata_settings` into `result_settings`.
dict_deep_merge(result_settings, new_metadata_settings)
return result_settings
Expand Down
75 changes: 75 additions & 0 deletions tests/data/metadata/idp_metadata_multi_certs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?xml version="1.0"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://idp.examle.com/saml/metadata">
<IDPSSODescriptor xmlns:ds="http://www.w3.org/2000/09/xmldsig#" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<KeyDescriptor use="encryption">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</KeyDescriptor>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.examle.com/saml/slo"/>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.examle.com/saml/sso"/>
</IDPSSODescriptor>
</EntityDescriptor>
Loading

0 comments on commit 650d2b9

Please sign in to comment.