diff options
author | Ivan Kanakarakis <ivan.kanak@gmail.com> | 2020-10-28 00:16:50 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-28 00:16:50 +0200 |
commit | 06920df3d7c1ebfdcff92716e798f91f6f9f173c (patch) | |
tree | 8accb74f2350925b0593a806c481b767bd432203 | |
parent | ca60cd969a263570c6fe0b1247e319dcba63f813 (diff) | |
parent | 59913a155ce06c6fe4e43ea1b15586e3d59bafb3 (diff) | |
download | pysaml2-06920df3d7c1ebfdcff92716e798f91f6f9f173c.tar.gz |
Merge pull request #728 from IdentityPython/feature-logout-response-location
-rw-r--r-- | src/saml2/client.py | 12 | ||||
-rw-r--r-- | src/saml2/client_base.py | 9 | ||||
-rw-r--r-- | src/saml2/entity.py | 16 | ||||
-rw-r--r-- | src/saml2/mdstore.py | 59 | ||||
-rw-r--r-- | tests/sp_slo_redirect.xml | 2 | ||||
-rw-r--r-- | tests/test_30_mdstore.py | 17 | ||||
-rw-r--r-- | tests/test_30_mdstore_old.py | 17 | ||||
-rw-r--r-- | tests/test_50_server.py | 31 | ||||
-rw-r--r-- | tests/test_76_metadata_in_mdb.py | 4 |
9 files changed, 124 insertions, 43 deletions
diff --git a/src/saml2/client.py b/src/saml2/client.py index e283420a..60b108ef 100644 --- a/src/saml2/client.py +++ b/src/saml2/client.py @@ -29,7 +29,7 @@ from saml2.client_base import Base from saml2.client_base import SignOnError from saml2.client_base import LogoutError from saml2.client_base import NoServiceDefined -from saml2.mdstore import destinations +from saml2.mdstore import locations import logging @@ -209,7 +209,7 @@ class Saml2Client(Base): logger.debug("No SLO '%s' service", binding) continue - destination = destinations(srvs)[0] + destination = next(locations(srvs), None) logger.info("destination to provider: %s", destination) try: session_info = self.users.get_info_from(name_id, @@ -374,7 +374,7 @@ class Saml2Client(Base): name_qualifier=name_qualifier)) srvs = self.metadata.authz_service(entity_id, BINDING_SOAP) - for dest in destinations(srvs): + for dest in locations(srvs): resp = self._use_soap(dest, "authz_decision_query", action=action, evidence=evidence, resource=resource, subject=subject) @@ -397,7 +397,7 @@ class Saml2Client(Base): _id_refs = [AssertionIDRef(_id) for _id in assertion_ids] - for destination in destinations(srvs): + for destination in locations(srvs): res = self._use_soap(destination, "assertion_id_request", assertion_id_refs=_id_refs, consent=consent, extensions=extensions, sign=sign) @@ -411,7 +411,7 @@ class Saml2Client(Base): srvs = self.metadata.authn_request_service(entity_id, BINDING_SOAP) - for destination in destinations(srvs): + for destination in locations(srvs): resp = self._use_soap(destination, "authn_query", consent=consent, extensions=extensions, sign=sign) if resp: @@ -461,7 +461,7 @@ class Saml2Client(Base): if srvs is []: raise SAMLError("No attribute service support at entity") - destination = destinations(srvs)[0] + destination = next(locations(srvs), None) if binding == BINDING_SOAP: return self._use_soap(destination, "attribute_query", diff --git a/src/saml2/client_base.py b/src/saml2/client_base.py index 871f3f2c..51a3a574 100644 --- a/src/saml2/client_base.py +++ b/src/saml2/client_base.py @@ -12,7 +12,7 @@ import logging from saml2.entity import Entity -from saml2.mdstore import destinations +from saml2.mdstore import locations from saml2.profile import paos, ecp from saml2.saml import NAMEID_FORMAT_PERSISTENT from saml2.saml import NAMEID_FORMAT_TRANSIENT @@ -212,7 +212,7 @@ class Base(Entity): # verify that it's in the metadata srvs = self.metadata.single_sign_on_service(entityid, binding) if srvs: - return destinations(srvs)[0] + return next(locations(srvs), None) else: logger.info("_sso_location: %s, %s", entityid, binding) raise IdpUnspecified("No IdP to send to given the premises") @@ -224,9 +224,8 @@ class Base(Entity): raise IdpUnspecified("Too many IdPs to choose from: %s" % eids) try: - srvs = self.metadata.single_sign_on_service(list(eids.keys())[0], - binding) - return destinations(srvs)[0] + srvs = self.metadata.single_sign_on_service(list(eids.keys())[0], binding) + return next(locations(srvs), None) except IndexError: raise IdpUnspecified("No IdP to send to given the premises") diff --git a/src/saml2/entity.py b/src/saml2/entity.py index 9f564ffa..c9572aef 100644 --- a/src/saml2/entity.py +++ b/src/saml2/entity.py @@ -53,7 +53,7 @@ from saml2.samlp import ArtifactResponse from saml2.samlp import Artifact from saml2.samlp import LogoutRequest from saml2.samlp import AttributeQuery -from saml2.mdstore import destinations +from saml2.mdstore import all_locations from saml2 import BINDING_HTTP_POST from saml2 import BINDING_HTTP_REDIRECT from saml2 import BINDING_SOAP @@ -249,8 +249,9 @@ class Entity(HTTPBase): return info - def pick_binding(self, service, bindings=None, descr_type="", request=None, - entity_id=""): + def pick_binding( + self, service, bindings=None, descr_type="", request=None, entity_id="" + ): if request and not entity_id: entity_id = request.issuer.text.strip() @@ -284,7 +285,8 @@ class Entity(HTTPBase): if srv["index"] == _index: return binding, srv["location"] else: - return binding, destinations(srvs)[0] + destination = next(all_locations(srvs), None) + return binding, destination except UnsupportedBinding: pass @@ -349,9 +351,9 @@ class Entity(HTTPBase): else: descr_type = "spsso" - binding, destination = self.pick_binding(rsrv, bindings, - descr_type=descr_type, - request=message) + binding, destination = self.pick_binding( + rsrv, bindings, descr_type=descr_type, request=message + ) info["binding"] = binding info["destination"] = destination diff --git a/src/saml2/mdstore.py b/src/saml2/mdstore.py index 24fccb4d..3dfd0e5a 100644 --- a/src/saml2/mdstore.py +++ b/src/saml2/mdstore.py @@ -5,6 +5,8 @@ import json import logging import os import sys +from itertools import chain +from warnings import warn as _warn from hashlib import sha1 from os.path import isfile @@ -26,7 +28,11 @@ from saml2 import BINDING_SOAP from saml2.httpbase import HTTPBase from saml2.extension.idpdisc import BINDING_DISCO from saml2.extension.idpdisc import DiscoveryResponse +from saml2.md import NAMESPACE as NS_MD from saml2.md import EntitiesDescriptor +from saml2.md import ArtifactResolutionService +from saml2.md import NameIDMappingService +from saml2.md import SingleSignOnService from saml2.mdie import to_dict from saml2.s_utils import UnsupportedBinding from saml2.s_utils import UnknownSystemEntity @@ -70,6 +76,9 @@ classnames = { ns=NS_MDUI, tag=PrivacyStatementURL.c_tag ), "mdui_uiinfo_logo": "{ns}&{tag}".format(ns=NS_MDUI, tag=Logo.c_tag), + "service_artifact_resolution": "{ns}&{tag}".format(ns=NS_MD, tag=ArtifactResolutionService.c_tag), + "service_single_sign_on": "{ns}&{tag}".format(ns=NS_MD, tag=SingleSignOnService.c_tag), + "service_nameid_mapping": "{ns}&{tag}".format(ns=NS_MD, tag=NameIDMappingService.c_tag), } ENTITY_CATEGORY = "http://macedir.org/entity-category" @@ -79,8 +88,6 @@ ASSURANCE_CERTIFICATION = "urn:oasis:names:tc:SAML:attribute:assurance-certifica SAML_METADATA_CONTENT_TYPE = "application/samlmetadata+xml" DEFAULT_FRESHNESS_PERIOD = "P0Y0M0DT12H0M0S" - - REQ2SRV = { # IDP "authn_request": "single_sign_on_service", @@ -149,8 +156,54 @@ def metadata_modules(): return _res +def response_locations(srvs): + """ + Return the ResponseLocation attributes mapped to the services. + + ArtifactResolutionService, SingleSignOnService and NameIDMappingService MUST omit + the ResponseLocation attribute. This is enforced here, but metadata with such + service declarations and such attributes should not have been part of the metadata + store in the first place. + """ + values = ( + s["response_location"] + for s in srvs + if "response_location" in s + if s["__class__"] not in [ + classnames["service_artifact_resolution"], + classnames["service_single_sign_on"], + classnames["service_nameid_mapping"], + ] + ) + return values + + +def locations(srvs): + values = ( + s["location"] + for s in srvs + if "location" in s + ) + return values + + def destinations(srvs): - return [s["location"] for s in srvs] + warn_msg = ( + "`saml2.mdstore.destinations` function is deprecated; " + "instead, use `saml2.mdstore.locations` or `saml2.mdstore.all_locations`." + ) + logger.warning(warn_msg) + _warn(warn_msg) + values = list(locations(srvs)) + return values + + +def all_locations(srvs): + values = chain( + response_locations(srvs), + locations(srvs), + ) + return values def attribute_requirement(entity, index=None): diff --git a/tests/sp_slo_redirect.xml b/tests/sp_slo_redirect.xml index 7db2c837..b0c484ec 100644 --- a/tests/sp_slo_redirect.xml +++ b/tests/sp_slo_redirect.xml @@ -14,4 +14,4 @@ mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6 h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5 U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6 mrPzGzk3ECbupFnqyREH3+ZPSdk= -</ns1:X509Certificate></ns1:X509Data></ns1:KeyInfo></ns0:KeyDescriptor><ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://lingon.catalogix.se:8087/slo" /><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://lingon.catalogix.se:8087/" index="1" /><ns0:AttributeConsumingService index="1"><ns0:ServiceName xml:lang="en">urn:mace:example.com:saml:roland:sp</ns0:ServiceName><ns0:ServiceDescription xml:lang="en">My own SP</ns0:ServiceDescription><ns0:RequestedAttribute Name="surName" isRequired="true" /><ns0:RequestedAttribute FriendlyName="givenName" Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true" /><ns0:RequestedAttribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true" /><ns0:RequestedAttribute FriendlyName="title" Name="urn:oid:2.5.4.12" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="false" /></ns0:AttributeConsumingService></ns0:SPSSODescriptor><ns0:Organization><ns0:OrganizationName xml:lang="se">AB Exempel</ns0:OrganizationName><ns0:OrganizationDisplayName xml:lang="se">AB Exempel</ns0:OrganizationDisplayName><ns0:OrganizationURL xml:lang="en">http://www.example.org</ns0:OrganizationURL></ns0:Organization><ns0:ContactPerson contactType="technical"><ns0:GivenName>Roland</ns0:GivenName><ns0:SurName>Hedberg</ns0:SurName><ns0:EmailAddress>tech@eample.com</ns0:EmailAddress><ns0:EmailAddress>tech@example.org</ns0:EmailAddress><ns0:TelephoneNumber>+46 70 100 0000</ns0:TelephoneNumber></ns0:ContactPerson></ns0:EntityDescriptor></ns0:EntitiesDescriptor> +</ns1:X509Certificate></ns1:X509Data></ns1:KeyInfo></ns0:KeyDescriptor><ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://lingon.catalogix.se:8087/sloreq" ResponseLocation="http://lingon.catalogix.se:8087/sloresp" /><ns0:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://lingon.catalogix.se:8087/slo"/><ns0:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://lingon.catalogix.se:8087/" index="1" /><ns0:AttributeConsumingService index="1"><ns0:ServiceName xml:lang="en">urn:mace:example.com:saml:roland:sp</ns0:ServiceName><ns0:ServiceDescription xml:lang="en">My own SP</ns0:ServiceDescription><ns0:RequestedAttribute Name="surName" isRequired="true" /><ns0:RequestedAttribute FriendlyName="givenName" Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true" /><ns0:RequestedAttribute FriendlyName="mail" Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true" /><ns0:RequestedAttribute FriendlyName="title" Name="urn:oid:2.5.4.12" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="false" /></ns0:AttributeConsumingService></ns0:SPSSODescriptor><ns0:Organization><ns0:OrganizationName xml:lang="se">AB Exempel</ns0:OrganizationName><ns0:OrganizationDisplayName xml:lang="se">AB Exempel</ns0:OrganizationDisplayName><ns0:OrganizationURL xml:lang="en">http://www.example.org</ns0:OrganizationURL></ns0:Organization><ns0:ContactPerson contactType="technical"><ns0:GivenName>Roland</ns0:GivenName><ns0:SurName>Hedberg</ns0:SurName><ns0:EmailAddress>tech@eample.com</ns0:EmailAddress><ns0:EmailAddress>tech@example.org</ns0:EmailAddress><ns0:TelephoneNumber>+46 70 100 0000</ns0:TelephoneNumber></ns0:ContactPerson></ns0:EntityDescriptor></ns0:EntitiesDescriptor> diff --git a/tests/test_30_mdstore.py b/tests/test_30_mdstore.py index 59dc7da6..d712383f 100644 --- a/tests/test_30_mdstore.py +++ b/tests/test_30_mdstore.py @@ -15,7 +15,7 @@ from saml2.config import Config from saml2.mdstore import MetadataStore, MetaDataExtern from saml2.mdstore import MetaDataMDX from saml2.mdstore import SAML_METADATA_CONTENT_TYPE -from saml2.mdstore import destinations +from saml2.mdstore import locations from saml2.mdstore import name from saml2 import sigver from saml2.httpbase import HTTPBase @@ -177,8 +177,9 @@ def test_swami_1(): assert idps.keys() idpsso = mds.single_sign_on_service(UMU_IDP) assert len(idpsso) == 1 - assert destinations(idpsso) == [ - 'https://idp.umu.se/saml2/idp/SSOService.php'] + assert list(locations(idpsso)) == [ + 'https://idp.umu.se/saml2/idp/SSOService.php' + ] _name = name(mds[UMU_IDP]) assert _name == u'Umeå University (SAML2)' @@ -219,8 +220,9 @@ def test_incommon_1(): idpsso = mds.single_sign_on_service('urn:mace:incommon:alaska.edu') assert len(idpsso) == 1 print(idpsso) - assert destinations(idpsso) == [ - 'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO'] + assert list(locations(idpsso)) == [ + 'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO' + ] sps = mds.with_descriptor("spsso") @@ -279,8 +281,9 @@ def test_switch_1(): 'https://aai-demo-idp.switch.ch/idp/shibboleth') assert len(idpsso) == 1 print(idpsso) - assert destinations(idpsso) == [ - 'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO'] + assert list(locations(idpsso)) == [ + 'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO' + ] assert len(idps) > 30 aas = mds.with_descriptor("attribute_authority") print(aas.keys()) diff --git a/tests/test_30_mdstore_old.py b/tests/test_30_mdstore_old.py index 7ceb6653..d9f400ed 100644 --- a/tests/test_30_mdstore_old.py +++ b/tests/test_30_mdstore_old.py @@ -6,7 +6,7 @@ import os from unittest.mock import patch from saml2.mdstore import MetadataStore, MetaDataMDX -from saml2.mdstore import destinations +from saml2.mdstore import locations from saml2.mdstore import name from saml2 import md @@ -145,8 +145,9 @@ def test_swami_1(): assert idps.keys() idpsso = mds.single_sign_on_service(UMU_IDP) assert len(idpsso) == 1 - assert destinations(idpsso) == [ - 'https://idp.umu.se/saml2/idp/SSOService.php'] + assert list(locations(idpsso)) == [ + 'https://idp.umu.se/saml2/idp/SSOService.php' + ] _name = name(mds[UMU_IDP]) assert _name == u'Umeå University (SAML2)' @@ -187,8 +188,9 @@ def test_incommon_1(): idpsso = mds.single_sign_on_service('urn:mace:incommon:alaska.edu') assert len(idpsso) == 1 print(idpsso) - assert destinations(idpsso) == [ - 'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO'] + assert list(locations(idpsso)) == [ + 'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO' + ] sps = mds.with_descriptor("spsso") @@ -247,8 +249,9 @@ def test_switch_1(): 'https://aai-demo-idp.switch.ch/idp/shibboleth') assert len(idpsso) == 1 print(idpsso) - assert destinations(idpsso) == [ - 'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO'] + assert list(locations(idpsso)) == [ + 'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO' + ] assert len(idps) > 30 aas = mds.with_descriptor("attribute_authority") print(aas.keys()) diff --git a/tests/test_50_server.py b/tests/test_50_server.py index 589890cc..2aee6d8a 100644 --- a/tests/test_50_server.py +++ b/tests/test_50_server.py @@ -2303,16 +2303,37 @@ class TestServerLogout(): print(request) bindings = [BINDING_HTTP_REDIRECT] response = server.create_logout_response(request, bindings) - binding, destination = server.pick_binding("single_logout_service", - bindings, "spsso", - request) - http_args = server.apply_binding(binding, "%s" % response, destination, - "relay_state", response=True) + binding, destination = server.pick_binding( + "single_logout_service", bindings, "spsso", request + ) + http_args = server.apply_binding( + binding, "%s" % response, destination, "relay_state", response=True + ) assert len(http_args) == 4 assert http_args["headers"][0][0] == "Location" assert http_args["data"] == [] + assert http_args['url'] == 'http://lingon.catalogix.se:8087/sloresp' + + def test_2(self): + with closing(Server("idp_slo_redirect_conf")) as server: + req_id, request = _logout_request("sp_slo_redirect_conf") + print(request) + bindings = [BINDING_HTTP_POST] + response = server.create_logout_response(request, bindings) + + binding, destination = server.pick_binding( + "single_logout_service", bindings, "spsso", request + ) + http_args = server.apply_binding( + binding, "%s" % response, destination, "relay_state", response=True + ) + + assert len(http_args) == 4 + assert len(http_args["data"]) > 0 + assert http_args["method"] == "POST" + assert http_args['url'] == 'http://lingon.catalogix.se:8087/slo' if __name__ == "__main__": diff --git a/tests/test_76_metadata_in_mdb.py b/tests/test_76_metadata_in_mdb.py index f1376b17..60c1ff1c 100644 --- a/tests/test_76_metadata_in_mdb.py +++ b/tests/test_76_metadata_in_mdb.py @@ -5,7 +5,7 @@ from saml2.attribute_converter import ac_factory from saml2.mongo_store import export_mdstore_to_mongo_db from saml2.mongo_store import MetadataMDB from saml2.mdstore import MetadataStore -from saml2.mdstore import destinations +from saml2.mdstore import locations from saml2.mdstore import name from saml2 import config from pathutils import full_path @@ -46,7 +46,7 @@ def test_metadata(): assert idps.keys() idpsso = mds.single_sign_on_service(umu_idp) assert len(idpsso) == 1 - assert destinations(idpsso) == [ + assert list(locations(idpsso)) == [ 'https://idp.umu.se/saml2/idp/SSOService.php'] _name = name(mds[umu_idp]) |