Skip to content

Commit 6f4542d

Browse files
committed
Idp Metadata parser
1 parent b3d7c5d commit 6f4542d

3 files changed

Lines changed: 246 additions & 0 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,16 @@ Auxiliary class that contains several methods
894894
* ***validate_sign*** Validates a signature (Message or Assertion).
895895
* ***validate_binary_sign*** Validates signed bynary data (Used to validate GET Signature).
896896

897+
####OneLogin_Saml2_IdPMetadataParser - idp_metadata_parser.py####
898+
899+
A class that contains methods to obtain and parse metadata from IdP
900+
901+
* ***get_metadata*** Get the metadata XML from the provided URL
902+
* ***parse_remote*** Get the metadata XML from the provided URL and parse it, returning a dict with extracted data
903+
* ***parse*** Parse the Identity Provider metadata and returns a dict with extracted data
904+
* ***merge_settings*** Will update the settings with the provided new settings data extracted from the IdP metadata
905+
906+
897907
For more info, look at the source code; each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided.
898908

899909
Demos included in the toolkit
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# -*- coding: utf-8 -*-
2+
3+
""" OneLogin_Saml2_IdPMetadataParser class
4+
Copyright (c) 2014, OneLogin, Inc.
5+
All rights reserved.
6+
Metadata class of OneLogin's Python Toolkit.
7+
"""
8+
9+
try:
10+
import urllib.request as urllib2
11+
except ImportError:
12+
import urllib2
13+
14+
from onelogin.saml2.constants import OneLogin_Saml2_Constants
15+
from onelogin.saml2.xml_utils import OneLogin_Saml2_XML
16+
17+
18+
class OneLogin_Saml2_IdPMetadataParser(object):
19+
"""
20+
A class that contains methods to obtain and parse metadata from IdP
21+
"""
22+
23+
@staticmethod
24+
def get_metadata(url):
25+
"""
26+
Get the metadata XML from the provided URL
27+
:param url: Url where the XML of the Identity Provider Metadata is published.
28+
:type url: string
29+
:returns: metadata XML
30+
:rtype: string
31+
"""
32+
valid = False
33+
response = urllib2.urlopen(url)
34+
xml = response.read()
35+
36+
if xml:
37+
try:
38+
dom = OneLogin_Saml2_XML.to_etree(xml)
39+
idp_descriptor_nodes = OneLogin_Saml2_XML.query(dom, '//md:IDPSSODescriptor')
40+
if idp_descriptor_nodes:
41+
valid = True
42+
except:
43+
pass
44+
45+
if not valid:
46+
raise Exception('Not valid IdP XML found from URL: %s' % (url))
47+
48+
return xml
49+
50+
@staticmethod
51+
def parse_remote(url):
52+
"""
53+
Get the metadata XML from the provided URL and parse it, returning a dict with extracted data
54+
:param url: Url where the XML of the Identity Provider Metadata is published.
55+
:type url: string
56+
:returns: settings dict with extracted data
57+
:rtype: dict
58+
"""
59+
idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url)
60+
return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata)
61+
62+
@staticmethod
63+
def parse(idp_metadata):
64+
"""
65+
Parse the Identity Provider metadata and returns a dict with extracted data
66+
If there are multiple IDPSSODescriptor it will only parse the first
67+
:param idp_metadata: XML of the Identity Provider Metadata.
68+
:type idp_metadata: string
69+
:param url: If true and the URL is HTTPs, the cert of the domain is checked.
70+
:type url: bool
71+
:returns: settings dict with extracted data
72+
:rtype: string
73+
"""
74+
data = {}
75+
76+
dom = OneLogin_Saml2_XML.to_etree(idp_metadata)
77+
entity_descriptor_nodes = OneLogin_Saml2_XML.query(dom, '//md:EntityDescriptor')
78+
79+
idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = idp_x509_cert = None
80+
81+
if len(entity_descriptor_nodes) > 0:
82+
for entity_descriptor_node in entity_descriptor_nodes:
83+
idp_descriptor_nodes = OneLogin_Saml2_XML.query(entity_descriptor_node, './md:IDPSSODescriptor')
84+
if len(idp_descriptor_nodes) > 0:
85+
idp_descriptor_node = idp_descriptor_nodes[0]
86+
87+
idp_entity_id = entity_descriptor_node.get('entityID', None)
88+
89+
want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None)
90+
91+
name_id_format_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, './md:NameIDFormat')
92+
if len(name_id_format_nodes) > 0:
93+
idp_name_id_format = name_id_format_nodes[0].text
94+
95+
sso_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, "./md:SingleSignOnService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT)
96+
if len(sso_nodes) > 0:
97+
idp_sso_url = sso_nodes[0].get('Location', None)
98+
99+
slo_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, "./md:SingleLogoutService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT)
100+
if len(slo_nodes) > 0:
101+
idp_slo_url = slo_nodes[0].get('Location', None)
102+
103+
cert_nodes = OneLogin_Saml2_XML.query(idp_descriptor_node, "./md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
104+
if len(cert_nodes) > 0:
105+
idp_x509_cert = cert_nodes[0].text
106+
107+
data['idp'] = {}
108+
109+
if idp_entity_id is not None:
110+
data['idp']['entityId'] = idp_entity_id
111+
if idp_sso_url is not None:
112+
data['idp']['singleLogoutService'] = {}
113+
data['idp']['singleLogoutService']['url'] = idp_sso_url
114+
if idp_slo_url is not None:
115+
data['idp']['singleLogoutService'] = {}
116+
data['idp']['singleLogoutService']['url'] = idp_slo_url
117+
if idp_x509_cert is not None:
118+
data['idp']['x509cert'] = idp_x509_cert
119+
120+
if want_authn_requests_signed is not None:
121+
data['security'] = {}
122+
data['security']['authnRequestsSigned'] = want_authn_requests_signed
123+
124+
if idp_name_id_format:
125+
data['sp'] = {}
126+
data['sp']['NameIDFormat'] = idp_name_id_format
127+
128+
break
129+
return data
130+
131+
@staticmethod
132+
def merge_settings(settings, new_metadata_settings):
133+
"""
134+
Will update the settings with the provided new settings data extracted from the IdP metadata
135+
:param settings: Current settings dict data
136+
:type settings: string
137+
:param new_metadata_settings: Settings to be merged (extracted from IdP metadata after parsing)
138+
:type new_metadata_settings: string
139+
:returns: merged settings
140+
:rtype: dict
141+
"""
142+
result_settings = settings.copy()
143+
result_settings.update(new_metadata_settings)
144+
return result_settings
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright (c) 2014, OneLogin, Inc.
4+
# All rights reserved.
5+
6+
7+
import json
8+
from os.path import dirname, join, exists
9+
from lxml.etree import XMLSyntaxError
10+
import unittest
11+
12+
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
13+
14+
15+
class OneLogin_Saml2_IdPMetadataParser_Test(unittest.TestCase):
16+
data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
17+
settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
18+
19+
def loadSettingsJSON(self, filename='settings1.json'):
20+
filename = join(self.settings_path, filename)
21+
if exists(filename):
22+
stream = open(filename, 'r')
23+
settings = json.load(stream)
24+
stream.close()
25+
return settings
26+
else:
27+
raise Exception('Settings json file does not exist')
28+
29+
def file_contents(self, filename):
30+
f = open(filename, 'r')
31+
content = f.read()
32+
f.close()
33+
return content
34+
35+
def testGetMetadata(self):
36+
"""
37+
Tests the get_metadata method of the OneLogin_Saml2_IdPMetadataParser
38+
"""
39+
with self.assertRaises(Exception):
40+
data = OneLogin_Saml2_IdPMetadataParser.get_metadata('http://google.es')
41+
42+
data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://www.testshib.org/metadata/testshib-providers.xml')
43+
self.assertTrue(data is not None and data is not {})
44+
45+
def testParseRemote(self):
46+
"""
47+
Tests the parse_remote method of the OneLogin_Saml2_IdPMetadataParser
48+
"""
49+
with self.assertRaises(Exception):
50+
data = OneLogin_Saml2_IdPMetadataParser.parse_remote('http://google.es')
51+
52+
data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml')
53+
self.assertTrue(data is not None and data is not {})
54+
expected_data = {'sp': {'NameIDFormat': 'urn:mace:shibboleth:1.0:nameIdentifier'}, 'idp': {'singleLogoutService': {'url': 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'}, 'entityId': 'https://idp.testshib.org/idp/shibboleth'}}
55+
self.assertEqual(expected_data, data)
56+
57+
def testParse(self):
58+
"""
59+
Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
60+
"""
61+
with self.assertRaises(XMLSyntaxError):
62+
data = OneLogin_Saml2_IdPMetadataParser.parse('')
63+
64+
xml_sp_metadata = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml'))
65+
data = OneLogin_Saml2_IdPMetadataParser.parse(xml_sp_metadata)
66+
self.assertEqual({}, data)
67+
68+
xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml'))
69+
data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
70+
expected_data = {'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, 'idp': {'singleLogoutService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}}
71+
self.assertEqual(expected_data, data)
72+
73+
def testMergeSettings(self):
74+
"""
75+
Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser
76+
"""
77+
with self.assertRaises(AttributeError):
78+
settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(None, {})
79+
80+
with self.assertRaises(TypeError):
81+
settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings({}, None)
82+
83+
xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml'))
84+
data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
85+
settings = self.loadSettingsJSON()
86+
settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data)
87+
expected_data = {u'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, u'idp': {'singleLogoutService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'}
88+
self.assertEqual(expected_data, settings_result)
89+
90+
expected_data2 = {'sp': {u'singleLogoutService': {u'url': u'http://stuff.com/endpoints/endpoints/sls.php'}, u'assertionConsumerService': {u'url': u'http://stuff.com/endpoints/endpoints/acs.php'}, u'entityId': u'http://stuff.com/endpoints/metadata.php', u'NameIDFormat': u'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified'}, 'idp': {u'singleLogoutService': {u'url': u'http://idp.example.com/SingleLogoutService.php'}, u'entityId': u'http://idp.example.com/', u'x509cert': u'MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo', u'singleSignOnService': {u'url': u'http://idp.example.com/SSOService.php'}}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'}
91+
settings_result2 = OneLogin_Saml2_IdPMetadataParser.merge_settings(data, settings)
92+
self.assertEqual(expected_data2, settings_result2)

0 commit comments

Comments
 (0)