diff --git a/.gitignore b/.gitignore index 2059549..65bebc8 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ tmp* *.tmpl *.iml _build/ +.cache +*.swp example/idp3/htdocs/login.mako diff --git a/doc/howto/config.rst b/doc/howto/config.rst index 4412b57..1819f6f 100644 --- a/doc/howto/config.rst +++ b/doc/howto/config.rst @@ -270,6 +270,19 @@ idp/aa Directives that are specific to an IdP or AA service instance +sign_assertion +"""""""""""""" + +Specifies if the IdP should sign the assertion in an authentication response +or not. Can be True or False. Default is False. + +sign_response +""""""""""""" + +Specifies if the IdP should sign the authentication response or not. Can be +True or False. Default is False. + + policy """""" @@ -419,7 +432,7 @@ Indicates if this SP wants the IdP to send the assertions signed. This sets the WantAssertionsSigned attribute of the SPSSODescriptor node of the metadata so the IdP will know this SP preference. -Valid values are True or False. Default value is True. +Valid values are True or False. Default value is False. Example:: diff --git a/src/saml2/algsupport.py b/src/saml2/algsupport.py index a2d3213..f9bc06b 100644 --- a/src/saml2/algsupport.py +++ b/src/saml2/algsupport.py @@ -42,7 +42,7 @@ def get_algorithm_support(xmlsec): pof.wait() if not p_err: - p = p_out.split('\n') + p = p_out.splitlines() algs = [x.strip('"') for x in p[1].split(',')] digest = [] signing = [] diff --git a/src/saml2/assertion.py b/src/saml2/assertion.py index adfeecd..64944d1 100644 --- a/src/saml2/assertion.py +++ b/src/saml2/assertion.py @@ -615,7 +615,7 @@ def _authn_context_decl_ref(decl_ref, authn_auth=None): def authn_statement(authn_class=None, authn_auth=None, authn_decl=None, authn_decl_ref=None, authn_instant="", - subject_locality=""): + subject_locality="", session_not_on_or_after=None): """ Construct the AuthnStatement :param authn_class: Authentication Context Class reference @@ -639,6 +639,7 @@ def authn_statement(authn_class=None, authn_auth=None, saml.AuthnStatement, authn_instant=_instant, session_index=sid(), + session_not_on_or_after=session_not_on_or_after, authn_context=_authn_context_class_ref( authn_class, authn_auth)) elif authn_decl: @@ -646,19 +647,22 @@ def authn_statement(authn_class=None, authn_auth=None, saml.AuthnStatement, authn_instant=_instant, session_index=sid(), + session_not_on_or_after=session_not_on_or_after, authn_context=_authn_context_decl(authn_decl, authn_auth)) elif authn_decl_ref: res = factory( saml.AuthnStatement, authn_instant=_instant, session_index=sid(), + session_not_on_or_after=session_not_on_or_after, authn_context=_authn_context_decl_ref(authn_decl_ref, authn_auth)) else: res = factory( saml.AuthnStatement, authn_instant=_instant, - session_index=sid()) + session_index=sid(), + session_not_on_or_after=session_not_on_or_after) if subject_locality: res.subject_locality = saml.SubjectLocality(text=subject_locality) @@ -719,7 +723,7 @@ class Assertion(dict): authn_class=None, authn_auth=None, authn_decl=None, encrypt=None, sec_context=None, authn_decl_ref=None, authn_instant="", subject_locality="", authn_statem=None, - name_id=None): + name_id=None, session_not_on_or_after=None): """ Construct the Assertion :param sp_entity_id: The entityid of the SP @@ -770,7 +774,8 @@ class Assertion(dict): _authn_statement = authn_statement(authn_class, authn_auth, authn_decl, authn_decl_ref, authn_instant, - subject_locality) + subject_locality, + session_not_on_or_after=session_not_on_or_after) else: _authn_statement = None diff --git a/src/saml2/attributemaps/basic.py b/src/saml2/attributemaps/basic.py index 27ab83d..c05b6e9 100644 --- a/src/saml2/attributemaps/basic.py +++ b/src/saml2/attributemaps/basic.py @@ -61,7 +61,7 @@ MAP = { DEF+'eduPersonScopedAffiliation': 'eduPersonScopedAffiliation', DEF+'eduPersonTargetedID': 'eduPersonTargetedID', DEF+'eduPersonAssurance': 'eduPersonAssurance', - DEF+'eduPersonUniqueID': 'eduPersonUniqueID', + DEF+'eduPersonUniqueId': 'eduPersonUniqueId', DEF+'eduPersonOrcid': 'eduPersonOrcid', DEF+'email': 'email', DEF+'emailAddress': 'emailAddress', @@ -228,7 +228,7 @@ MAP = { 'eduPersonScopedAffiliation': DEF+'eduPersonScopedAffiliation', 'eduPersonTargetedID': DEF+'eduPersonTargetedID', 'eduPersonAssurance': DEF+'eduPersonAssurance', - 'eduPersonUniqueID': DEF+'eduPersonUniqueID', + 'eduPersonUniqueId': DEF+'eduPersonUniqueId', 'eduPersonOrcid': DEF+'eduPersonOrcid', 'email': DEF+'email', 'emailAddress': DEF+'emailAddress', diff --git a/src/saml2/attributemaps/saml_uri.py b/src/saml2/attributemaps/saml_uri.py index afdfee2..7eb8928 100644 --- a/src/saml2/attributemaps/saml_uri.py +++ b/src/saml2/attributemaps/saml_uri.py @@ -19,19 +19,19 @@ MAP = { EDUCOURSE_OID+'1': 'eduCourseOffering', EDUCOURSE_OID+'2': 'eduCourseMember', EDUPERSON_OID+'1': 'eduPersonAffiliation', - EDUPERSON_OID+'2': 'eduPersonEntitlement', - EDUPERSON_OID+'3': 'eduPersonNickname', - EDUPERSON_OID+'4': 'eduPersonOrgDN', - EDUPERSON_OID+'5': 'eduPersonOrgUnitDN', - EDUPERSON_OID+'6': 'eduPersonPrimaryAffiliation', - EDUPERSON_OID+'7': 'eduPersonPrimaryOrgUnitDN', - EDUPERSON_OID+'8': 'eduPersonPrincipalName', - EDUPERSON_OID+'9': 'eduPersonPrincipalName', - EDUPERSON_OID+'10': 'eduPersonScopedAffiliation', - EDUPERSON_OID+'11': 'eduPersonTargetedID', - EDUPERSON_OID+'12': 'eduPersonAssurance', - EDUPERSON_OID+'13': 'eduPersonUniqueID', - EDUPERSON_OID+'14': 'eduPersonOrcid', + EDUPERSON_OID+'2': 'eduPersonNickname', + EDUPERSON_OID+'3': 'eduPersonOrgDN', + EDUPERSON_OID+'4': 'eduPersonOrgUnitDN', + EDUPERSON_OID+'5': 'eduPersonPrimaryAffiliation', + EDUPERSON_OID+'6': 'eduPersonPrincipalName', + EDUPERSON_OID+'7': 'eduPersonEntitlement', + EDUPERSON_OID+'8': 'eduPersonPrimaryOrgUnitDN', + EDUPERSON_OID+'9': 'eduPersonScopedAffiliation', + EDUPERSON_OID+'10': 'eduPersonTargetedID', + EDUPERSON_OID+'11': 'eduPersonAssurance', + EDUPERSON_OID+'12': 'eduPersonPrincipalNamePrior', + EDUPERSON_OID+'13': 'eduPersonUniqueId', + EDUPERSON_OID+'16': 'eduPersonOrcid', LDAPGVAT_OID+'1': 'PVP-GID', LDAPGVAT_OID+'149': 'PVP-BPK', LDAPGVAT_OID+'153': 'PVP-OU-OKZ', @@ -179,19 +179,19 @@ MAP = { 'eduCourseMember': EDUCOURSE_OID+'2', 'eduCourseOffering': EDUCOURSE_OID+'1', 'eduPersonAffiliation': EDUPERSON_OID+'1', - 'eduPersonEntitlement': EDUPERSON_OID+'2', - 'eduPersonNickname': EDUPERSON_OID+'3', - 'eduPersonOrgDN': EDUPERSON_OID+'4', - 'eduPersonOrgUnitDN': EDUPERSON_OID+'5', - 'eduPersonPrimaryAffiliation': EDUPERSON_OID+'6', - 'eduPersonPrimaryOrgUnitDN': EDUPERSON_OID+'7', - 'eduPersonPrincipalName': EDUPERSON_OID+'8', - 'eduPersonPrincipalNamePrior': EDUPERSON_OID+'9', - 'eduPersonScopedAffiliation': EDUPERSON_OID+'10', - 'eduPersonTargetedID': EDUPERSON_OID+'11', - 'eduPersonAssurance': EDUPERSON_OID+'12', - 'eduPersonUniqueID': EDUPERSON_OID+'13', - 'eduPersonOrcid': EDUPERSON_OID+'14', + 'eduPersonEntitlement': EDUPERSON_OID+'7', + 'eduPersonNickname': EDUPERSON_OID+'2', + 'eduPersonOrgDN': EDUPERSON_OID+'3', + 'eduPersonOrgUnitDN': EDUPERSON_OID+'4', + 'eduPersonPrimaryAffiliation': EDUPERSON_OID+'5', + 'eduPersonPrimaryOrgUnitDN': EDUPERSON_OID+'8', + 'eduPersonPrincipalName': EDUPERSON_OID+'6', + 'eduPersonPrincipalNamePrior': EDUPERSON_OID+'12', + 'eduPersonScopedAffiliation': EDUPERSON_OID+'9', + 'eduPersonTargetedID': EDUPERSON_OID+'10', + 'eduPersonAssurance': EDUPERSON_OID+'11', + 'eduPersonUniqueId': EDUPERSON_OID+'13', + 'eduPersonOrcid': EDUPERSON_OID+'16', 'email': PKCS_9+'1', 'employeeNumber': NETSCAPE_LDAP+'3', 'employeeType': NETSCAPE_LDAP+'4', diff --git a/src/saml2/attributemaps/shibboleth_uri.py b/src/saml2/attributemaps/shibboleth_uri.py index 81823a9..54de473 100644 --- a/src/saml2/attributemaps/shibboleth_uri.py +++ b/src/saml2/attributemaps/shibboleth_uri.py @@ -11,19 +11,19 @@ MAP = { "identifier": "urn:mace:shibboleth:1.0:attributeNamespace:uri", 'fro': { EDUPERSON_OID+'1': 'eduPersonAffiliation', - EDUPERSON_OID+'2': 'eduPersonEntitlement', - EDUPERSON_OID+'3': 'eduPersonNickname', - EDUPERSON_OID+'4': 'eduPersonOrgDN', - EDUPERSON_OID+'5': 'eduPersonOrgUnitDN', - EDUPERSON_OID+'6': 'eduPersonPrimaryAffiliation', - EDUPERSON_OID+'7': 'eduPersonPrimaryOrgUnitDN', - EDUPERSON_OID+'8': 'eduPersonPrincipalName', - EDUPERSON_OID+'9': 'eduPersonPrincipalNamePrior', - EDUPERSON_OID+'10': 'eduPersonScopedAffiliation', - EDUPERSON_OID+'11': 'eduPersonTargetedID', - EDUPERSON_OID+'12': 'eduPersonAssurance', - EDUPERSON_OID+'13': 'eduPersonUniqueID', - EDUPERSON_OID+'14': 'eduPersonOrcid', + EDUPERSON_OID+'2': 'eduPersonNickname', + EDUPERSON_OID+'3': 'eduPersonOrgDN', + EDUPERSON_OID+'4': 'eduPersonOrgUnitDN', + EDUPERSON_OID+'5': 'eduPersonPrimaryAffiliation', + EDUPERSON_OID+'6': 'eduPersonPrincipalName', + EDUPERSON_OID+'7': 'eduPersonEntitlement', + EDUPERSON_OID+'8': 'eduPersonPrimaryOrgUnitDN', + EDUPERSON_OID+'9': 'eduPersonScopedAffiliation', + EDUPERSON_OID+'10': 'eduPersonTargetedID', + EDUPERSON_OID+'11': 'eduPersonAssurance', + EDUPERSON_OID+'12': 'eduPersonPrincipalNamePrior', + EDUPERSON_OID+'13': 'eduPersonUniqueId', + EDUPERSON_OID+'16': 'eduPersonOrcid', NETSCAPE_LDAP+'1': 'carLicense', NETSCAPE_LDAP+'2': 'departmentNumber', NETSCAPE_LDAP+'3': 'employeeNumber', @@ -114,19 +114,19 @@ MAP = { 'dnQualifier': X500ATTR+'46', 'domainComponent': UCL_DIR_PILOT+'25', 'eduPersonAffiliation': EDUPERSON_OID+'1', - 'eduPersonEntitlement': EDUPERSON_OID+'2', - 'eduPersonNickname': EDUPERSON_OID+'3', - 'eduPersonOrgDN': EDUPERSON_OID+'4', - 'eduPersonOrgUnitDN': EDUPERSON_OID+'5', - 'eduPersonPrimaryAffiliation': EDUPERSON_OID+'6', - 'eduPersonPrimaryOrgUnitDN': EDUPERSON_OID+'7', - 'eduPersonPrincipalName': EDUPERSON_OID+'8', - 'eduPersonPrincipalNamePrior': EDUPERSON_OID+'9', - 'eduPersonScopedAffiliation': EDUPERSON_OID+'10', - 'eduPersonTargetedID': EDUPERSON_OID+'11', - 'eduPersonAssurance': EDUPERSON_OID+'12', - 'eduPersonUniqueID': EDUPERSON_OID+'13', - 'eduPersonOrcid': EDUPERSON_OID+'14', + 'eduPersonEntitlement': EDUPERSON_OID+'7', + 'eduPersonNickname': EDUPERSON_OID+'2', + 'eduPersonOrgDN': EDUPERSON_OID+'3', + 'eduPersonOrgUnitDN': EDUPERSON_OID+'4', + 'eduPersonPrimaryAffiliation': EDUPERSON_OID+'5', + 'eduPersonPrimaryOrgUnitDN': EDUPERSON_OID+'8', + 'eduPersonPrincipalName': EDUPERSON_OID+'6', + 'eduPersonPrincipalNamePrior': EDUPERSON_OID+'12', + 'eduPersonScopedAffiliation': EDUPERSON_OID+'9', + 'eduPersonTargetedID': EDUPERSON_OID+'10', + 'eduPersonAssurance': EDUPERSON_OID+'11', + 'eduPersonUniqueId': EDUPERSON_OID+'13', + 'eduPersonOrcid': EDUPERSON_OID+'16', 'email': PKCS_9+'1', 'emailAddress': PKCS_9+'1', 'employeeNumber': NETSCAPE_LDAP+'3', diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py index 621752b..ce734a5 100644 --- a/src/saml2/mdstore.py +++ b/src/saml2/mdstore.py @@ -902,6 +902,9 @@ class MetadataStore(MetaData): elif typ == "loader": key = args[1] _md = MetaDataLoader(self.attrc, args[1], **_args) + elif typ == "mdq": + key = args[1] + _md = MetaDataMDX(args[1]) else: raise SAMLError("Unknown metadata type '%s'" % typ) _md.load() @@ -992,10 +995,7 @@ class MetadataStore(MetaData): try: srvs = _md[entity_id][typ] except KeyError: - return None - - if not srvs: - return srvs + continue res = [] for srv in srvs: @@ -1005,6 +1005,8 @@ class MetadataStore(MetaData): res.append(elem) return res + return None + def ext_service(self, entity_id, typ, service, binding=None): known_entity = False for key, _md in self.metadata.items(): diff --git a/src/saml2/server.py b/src/saml2/server.py index 3032f33..2dad57f 100644 --- a/src/saml2/server.py +++ b/src/saml2/server.py @@ -326,7 +326,8 @@ class Server(Entity): def setup_assertion(self, authn, sp_entity_id, in_response_to, consumer_url, name_id, policy, _issuer, authn_statement, identity, - best_effort, sign_response, farg=None, **kwargs): + best_effort, sign_response, farg=None, + session_not_on_or_after=None, **kwargs): """ Construct and return the Assertion @@ -370,17 +371,20 @@ class Server(Entity): assertion = ast.construct( sp_entity_id, self.config.attribute_converters, policy, issuer=_issuer, farg=farg['assertion'], name_id=name_id, + session_not_on_or_after=session_not_on_or_after, **authn_args) elif authn_statement: # Got a complete AuthnStatement assertion = ast.construct( sp_entity_id, self.config.attribute_converters, policy, issuer=_issuer, authn_statem=authn_statement, - farg=farg['assertion'], name_id=name_id, **kwargs) + farg=farg['assertion'], name_id=name_id, + **kwargs) else: assertion = ast.construct( sp_entity_id, self.config.attribute_converters, policy, issuer=_issuer, farg=farg['assertion'], name_id=name_id, + session_not_on_or_after=session_not_on_or_after, **kwargs) return assertion @@ -394,7 +398,7 @@ class Server(Entity): encrypt_assertion_self_contained=False, encrypted_advice_attributes=False, pefim=False, sign_alg=None, digest_alg=None, - farg=None): + farg=None, session_not_on_or_after=None): """ Create a response. A layer of indirection. :param in_response_to: The session identifier of the request @@ -455,7 +459,7 @@ class Server(Entity): assertion = self.setup_assertion( authn, sp_entity_id, in_response_to, consumer_url, name_id, policy, _issuer, authn_statement, [], True, sign_response, - farg=farg) + farg=farg, session_not_on_or_after=session_not_on_or_after) assertion.advice = saml.Advice() # assertion.advice.assertion_id_ref.append(saml.AssertionIDRef()) @@ -465,7 +469,8 @@ class Server(Entity): assertion = self.setup_assertion( authn, sp_entity_id, in_response_to, consumer_url, name_id, policy, _issuer, authn_statement, identity, True, - sign_response, farg=farg) + sign_response, farg=farg, + session_not_on_or_after=session_not_on_or_after) to_sign = [] if not encrypt_assertion: @@ -681,6 +686,7 @@ class Server(Entity): encrypt_assertion_self_contained=True, encrypted_advice_attributes=False, pefim=False, sign_alg=None, digest_alg=None, + session_not_on_or_after=None, **kwargs): """ Constructs an AuthenticationResponse @@ -741,11 +747,13 @@ class Server(Entity): return self._authn_response( in_response_to, destination, sp_entity_id, identity, authn=_authn, issuer=issuer, pefim=pefim, - sign_alg=sign_alg, digest_alg=digest_alg, **args) + sign_alg=sign_alg, digest_alg=digest_alg, + session_not_on_or_after=session_not_on_or_after, **args) return self._authn_response( in_response_to, destination, sp_entity_id, identity, authn=_authn, issuer=issuer, pefim=pefim, sign_alg=sign_alg, - digest_alg=digest_alg, **args) + digest_alg=digest_alg, + session_not_on_or_after=session_not_on_or_after, **args) except MissingValue as exc: return self.create_error_response(in_response_to, destination, @@ -756,13 +764,15 @@ class Server(Entity): name_id_policy=None, userid=None, name_id=None, authn=None, authn_decl=None, issuer=None, sign_response=False, - sign_assertion=False, **kwargs): + sign_assertion=False, + session_not_on_or_after=None, **kwargs): return self.create_authn_response(identity, in_response_to, destination, sp_entity_id, name_id_policy, userid, name_id, authn, issuer, sign_response, sign_assertion, - authn_decl=authn_decl) + authn_decl=authn_decl, + session_not_on_or_after=session_not_on_or_after) # noinspection PyUnusedLocal def create_assertion_id_request_response(self, assertion_id, sign=False, diff --git a/src/saml2/sigver.py b/src/saml2/sigver.py index 30be593..095a79c 100644 --- a/src/saml2/sigver.py +++ b/src/saml2/sigver.py @@ -586,7 +586,7 @@ def parse_xmlsec_output(output): :param output: The output from Popen :return: A boolean; True if the command was a success otherwise False """ - for line in output.split("\n"): + for line in output.splitlines(): if line == "OK": return True elif line == "FAIL": diff --git a/src/saml2/validate.py b/src/saml2/validate.py index b749773..de68fc0 100644 --- a/src/saml2/validate.py +++ b/src/saml2/validate.py @@ -91,7 +91,7 @@ def validate_on_or_after(not_on_or_after, slack): nooa = calendar.timegm(time_util.str_to_time(not_on_or_after)) if now > nooa + slack: raise ResponseLifetimeExceed( - "Can't use it, it's too old %d > %d".format(now - slack, nooa)) + "Can't use it, it's too old %d > %d" % (now - slack, nooa)) return nooa else: return False diff --git a/tests/test_20_assertion.py b/tests/test_20_assertion.py index f04883d..ae661d5 100644 --- a/tests/test_20_assertion.py +++ b/tests/test_20_assertion.py @@ -86,7 +86,7 @@ def test_filter_on_attributes_without_friendly_name(): "eduPersonAffiliation": "test", "extra": "foo"} eptid = to_dict( - Attribute(name="urn:oid:1.3.6.1.4.1.5923.1.1.1.11", + Attribute(name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10", name_format=NAME_FORMAT_URI), ONTS) ep_affiliation = to_dict( Attribute(name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1", diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py index d4dd166..aadd772 100644 --- a/tests/test_30_mdstore.py +++ b/tests/test_30_mdstore.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import datetime import re +from collections import OrderedDict from future.backports.urllib.parse import quote_plus @@ -455,5 +456,16 @@ def test_metadata_extension_algsupport(): mdf = mds.metadata[full_path("uu.xml")] assert mds + +def test_extension(): + mds = MetadataStore(ATTRCONV, None) + # use ordered dict to force expected entity to be last + metadata = OrderedDict() + metadata["1"] = {"entity1": {}} + metadata["2"] = {"entity2": {"idpsso_descriptor": [{"extensions": {"extension_elements": [{"__class__": "test"}]}}]}} + mds.metadata = metadata + assert mds.extension("entity2", "idpsso_descriptor", "test") + + if __name__ == "__main__": test_metadata_extension_algsupport() diff --git a/tests/test_40_sigver.py b/tests/test_40_sigver.py index 801454d..e2ba952 100644 --- a/tests/test_40_sigver.py +++ b/tests/test_40_sigver.py @@ -540,6 +540,19 @@ def test_sha256_signing(): assert s +def test_xmlsec_output_line_parsing(): + output1 = "prefix\nOK\npostfix" + assert sigver.parse_xmlsec_output(output1) + + output2 = "prefix\nFAIL\npostfix" + raises(sigver.XmlsecError, sigver.parse_xmlsec_output, output2) + + output3 = "prefix\r\nOK\r\npostfix" + assert sigver.parse_xmlsec_output(output3) + + output4 = "prefix\r\nFAIL\r\npostfix" + raises(sigver.XmlsecError, sigver.parse_xmlsec_output, output4) + if __name__ == "__main__": # t = TestSecurity() diff --git a/tools/mdexport.py b/tools/mdexport.py index 36becd2..a427af7 100755 --- a/tools/mdexport.py +++ b/tools/mdexport.py @@ -1,4 +1,4 @@ - #!/usr/bin/env python +#!/usr/bin/env python from saml2.sigver import _get_xmlsec_cryptobackend from saml2.sigver import SecurityContext from saml2.httpbase import HTTPBase