Skip to content

Commit 59913a1

Browse files
committed
Return the ResponseLocation before falling back to Location
ResponseLocation [Optional] Optionally specifies a different location to which response messages sent as part of the protocol or profile should be sent. The allowable syntax of this URI depends on the protocol binding. The ResponseLocation attribute is used to enable different endpoints to be specified for receiving request and response messages associated with a protocol or profile, not as a means of load-balancing or redundancy (multiple elements of this type can be included for this purpose). When a role contains an element of this type pertaining to a protocol or profile for which only a single type of message (request or response) is applicable, then the ResponseLocation attribute is unused. [E41]If the ResponseLocation attribute is omitted, any response messages associated with a protocol or profile may be assumed to be handled at the URI indicated by the Location attribute. 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. Signed-off-by: Ivan Kanakarakis <[email protected]>
1 parent 524b70d commit 59913a1

File tree

8 files changed

+109
-60
lines changed

8 files changed

+109
-60
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 & 11 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, response_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="", response=False):
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,10 +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-
if response:
288-
return binding, response_destinations(srvs)[0]
289-
else:
290-
return binding, destinations(srvs)[0]
288+
destination = next(all_locations(srvs), None)
289+
return binding, destination
291290
except UnsupportedBinding:
292291
pass
293292

@@ -352,10 +351,9 @@ def response_args(self, message, bindings=None, descr_type=""):
352351
else:
353352
descr_type = "spsso"
354353

355-
binding, destination = self.pick_binding(rsrv, bindings,
356-
descr_type=descr_type,
357-
request=message,
358-
response=True)
354+
binding, destination = self.pick_binding(
355+
rsrv, bindings, descr_type=descr_type, request=message
356+
)
359357
info["binding"] = binding
360358
info["destination"] = destination
361359

src/saml2/mdstore.py

Lines changed: 55 additions & 11 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,17 +156,54 @@ def metadata_modules():
149156
return _res
150157

151158

152-
def response_destinations(srvs):
153-
_res = []
154-
for s in srvs:
155-
if "response_location" in s:
156-
_res.append(s["response_location"])
157-
else:
158-
_res.append(s["location"])
159-
return _res
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+
160189

161190
def destinations(srvs):
162-
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
163207

164208

165209
def attribute_requirement(entity, index=None):

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: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,19 +2297,19 @@ def _logout_request(conf_file):
22972297

22982298

22992299
class TestServerLogout():
2300-
23012300
def test_1(self):
23022301
with closing(Server("idp_slo_redirect_conf")) as server:
23032302
req_id, request = _logout_request("sp_slo_redirect_conf")
23042303
print(request)
23052304
bindings = [BINDING_HTTP_REDIRECT]
23062305
response = server.create_logout_response(request, bindings)
2307-
binding, destination = server.pick_binding("single_logout_service",
2308-
bindings, "spsso",
2309-
request, response=True)
23102306

2311-
http_args = server.apply_binding(binding, "%s" % response, destination,
2312-
"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+
)
23132313

23142314
assert len(http_args) == 4
23152315
assert http_args["headers"][0][0] == "Location"
@@ -2322,18 +2322,20 @@ def test_2(self):
23222322
print(request)
23232323
bindings = [BINDING_HTTP_POST]
23242324
response = server.create_logout_response(request, bindings)
2325-
binding, destination = server.pick_binding("single_logout_service",
2326-
bindings, "spsso",
2327-
request, response=True)
23282325

2329-
http_args = server.apply_binding(binding, "%s" % response, destination,
2330-
"relay_state", response=True)
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+
)
23312332

23322333
assert len(http_args) == 4
23332334
assert len(http_args["data"]) > 0
23342335
assert http_args["method"] == "POST"
23352336
assert http_args['url'] == 'http://lingon.catalogix.se:8087/slo'
23362337

2338+
23372339
if __name__ == "__main__":
23382340
ts = TestServer1()
23392341
ts.setup_class()

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)