Skip to content

Commit b44e7fb

Browse files
committed
Merge branch 'jimmyislive-master'
2 parents 978ba1e + 0e2ee59 commit b44e7fb

9 files changed

Lines changed: 274 additions & 13 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,22 @@ This is the settings.json file:
223223
// HTTP-POST binding only.
224224
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
225225
},
226+
// If you need to specify requested attributes, set a
227+
// attributeConsumingService. nameFormat, attributeValue and
228+
// friendlyName can be ommited
229+
"attributeConsumingService": {
230+
"ServiceName": "SP test",
231+
"serviceDescription": "Test Service",
232+
"requestedAttributes": [
233+
{
234+
"name": "",
235+
"isRequired": false,
236+
"nameFormat": "",
237+
"friendlyName": "",
238+
"attributeValue": ""
239+
}
240+
]
241+
},
226242
// Specifies info about where and how the <Logout Response> message MUST be
227243
// returned to the requester, in this case our SP.
228244
"singleLogoutService": {

src/onelogin/saml2/authn_request.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ def __init__(self, settings, force_authn=False, is_passive=False):
8686
requested_authn_context_str += '<saml:AuthnContextClassRef>%s</saml:AuthnContextClassRef>' % authn_context
8787
requested_authn_context_str += ' </samlp:RequestedAuthnContext>'
8888

89+
attr_consuming_service_str = ''
90+
if 'attributeConsumingService' in sp_data and sp_data['attributeConsumingService']:
91+
attr_consuming_service_str = 'AttributeConsumingServiceIndex="1"'
92+
8993
request = """<samlp:AuthnRequest
9094
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
9195
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
@@ -97,7 +101,8 @@ def __init__(self, settings, force_authn=False, is_passive=False):
97101
IssueInstant="%(issue_instant)s"
98102
Destination="%(destination)s"
99103
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
100-
AssertionConsumerServiceURL="%(assertion_url)s">
104+
AssertionConsumerServiceURL="%(assertion_url)s"
105+
%(attr_consuming_service_str)s>
101106
<saml:Issuer>%(entity_id)s</saml:Issuer>
102107
<samlp:NameIDPolicy
103108
Format="%(name_id_policy)s"
@@ -115,6 +120,7 @@ def __init__(self, settings, force_authn=False, is_passive=False):
115120
'entity_id': sp_data['entityId'],
116121
'name_id_policy': name_id_policy_format,
117122
'requested_authn_context_str': requested_authn_context_str,
123+
'attr_consuming_service_str': attr_consuming_service_str
118124
}
119125

120126
self.__authn_request = request

src/onelogin/saml2/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class OneLogin_Saml2_Constants(object):
2121
# Value added to the current time in time condition validations
2222
ALLOWED_CLOCK_DRIFT = 300
2323

24+
XML = 'http://www.w3.org/XML/1998/namespace'
25+
XSI = 'http://www.w3.org/2001/XMLSchema-instance'
26+
2427
# NameID Formats
2528
NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
2629
NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName'

src/onelogin/saml2/metadata.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
5050
:param contacts: Contacts info
5151
:type contacts: dict
5252
53-
:param organization: Organization ingo
53+
:param organization: Organization info
5454
:type organization: dict
5555
"""
5656
if valid_until is None:
@@ -76,6 +76,55 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
7676
if organization is None:
7777
organization = {}
7878

79+
str_attribute_consuming_service = ''
80+
81+
if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']):
82+
attr_cs_desc_str = ''
83+
if "serviceDescription" in sp['attributeConsumingService']:
84+
attr_cs_desc_str = """ <md:ServiceDescription xml:lang="en">%s</md:ServiceDescription>
85+
""" % sp['attributeConsumingService']['serviceDescription']
86+
87+
requested_attribute_data = []
88+
for req_attribs in sp['attributeConsumingService']['requestedAttributes']:
89+
req_attr_nameformat_str = req_attr_friendlyname_str = req_attr_isrequired_str = ''
90+
req_attr_aux_str = ' \>'
91+
92+
if 'nameFormat' in req_attribs.keys() and req_attribs['nameFormat']:
93+
req_attr_nameformat_str = " NameFormat=\"%s\"" % req_attribs['nameFormat']
94+
if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']:
95+
req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName']
96+
if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']:
97+
req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired']
98+
if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']:
99+
req_attr_aux_str = """ >
100+
<saml:AttributeValue xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion>%(attributeValue)</saml:AttributeValue>
101+
</md:RequestedAttribute>""" % \
102+
{
103+
'attributeValue': req_attribs['attributeValue']
104+
}
105+
106+
requested_attribute = """ <md:RequestedAttribute Name="%(req_attr_name)s"%(req_attr_nameformat_str)s%(req_attr_isrequired_str)s%(req_attr_aux_str)s""" % \
107+
{
108+
'req_attr_name': req_attribs['name'],
109+
'req_attr_nameformat_str': req_attr_nameformat_str,
110+
'req_attr_friendlyname_str': req_attr_friendlyname_str,
111+
'req_attr_isrequired_str': req_attr_isrequired_str,
112+
'req_attr_aux_str': req_attr_aux_str
113+
}
114+
115+
requested_attribute_data.append(requested_attribute)
116+
117+
str_attribute_consuming_service = """ <md:AttributeConsumingService index="1">
118+
<md:ServiceName xml:lang="en">%(service_name)s</md:ServiceName>
119+
%(attr_cs_desc)s%(requested_attribute_str)s
120+
</md:AttributeConsumingService>
121+
""" % \
122+
{
123+
'service_name': sp['attributeConsumingService']['serviceName'],
124+
'attr_cs_desc': attr_cs_desc_str,
125+
'requested_attribute_str': '\n'.join(requested_attribute_data)
126+
}
127+
79128
sls = ''
80129
if 'singleLogoutService' in sp and 'url' in sp['singleLogoutService']:
81130
sls = """ <md:SingleLogoutService Binding="%(binding)s"
@@ -100,7 +149,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
100149
org_data = '\n'.join(organization_names) + '\n' + '\n'.join(organization_displaynames) + '\n' + '\n'.join(organization_urls)
101150
str_organization = """ <md:Organization>
102151
%(org)s
103-
</md:Organization>""" % {'org': org_data}
152+
</md:Organization>\n""" % {'org': org_data}
104153

105154
str_contacts = ''
106155
if len(contacts) > 0:
@@ -116,7 +165,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
116165
'email': info['emailAddress'],
117166
}
118167
contacts_info.append(contact)
119-
str_contacts = '\n'.join(contacts_info)
168+
str_contacts = '\n'.join(contacts_info) + '\n'
120169

121170
metadata = """<?xml version="1.0"?>
122171
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
@@ -128,10 +177,8 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
128177
<md:AssertionConsumerService Binding="%(binding)s"
129178
Location="%(location)s"
130179
index="1" />
131-
</md:SPSSODescriptor>
132-
%(organization)s
133-
%(contacts)s
134-
</md:EntityDescriptor>""" % \
180+
%(attribute_consuming_service)s </md:SPSSODescriptor>
181+
%(organization)s%(contacts)s</md:EntityDescriptor>""" % \
135182
{
136183
'valid': ('validUntil="%s"' % valid_until_str) if valid_until_str else '',
137184
'cache': ('cacheDuration="%s"' % cache_duration_str) if cache_duration_str else '',
@@ -144,6 +191,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
144191
'sls': sls,
145192
'organization': str_organization,
146193
'contacts': str_contacts,
194+
'attribute_consuming_service': str_attribute_consuming_service
147195
}
148196
return metadata
149197

src/onelogin/saml2/settings.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,9 @@ def __add_default_values(self):
257257
if 'binding' not in self.__sp['assertionConsumerService'].keys():
258258
self.__sp['assertionConsumerService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_POST
259259

260+
if 'attributeConsumingService' not in self.__sp.keys():
261+
self.__sp['attributeConsumingService'] = {}
262+
260263
if 'singleLogoutService' not in self.__sp.keys():
261264
self.__sp['singleLogoutService'] = {}
262265
if 'binding' not in self.__sp['singleLogoutService']:
@@ -436,6 +439,31 @@ def check_sp_settings(self, settings):
436439
elif not validate_url(sp['assertionConsumerService']['url']):
437440
errors.append('sp_acs_url_invalid')
438441

442+
if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']):
443+
attributeConsumingService = sp['attributeConsumingService']
444+
if 'serviceName' not in attributeConsumingService:
445+
errors.append('sp_attributeConsumingService_serviceName_not_found')
446+
elif not isinstance(attributeConsumingService['serviceName'], basestring):
447+
errors.append('sp_attributeConsumingService_serviceName_type_invalid')
448+
449+
if 'requestedAttributes' not in attributeConsumingService:
450+
errors.append('sp_attributeConsumingService_requestedAttributes_not_found')
451+
elif not isinstance(attributeConsumingService['requestedAttributes'], list):
452+
errors.append('sp_attributeConsumingService_serviceName_type_invalid')
453+
else:
454+
for req_attrib in attributeConsumingService['requestedAttributes']:
455+
if 'name' not in req_attrib:
456+
errors.append('sp_attributeConsumingService_requestedAttributes_name_not_found')
457+
if 'name' in req_attrib and not req_attrib['name'].strip():
458+
errors.append('sp_attributeConsumingService_requestedAttributes_name_invalid')
459+
if 'attributeValue' in req_attrib and type(req_attrib['attributeValue']) != list:
460+
errors.append('sp_attributeConsumingService_requestedAttributes_attributeValue_type_invalid')
461+
if 'isRequired' in req_attrib and type(req_attrib['isRequired']) != bool:
462+
errors.append('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid')
463+
464+
if "serviceDescription" in attributeConsumingService and not isinstance(attributeConsumingService['serviceDescription'], basestring):
465+
errors.append('sp_attributeConsumingService_serviceDescription_type_invalid')
466+
439467
if 'singleLogoutService' in sp and \
440468
'url' in sp['singleLogoutService'] and \
441469
len(sp['singleLogoutService']['url']) > 0 and \

tests/settings/settings4.json

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"strict": false,
3+
"debug": false,
4+
"custom_base_path": "../../../tests/data/customPath/",
5+
"sp": {
6+
"entityId": "http://pytoolkit.com:8000/metadata/",
7+
"assertionConsumerService": {
8+
"url": "http://pytoolkit.com:8000/?acs"
9+
},
10+
"attributeConsumingService": {
11+
"isDefault": false,
12+
"serviceName": "Test Service",
13+
"serviceDescription": "Test Service",
14+
"requestedAttributes": [ {
15+
"name": "urn:oid:2.5.4.42",
16+
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
17+
"friendlyName": "givenName",
18+
"isRequired": false
19+
},
20+
{
21+
"name": "urn:oid:2.5.4.4",
22+
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
23+
"friendlyName": "sn",
24+
"isRequired": false
25+
},
26+
{
27+
"name": "urn:oid:2.16.840.1.113730.3.1.241",
28+
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
29+
"friendlyName": "displayName",
30+
"isRequired": false
31+
},
32+
{
33+
"name": "urn:oid:0.9.2342.19200300.100.1.3",
34+
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
35+
"friendlyName": "mail",
36+
"isRequired": false
37+
},
38+
{
39+
"name": "urn:oid:0.9.2342.19200300.100.1.1",
40+
"nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
41+
"friendlyName": "uid",
42+
"isRequired": false
43+
}
44+
]
45+
},
46+
"singleLogoutService": {
47+
"url": "http://pytoolkit.com:8000/?sls"
48+
},
49+
"NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
50+
},
51+
"idp": {
52+
"entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php",
53+
"singleSignOnService": {
54+
"url": "http://pitbulk.no-ip.org/SSOService.php"
55+
},
56+
"singleLogoutService": {
57+
"url": "http://pitbulk.no-ip.org/SingleLogoutService.php"
58+
},
59+
"x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m"
60+
},
61+
"security": {
62+
"authnRequestsSigned": false,
63+
"wantAssertionsSigned": false,
64+
"signMetadata": false
65+
},
66+
"contactPerson": {
67+
"technical": {
68+
"givenName": "technical_name",
69+
"emailAddress": "technical@example.com"
70+
},
71+
"support": {
72+
"givenName": "support_name",
73+
"emailAddress": "support@example.com"
74+
}
75+
},
76+
"organization": {
77+
"en-US": {
78+
"name": "sp_test",
79+
"displayname": "SP test",
80+
"url": "http://sp.example.com"
81+
}
82+
}
83+
}

tests/src/OneLogin/saml2_tests/authn_request_test.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020

2121
class OneLogin_Saml2_Authn_Request_Test(unittest.TestCase):
22-
def loadSettingsJSON(self):
23-
filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', 'settings1.json')
22+
def loadSettingsJSON(self, filename='settings1.json'):
23+
filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', filename)
2424
if exists(filename):
2525
stream = open(filename, 'r')
2626
settings = json.load(stream)
@@ -255,11 +255,34 @@ def testCreateEncSAMLRequest(self):
255255
inflated = decompress(decoded, -15)
256256

257257
self.assertRegexpMatches(inflated, '^<samlp:AuthnRequest')
258-
self.assertRegexpMatches(inflated, 'AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php">')
258+
self.assertRegexpMatches(inflated, 'AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php"')
259259
self.assertRegexpMatches(inflated, '<saml:Issuer>http://stuff.com/endpoints/metadata.php</saml:Issuer>')
260260
self.assertRegexpMatches(inflated, 'Format="urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"')
261261
self.assertRegexpMatches(inflated, 'ProviderName="SP prueba"')
262262

263+
def testAttributeConsumingService(self):
264+
"""
265+
Tests that the attributeConsumingServiceIndex is present as an attribute
266+
"""
267+
saml_settings = self.loadSettingsJSON()
268+
settings = OneLogin_Saml2_Settings(saml_settings)
269+
270+
authn_request = OneLogin_Saml2_Authn_Request(settings)
271+
authn_request_encoded = authn_request.get_request()
272+
decoded = b64decode(authn_request_encoded)
273+
inflated = decompress(decoded, -15)
274+
275+
self.assertNotIn('AttributeConsumingServiceIndex="1"', inflated)
276+
277+
saml_settings = self.loadSettingsJSON('settings4.json')
278+
settings = OneLogin_Saml2_Settings(saml_settings)
279+
280+
authn_request = OneLogin_Saml2_Authn_Request(settings)
281+
authn_request_encoded = authn_request.get_request()
282+
decoded = b64decode(authn_request_encoded)
283+
inflated = decompress(decoded, -15)
284+
285+
self.assertRegexpMatches(inflated, 'AttributeConsumingServiceIndex="1"')
263286

264287
if __name__ == '__main__':
265288
if is_running_under_teamcity():

tests/src/OneLogin/saml2_tests/metadata_test.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
class OneLogin_Saml2_Metadata_Test(unittest.TestCase):
2020
settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
2121

22-
def loadSettingsJSON(self):
23-
filename = join(self.settings_path, 'settings1.json')
22+
def loadSettingsJSON(self, filename='settings1.json'):
23+
filename = join(self.settings_path, filename)
2424
if exists(filename):
2525
stream = open(filename, 'r')
2626
settings = json.load(stream)
@@ -143,6 +143,28 @@ def testBuilder(self):
143143
parsed_datetime = strftime(r'%Y-%m-%dT%H:%M:%SZ', datetime_value.timetuple())
144144
self.assertIn('validUntil="%s"' % parsed_datetime, metadata6)
145145

146+
def testBuilderAttributeConsumingService(self):
147+
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings4.json'))
148+
sp_data = settings.get_sp_data()
149+
security = settings.get_security_data()
150+
organization = settings.get_organization()
151+
contacts = settings.get_contacts()
152+
153+
metadata = OneLogin_Saml2_Metadata.builder(
154+
sp_data, security['authnRequestsSigned'],
155+
security['wantAssertionsSigned'], None, None, contacts,
156+
organization
157+
)
158+
self.assertIn(""" <md:AttributeConsumingService index="1">
159+
<md:ServiceName xml:lang="en">Test Service</md:ServiceName>
160+
<md:ServiceDescription xml:lang="en">Test Service</md:ServiceDescription>
161+
<md:RequestedAttribute Name="urn:oid:2.5.4.42" FriendlyName="givenName" \>
162+
<md:RequestedAttribute Name="urn:oid:2.5.4.4" FriendlyName="sn" \>
163+
<md:RequestedAttribute Name="urn:oid:2.16.840.1.113730.3.1.241" FriendlyName="displayName" \>
164+
<md:RequestedAttribute Name="urn:oid:0.9.2342.19200300.100.1.3" FriendlyName="mail" \>
165+
<md:RequestedAttribute Name="urn:oid:0.9.2342.19200300.100.1.1" FriendlyName="uid" \>
166+
</md:AttributeConsumingService>""", metadata)
167+
146168
def testSignMetadata(self):
147169
"""
148170
Tests the signMetadata method of the OneLogin_Saml2_Metadata

0 commit comments

Comments
 (0)