Skip to content

Commit 434ebf9

Browse files
committed
Merge branch 'master' of https://github.com/jimmyislive/python-saml into jimmyislive-master
2 parents 3b3ec18 + 51aa235 commit 434ebf9

9 files changed

Lines changed: 357 additions & 9 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"strict": true,
3+
"debug": true,
4+
"sp": {
5+
"entityId": "https://<sp_domain>/metadata/",
6+
"assertionConsumerService": {
7+
"url": "https://<sp_domain>/?acs",
8+
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
9+
},
10+
"attributeConsumingService": [
11+
{
12+
"isDefault": false,
13+
"serviceName": "Django Demo",
14+
"serviceDescription": "Django Name",
15+
"requestedAttributes": [ {
16+
"name": "",
17+
"nameFormat": "",
18+
"friendlyName": "",
19+
"isRequired": false,
20+
"attributeValue": [
21+
]
22+
}
23+
]
24+
}
25+
],
26+
"singleLogoutService": {
27+
"url": "https://<sp_domain>/?sls",
28+
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
29+
},
30+
"NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
31+
"x509cert": "",
32+
"privateKey": ""
33+
},
34+
"idp": {
35+
"entityId": "https://app.onelogin.com/saml/metadata/<onelogin_connector_id>",
36+
"singleSignOnService": {
37+
"url": "https://app.onelogin.com/trust/saml2/http-post/sso/<onelogin_connector_id>",
38+
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
39+
},
40+
"singleLogoutService": {
41+
"url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/<onelogin_connector_id>",
42+
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
43+
},
44+
"x509cert": "<onelogin_connector_cert>"
45+
}
46+
}

src/onelogin/saml2/authn_request.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ 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+
# TODO: Do we have to account for the case when we have multiple attributeconsumers?
92+
# like will the index be > 1?
93+
attr_consuming_service_str = 'AttributeConsumingServiceIndex="1"'
94+
8995
request = """<samlp:AuthnRequest
9096
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
9197
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
@@ -97,7 +103,8 @@ def __init__(self, settings, force_authn=False, is_passive=False):
97103
IssueInstant="%(issue_instant)s"
98104
Destination="%(destination)s"
99105
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
100-
AssertionConsumerServiceURL="%(assertion_url)s">
106+
AssertionConsumerServiceURL="%(assertion_url)s"
107+
%(attr_consuming_service_str)s>
101108
<saml:Issuer>%(entity_id)s</saml:Issuer>
102109
<samlp:NameIDPolicy
103110
Format="%(name_id_policy)s"
@@ -115,6 +122,7 @@ def __init__(self, settings, force_authn=False, is_passive=False):
115122
'entity_id': sp_data['entityId'],
116123
'name_id_policy': name_id_policy_format,
117124
'requested_authn_context_str': requested_authn_context_str,
125+
'attr_consuming_service_str': attr_consuming_service_str
118126
}
119127

120128
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: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from time import gmtime, strftime
1313
from datetime import datetime
1414
from defusedxml.minidom import parseString
15+
from lxml import etree
1516

1617
from onelogin.saml2.constants import OneLogin_Saml2_Constants
1718
from onelogin.saml2.utils import OneLogin_Saml2_Utils
@@ -27,6 +28,63 @@ class OneLogin_Saml2_Metadata(object):
2728
TIME_VALID = 172800 # 2 days
2829
TIME_CACHED = 604800 # 1 week
2930

31+
@staticmethod
32+
def add_attribute_consuming_service(root, attr_consuming_service):
33+
"""Helper function to add the AttributeConsumingService nodes"""
34+
attrib_index = 1
35+
spso_node = root.find('{%s}SPSSODescriptor' % OneLogin_Saml2_Constants.NS_MD)
36+
37+
# iterate through all the consuming services listed
38+
for acs in attr_consuming_service:
39+
acs_node = etree.SubElement(spso_node, "{%s}AttributeConsumingService" % OneLogin_Saml2_Constants.NS_MD)
40+
acs_node.set('index', str(attrib_index))
41+
try:
42+
acs_node.set('isDefault', str(acs['isDefault']).lower())
43+
except KeyError:
44+
pass
45+
46+
svc_name = etree.SubElement(acs_node, "{%s}ServiceName" % OneLogin_Saml2_Constants.NS_MD)
47+
svc_name.set('{%s}lang' % OneLogin_Saml2_Constants.XML, 'en')
48+
svc_name.text = acs['serviceName']
49+
try:
50+
svc_description = etree.SubElement(acs_node, "{%s}ServiceDescription" % OneLogin_Saml2_Constants.NS_MD)
51+
svc_description.set('{%s}lang' % OneLogin_Saml2_Constants.XML, 'en')
52+
svc_description.text = acs['serviceDescription']
53+
except KeyError:
54+
# serviceDescription is optional
55+
pass
56+
57+
# iterate through all the requested attributes of each service
58+
for req_attribs in acs['requestedAttributes']:
59+
60+
requested_attribute = etree.SubElement(acs_node, "{%s}RequestedAttribute" % OneLogin_Saml2_Constants.NS_MD)
61+
# construct the permissible attrib values, if present
62+
try:
63+
for attrib_val in req_attribs['attributeValue']:
64+
val = etree.SubElement(requested_attribute, "{%s}AttributeValue" % OneLogin_Saml2_Constants.NS_SAML)
65+
val.text = attrib_val
66+
except KeyError:
67+
# it's fine, attributeValue is an optional element
68+
pass
69+
70+
requested_attribute.set('Name', req_attribs['name'])
71+
try:
72+
requested_attribute.set('NameFormat', req_attribs['nameFormat'])
73+
except KeyError:
74+
pass
75+
76+
try:
77+
requested_attribute.set('FriendlyName', req_attribs['friendlyName'])
78+
except KeyError:
79+
pass
80+
81+
try:
82+
requested_attribute.set('isRequired', str(req_attribs['isRequired']).lower())
83+
except KeyError:
84+
pass
85+
86+
attrib_index += 1
87+
3088
@staticmethod
3189
def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=None, contacts=None, organization=None):
3290
"""
@@ -50,7 +108,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
50108
:param contacts: Contacts info
51109
:type contacts: dict
52110
53-
:param organization: Organization ingo
111+
:param organization: Organization info
54112
:type organization: dict
55113
"""
56114
if valid_until is None:
@@ -76,6 +134,11 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
76134
if organization is None:
77135
organization = {}
78136

137+
try:
138+
attr_consuming_service = sp['attributeConsumingService']
139+
except KeyError:
140+
attr_consuming_service = []
141+
79142
sls = ''
80143
if 'singleLogoutService' in sp and 'url' in sp['singleLogoutService']:
81144
sls = """ <md:SingleLogoutService Binding="%(binding)s"
@@ -119,7 +182,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
119182
str_contacts = '\n'.join(contacts_info)
120183

121184
metadata = """<?xml version="1.0"?>
122-
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
185+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" %(saml_namespace)s
123186
%(valid)s
124187
%(cache)s
125188
entityID="%(entity_id)s">
@@ -144,8 +207,16 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
144207
'sls': sls,
145208
'organization': str_organization,
146209
'contacts': str_contacts,
210+
'saml_namespace': 'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"' if attr_consuming_service else ''
147211
}
148-
return metadata
212+
213+
# i'm not sure why the above xml was build by hand. Building via lxml is way easier,
214+
# especially for conditional attributes etc..
215+
# So as a work around, i'm creating a xml dom, insert the attibute_consumer_service
216+
# nodes into it and then return the serialized xml
217+
root = etree.fromstring(metadata)
218+
OneLogin_Saml2_Metadata.add_attribute_consuming_service(root, attr_consuming_service)
219+
return etree.tostring(root, pretty_print=True)
149220

150221
@staticmethod
151222
def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1):

src/onelogin/saml2/settings.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ 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+
# attributeConsumingService is optional
261+
if 'attributeConsumingService' not in self.__sp:
262+
self.__sp['attributeConsumingService'] = []
263+
260264
if 'singleLogoutService' not in self.__sp.keys():
261265
self.__sp['singleLogoutService'] = {}
262266
if 'binding' not in self.__sp['singleLogoutService']:
@@ -436,6 +440,50 @@ def check_sp_settings(self, settings):
436440
elif not validate_url(sp['assertionConsumerService']['url']):
437441
errors.append('sp_acs_url_invalid')
438442

443+
if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']):
444+
# so we have a attributeConsumingService element...
445+
446+
# serviceName and requestedAttrib are required
447+
attribute_consuming_service = sp['attributeConsumingService']
448+
for attrib in attribute_consuming_service:
449+
if 'serviceName' not in attrib:
450+
errors.append('sp_attributeConsumingService_serviceName_not_found')
451+
if 'requestedAttributes' not in attrib:
452+
errors.append('sp_attributeConsumingService_requestedAttributes_not_found')
453+
454+
# verify that tags are of the correct types
455+
try:
456+
if type(attrib['isDefault']) != bool:
457+
errors.append('sp_attributeConsumingService_isDefault_type_invalid')
458+
except KeyError:
459+
# isDefault attribute is optional
460+
pass
461+
462+
if 'serviceName' in attrib and not isinstance(attrib['serviceName'], basestring):
463+
errors.append('sp_attributeConsumingService_serviceName_type_invalid')
464+
465+
try:
466+
if not isinstance(attrib['serviceDescription'], basestring):
467+
errors.append('sp_attributeConsumingService_serviceDescription_type_invalid')
468+
except KeyError:
469+
# serviceDescription attribute is optional
470+
pass
471+
472+
if 'requestedAttributes' in attrib:
473+
if type(attrib['requestedAttributes']) != list:
474+
errors.append('sp_attributeConsumingService_requestedAttributes_type_invalid')
475+
476+
for req_attrib in attrib['requestedAttributes']:
477+
if 'name' not in req_attrib:
478+
errors.append('sp_attributeConsumingService_requestedAttributes_name_not_found')
479+
if 'name' in req_attrib and not req_attrib['name'].strip():
480+
# name cannot be empty
481+
errors.append('sp_attributeConsumingService_requestedAttributes_name_invalid')
482+
if 'attributeValue' in req_attrib and type(req_attrib['attributeValue']) != list:
483+
errors.append('sp_attributeConsumingService_requestedAttributes_attributeValue_type_invalid')
484+
if 'isRequired' in req_attrib and type(req_attrib['isRequired']) != bool:
485+
errors.append('sp_attributeConsumingService_requestedAttributes_isRequired_type_invalid')
486+
439487
if 'singleLogoutService' in sp and \
440488
'url' in sp['singleLogoutService'] and \
441489
len(sp['singleLogoutService']['url']) > 0 and \

tests/settings/settings4.json

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

0 commit comments

Comments
 (0)