Skip to content

Commit 06920df

Browse files
Merge pull request #728 from IdentityPython/feature-logout-response-location
2 parents ca60cd9 + 59913a1 commit 06920df

File tree

9 files changed

+124
-43
lines changed

9 files changed

+124
-43
lines changed

src/saml2/client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from saml2.client_base import SignOnError
3030
from saml2.client_base import LogoutError
3131
from saml2.client_base import NoServiceDefined
32-
from saml2.mdstore import destinations
32+
from saml2.mdstore import locations
3333

3434
import logging
3535

@@ -209,7 +209,7 @@ def do_logout(self, name_id, entity_ids, reason, expire, sign=None,
209209
logger.debug("No SLO '%s' service", binding)
210210
continue
211211

212-
destination = destinations(srvs)[0]
212+
destination = next(locations(srvs), None)
213213
logger.info("destination to provider: %s", destination)
214214
try:
215215
session_info = self.users.get_info_from(name_id,
@@ -374,7 +374,7 @@ def do_authz_decision_query(self, entity_id, action,
374374
name_qualifier=name_qualifier))
375375

376376
srvs = self.metadata.authz_service(entity_id, BINDING_SOAP)
377-
for dest in destinations(srvs):
377+
for dest in locations(srvs):
378378
resp = self._use_soap(dest, "authz_decision_query",
379379
action=action, evidence=evidence,
380380
resource=resource, subject=subject)
@@ -397,7 +397,7 @@ def do_assertion_id_request(self, assertion_ids, entity_id,
397397

398398
_id_refs = [AssertionIDRef(_id) for _id in assertion_ids]
399399

400-
for destination in destinations(srvs):
400+
for destination in locations(srvs):
401401
res = self._use_soap(destination, "assertion_id_request",
402402
assertion_id_refs=_id_refs, consent=consent,
403403
extensions=extensions, sign=sign)
@@ -411,7 +411,7 @@ def do_authn_query(self, entity_id,
411411

412412
srvs = self.metadata.authn_request_service(entity_id, BINDING_SOAP)
413413

414-
for destination in destinations(srvs):
414+
for destination in locations(srvs):
415415
resp = self._use_soap(destination, "authn_query", consent=consent,
416416
extensions=extensions, sign=sign)
417417
if resp:
@@ -461,7 +461,7 @@ def do_attribute_query(self, entityid, subject_id,
461461
if srvs is []:
462462
raise SAMLError("No attribute service support at entity")
463463

464-
destination = destinations(srvs)[0]
464+
destination = next(locations(srvs), None)
465465

466466
if binding == BINDING_SOAP:
467467
return self._use_soap(destination, "attribute_query",

src/saml2/client_base.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from saml2.entity import Entity
1414

15-
from saml2.mdstore import destinations
15+
from saml2.mdstore import locations
1616
from saml2.profile import paos, ecp
1717
from saml2.saml import NAMEID_FORMAT_PERSISTENT
1818
from saml2.saml import NAMEID_FORMAT_TRANSIENT
@@ -212,7 +212,7 @@ def _sso_location(self, entityid=None, binding=BINDING_HTTP_REDIRECT):
212212
# verify that it's in the metadata
213213
srvs = self.metadata.single_sign_on_service(entityid, binding)
214214
if srvs:
215-
return destinations(srvs)[0]
215+
return next(locations(srvs), None)
216216
else:
217217
logger.info("_sso_location: %s, %s", entityid, binding)
218218
raise IdpUnspecified("No IdP to send to given the premises")
@@ -224,9 +224,8 @@ def _sso_location(self, entityid=None, binding=BINDING_HTTP_REDIRECT):
224224
raise IdpUnspecified("Too many IdPs to choose from: %s" % eids)
225225

226226
try:
227-
srvs = self.metadata.single_sign_on_service(list(eids.keys())[0],
228-
binding)
229-
return destinations(srvs)[0]
227+
srvs = self.metadata.single_sign_on_service(list(eids.keys())[0], binding)
228+
return next(locations(srvs), None)
230229
except IndexError:
231230
raise IdpUnspecified("No IdP to send to given the premises")
232231

src/saml2/entity.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
from saml2.samlp import Artifact
5454
from saml2.samlp import LogoutRequest
5555
from saml2.samlp import AttributeQuery
56-
from saml2.mdstore import destinations
56+
from saml2.mdstore import all_locations
5757
from saml2 import BINDING_HTTP_POST
5858
from saml2 import BINDING_HTTP_REDIRECT
5959
from saml2 import BINDING_SOAP
@@ -249,8 +249,9 @@ def apply_binding(self, binding, msg_str, destination="", relay_state="",
249249

250250
return info
251251

252-
def pick_binding(self, service, bindings=None, descr_type="", request=None,
253-
entity_id=""):
252+
def pick_binding(
253+
self, service, bindings=None, descr_type="", request=None, entity_id=""
254+
):
254255
if request and not entity_id:
255256
entity_id = request.issuer.text.strip()
256257

@@ -284,7 +285,8 @@ def pick_binding(self, service, bindings=None, descr_type="", request=None,
284285
if srv["index"] == _index:
285286
return binding, srv["location"]
286287
else:
287-
return binding, destinations(srvs)[0]
288+
destination = next(all_locations(srvs), None)
289+
return binding, destination
288290
except UnsupportedBinding:
289291
pass
290292

@@ -349,9 +351,9 @@ def response_args(self, message, bindings=None, descr_type=""):
349351
else:
350352
descr_type = "spsso"
351353

352-
binding, destination = self.pick_binding(rsrv, bindings,
353-
descr_type=descr_type,
354-
request=message)
354+
binding, destination = self.pick_binding(
355+
rsrv, bindings, descr_type=descr_type, request=message
356+
)
355357
info["binding"] = binding
356358
info["destination"] = destination
357359

src/saml2/mdstore.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import logging
66
import os
77
import sys
8+
from itertools import chain
9+
from warnings import warn as _warn
810

911
from hashlib import sha1
1012
from os.path import isfile
@@ -26,7 +28,11 @@
2628
from saml2.httpbase import HTTPBase
2729
from saml2.extension.idpdisc import BINDING_DISCO
2830
from saml2.extension.idpdisc import DiscoveryResponse
31+
from saml2.md import NAMESPACE as NS_MD
2932
from saml2.md import EntitiesDescriptor
33+
from saml2.md import ArtifactResolutionService
34+
from saml2.md import NameIDMappingService
35+
from saml2.md import SingleSignOnService
3036
from saml2.mdie import to_dict
3137
from saml2.s_utils import UnsupportedBinding
3238
from saml2.s_utils import UnknownSystemEntity
@@ -70,6 +76,9 @@
7076
ns=NS_MDUI, tag=PrivacyStatementURL.c_tag
7177
),
7278
"mdui_uiinfo_logo": "{ns}&{tag}".format(ns=NS_MDUI, tag=Logo.c_tag),
79+
"service_artifact_resolution": "{ns}&{tag}".format(ns=NS_MD, tag=ArtifactResolutionService.c_tag),
80+
"service_single_sign_on": "{ns}&{tag}".format(ns=NS_MD, tag=SingleSignOnService.c_tag),
81+
"service_nameid_mapping": "{ns}&{tag}".format(ns=NS_MD, tag=NameIDMappingService.c_tag),
7382
}
7483

7584
ENTITY_CATEGORY = "http://macedir.org/entity-category"
@@ -79,8 +88,6 @@
7988
SAML_METADATA_CONTENT_TYPE = "application/samlmetadata+xml"
8089
DEFAULT_FRESHNESS_PERIOD = "P0Y0M0DT12H0M0S"
8190

82-
83-
8491
REQ2SRV = {
8592
# IDP
8693
"authn_request": "single_sign_on_service",
@@ -149,8 +156,54 @@ def metadata_modules():
149156
return _res
150157

151158

159+
def response_locations(srvs):
160+
"""
161+
Return the ResponseLocation attributes mapped to the services.
162+
163+
ArtifactResolutionService, SingleSignOnService and NameIDMappingService MUST omit
164+
the ResponseLocation attribute. This is enforced here, but metadata with such
165+
service declarations and such attributes should not have been part of the metadata
166+
store in the first place.
167+
"""
168+
values = (
169+
s["response_location"]
170+
for s in srvs
171+
if "response_location" in s
172+
if s["__class__"] not in [
173+
classnames["service_artifact_resolution"],
174+
classnames["service_single_sign_on"],
175+
classnames["service_nameid_mapping"],
176+
]
177+
)
178+
return values
179+
180+
181+
def locations(srvs):
182+
values = (
183+
s["location"]
184+
for s in srvs
185+
if "location" in s
186+
)
187+
return values
188+
189+
152190
def destinations(srvs):
153-
return [s["location"] for s in srvs]
191+
warn_msg = (
192+
"`saml2.mdstore.destinations` function is deprecated; "
193+
"instead, use `saml2.mdstore.locations` or `saml2.mdstore.all_locations`."
194+
)
195+
logger.warning(warn_msg)
196+
_warn(warn_msg)
197+
values = list(locations(srvs))
198+
return values
199+
200+
201+
def all_locations(srvs):
202+
values = chain(
203+
response_locations(srvs),
204+
locations(srvs),
205+
)
206+
return values
154207

155208

156209
def attribute_requirement(entity, index=None):

tests/sp_slo_redirect.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ mDY9MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAJSrKOEzHO7TL5cy6
1414
h3qh+3+JAk8HbGBW+cbX6KBCAw/mzU8flK25vnWwXS3dv2FF3Aod0/S7AWNfKib5
1515
U/SA9nJaz/mWeF9S0farz9AQFc8/NSzAzaVq7YbM4F6f6N2FRl7GikdXRCed45j6
1616
mrPzGzk3ECbupFnqyREH3+ZPSdk=
17-
</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>[email protected]</ns0:EmailAddress><ns0:EmailAddress>[email protected]</ns0:EmailAddress><ns0:TelephoneNumber>+46 70 100 0000</ns0:TelephoneNumber></ns0:ContactPerson></ns0:EntityDescriptor></ns0:EntitiesDescriptor>
17+
</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>[email protected]</ns0:EmailAddress><ns0:EmailAddress>[email protected]</ns0:EmailAddress><ns0:TelephoneNumber>+46 70 100 0000</ns0:TelephoneNumber></ns0:ContactPerson></ns0:EntityDescriptor></ns0:EntitiesDescriptor>

tests/test_30_mdstore.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from saml2.mdstore import MetadataStore, MetaDataExtern
1616
from saml2.mdstore import MetaDataMDX
1717
from saml2.mdstore import SAML_METADATA_CONTENT_TYPE
18-
from saml2.mdstore import destinations
18+
from saml2.mdstore import locations
1919
from saml2.mdstore import name
2020
from saml2 import sigver
2121
from saml2.httpbase import HTTPBase
@@ -177,8 +177,9 @@ def test_swami_1():
177177
assert idps.keys()
178178
idpsso = mds.single_sign_on_service(UMU_IDP)
179179
assert len(idpsso) == 1
180-
assert destinations(idpsso) == [
181-
'https://idp.umu.se/saml2/idp/SSOService.php']
180+
assert list(locations(idpsso)) == [
181+
'https://idp.umu.se/saml2/idp/SSOService.php'
182+
]
182183

183184
_name = name(mds[UMU_IDP])
184185
assert _name == u'Umeå University (SAML2)'
@@ -219,8 +220,9 @@ def test_incommon_1():
219220
idpsso = mds.single_sign_on_service('urn:mace:incommon:alaska.edu')
220221
assert len(idpsso) == 1
221222
print(idpsso)
222-
assert destinations(idpsso) == [
223-
'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO']
223+
assert list(locations(idpsso)) == [
224+
'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO'
225+
]
224226

225227
sps = mds.with_descriptor("spsso")
226228

@@ -279,8 +281,9 @@ def test_switch_1():
279281
'https://aai-demo-idp.switch.ch/idp/shibboleth')
280282
assert len(idpsso) == 1
281283
print(idpsso)
282-
assert destinations(idpsso) == [
283-
'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO']
284+
assert list(locations(idpsso)) == [
285+
'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO'
286+
]
284287
assert len(idps) > 30
285288
aas = mds.with_descriptor("attribute_authority")
286289
print(aas.keys())

tests/test_30_mdstore_old.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from unittest.mock import patch
77

88
from saml2.mdstore import MetadataStore, MetaDataMDX
9-
from saml2.mdstore import destinations
9+
from saml2.mdstore import locations
1010
from saml2.mdstore import name
1111

1212
from saml2 import md
@@ -145,8 +145,9 @@ def test_swami_1():
145145
assert idps.keys()
146146
idpsso = mds.single_sign_on_service(UMU_IDP)
147147
assert len(idpsso) == 1
148-
assert destinations(idpsso) == [
149-
'https://idp.umu.se/saml2/idp/SSOService.php']
148+
assert list(locations(idpsso)) == [
149+
'https://idp.umu.se/saml2/idp/SSOService.php'
150+
]
150151

151152
_name = name(mds[UMU_IDP])
152153
assert _name == u'Umeå University (SAML2)'
@@ -187,8 +188,9 @@ def test_incommon_1():
187188
idpsso = mds.single_sign_on_service('urn:mace:incommon:alaska.edu')
188189
assert len(idpsso) == 1
189190
print(idpsso)
190-
assert destinations(idpsso) == [
191-
'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO']
191+
assert list(locations(idpsso)) == [
192+
'https://idp.alaska.edu/idp/profile/SAML2/Redirect/SSO'
193+
]
192194

193195
sps = mds.with_descriptor("spsso")
194196

@@ -247,8 +249,9 @@ def test_switch_1():
247249
'https://aai-demo-idp.switch.ch/idp/shibboleth')
248250
assert len(idpsso) == 1
249251
print(idpsso)
250-
assert destinations(idpsso) == [
251-
'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO']
252+
assert list(locations(idpsso)) == [
253+
'https://aai-demo-idp.switch.ch/idp/profile/SAML2/Redirect/SSO'
254+
]
252255
assert len(idps) > 30
253256
aas = mds.with_descriptor("attribute_authority")
254257
print(aas.keys())

tests/test_50_server.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2303,16 +2303,37 @@ def test_1(self):
23032303
print(request)
23042304
bindings = [BINDING_HTTP_REDIRECT]
23052305
response = server.create_logout_response(request, bindings)
2306-
binding, destination = server.pick_binding("single_logout_service",
2307-
bindings, "spsso",
2308-
request)
23092306

2310-
http_args = server.apply_binding(binding, "%s" % response, destination,
2311-
"relay_state", response=True)
2307+
binding, destination = server.pick_binding(
2308+
"single_logout_service", bindings, "spsso", request
2309+
)
2310+
http_args = server.apply_binding(
2311+
binding, "%s" % response, destination, "relay_state", response=True
2312+
)
23122313

23132314
assert len(http_args) == 4
23142315
assert http_args["headers"][0][0] == "Location"
23152316
assert http_args["data"] == []
2317+
assert http_args['url'] == 'http://lingon.catalogix.se:8087/sloresp'
2318+
2319+
def test_2(self):
2320+
with closing(Server("idp_slo_redirect_conf")) as server:
2321+
req_id, request = _logout_request("sp_slo_redirect_conf")
2322+
print(request)
2323+
bindings = [BINDING_HTTP_POST]
2324+
response = server.create_logout_response(request, bindings)
2325+
2326+
binding, destination = server.pick_binding(
2327+
"single_logout_service", bindings, "spsso", request
2328+
)
2329+
http_args = server.apply_binding(
2330+
binding, "%s" % response, destination, "relay_state", response=True
2331+
)
2332+
2333+
assert len(http_args) == 4
2334+
assert len(http_args["data"]) > 0
2335+
assert http_args["method"] == "POST"
2336+
assert http_args['url'] == 'http://lingon.catalogix.se:8087/slo'
23162337

23172338

23182339
if __name__ == "__main__":

tests/test_76_metadata_in_mdb.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from saml2.mongo_store import export_mdstore_to_mongo_db
66
from saml2.mongo_store import MetadataMDB
77
from saml2.mdstore import MetadataStore
8-
from saml2.mdstore import destinations
8+
from saml2.mdstore import locations
99
from saml2.mdstore import name
1010
from saml2 import config
1111
from pathutils import full_path
@@ -46,7 +46,7 @@ def test_metadata():
4646
assert idps.keys()
4747
idpsso = mds.single_sign_on_service(umu_idp)
4848
assert len(idpsso) == 1
49-
assert destinations(idpsso) == [
49+
assert list(locations(idpsso)) == [
5050
'https://idp.umu.se/saml2/idp/SSOService.php']
5151

5252
_name = name(mds[umu_idp])

0 commit comments

Comments
 (0)