Skip to content

Commit efc7095

Browse files
committed
Fix check for ForceAuthn value from context
The ForceAuthn value as delivered by the AuthnRequest to the frontend is an xsd:boolean value and can have the values: - "true" - "false" - "1" (true) - "0" (false) When checking the value, we must take into account all values. Signed-off-by: Ivan Kanakarakis <[email protected]>
1 parent f53bbd4 commit efc7095

File tree

2 files changed

+164
-117
lines changed

2 files changed

+164
-117
lines changed

src/satosa/backends/saml2.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ def get_force_authn(context, config, sp_config):
6262
"""
6363
mirror = config.get(SAMLBackend.KEY_MIRROR_FORCE_AUTHN)
6464
from_state = mirror and context.state.get(Context.KEY_FORCE_AUTHN)
65-
from_context = mirror and context.get_decoration(Context.KEY_FORCE_AUTHN)
65+
from_context = (
66+
mirror and context.get_decoration(Context.KEY_FORCE_AUTHN) in ["true", "1"]
67+
)
6668
from_config = sp_config.getattr("force_authn", "sp")
6769
is_set = str(from_state or from_context or from_config).lower() == "true"
6870
value = is_set and "true"

tests/satosa/backends/test_saml2.py

Lines changed: 161 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -39,47 +39,51 @@
3939
DISCOSRV_URL = "https://my.dicso.com/role/idp.ds"
4040

4141

42-
class TestSAMLBackend:
43-
def assert_redirect_to_idp(self, redirect_response, idp_conf):
44-
assert redirect_response.status == "303 See Other"
45-
parsed = urlparse(redirect_response.message)
46-
redirect_location = "{parsed.scheme}://{parsed.netloc}{parsed.path}".format(parsed=parsed)
47-
assert redirect_location == idp_conf["service"]["idp"]["endpoints"]["single_sign_on_service"][0][0]
48-
assert "SAMLRequest" in parse_qs(parsed.query)
49-
50-
def assert_redirect_to_discovery_server(
51-
self, redirect_response, sp_conf, expected_discosrv_url
52-
):
53-
assert redirect_response.status == "303 See Other"
54-
parsed = urlparse(redirect_response.message)
55-
redirect_location = "{parsed.scheme}://{parsed.netloc}{parsed.path}".format(parsed=parsed)
56-
assert redirect_location == expected_discosrv_url
57-
58-
request_params = dict(parse_qsl(parsed.query))
59-
assert request_params["return"] == sp_conf["service"]["sp"]["endpoints"]["discovery_response"][0][0]
60-
assert request_params["entityID"] == sp_conf["entityid"]
61-
62-
def assert_authn_response(self, internal_resp):
63-
assert internal_resp.auth_info.auth_class_ref == PASSWORD
64-
expected_data = {'surname': ['Testsson 1'], 'mail': ['[email protected]'],
65-
'displayname': ['Test Testsson'], 'givenname': ['Test 1'],
66-
'edupersontargetedid': ['one!for!all']}
67-
assert expected_data == internal_resp.attributes
68-
69-
def setup_test_config(self, sp_conf, idp_conf):
70-
idp_metadata_str = create_metadata_from_config_dict(idp_conf)
71-
sp_conf["metadata"]["inline"].append(idp_metadata_str)
72-
idp2_config = idp_conf.copy()
73-
idp2_config["entityid"] = "just_an_extra_idp"
74-
idp_metadata_str2 = create_metadata_from_config_dict(idp2_config)
75-
sp_conf["metadata"]["inline"].append(idp_metadata_str2)
76-
77-
sp_metadata_str = create_metadata_from_config_dict(sp_conf)
78-
idp_conf["metadata"]["inline"] = [sp_metadata_str]
42+
def assert_redirect_to_discovery_server(
43+
redirect_response, sp_conf, expected_discosrv_url
44+
):
45+
assert redirect_response.status == "303 See Other"
46+
parsed = urlparse(redirect_response.message)
47+
redirect_location = "{parsed.scheme}://{parsed.netloc}{parsed.path}".format(parsed=parsed)
48+
assert redirect_location == expected_discosrv_url
49+
50+
request_params = dict(parse_qsl(parsed.query))
51+
assert request_params["return"] == sp_conf["service"]["sp"]["endpoints"]["discovery_response"][0][0]
52+
assert request_params["entityID"] == sp_conf["entityid"]
53+
54+
55+
def assert_redirect_to_idp(redirect_response, idp_conf):
56+
assert redirect_response.status == "303 See Other"
57+
parsed = urlparse(redirect_response.message)
58+
redirect_location = "{parsed.scheme}://{parsed.netloc}{parsed.path}".format(parsed=parsed)
59+
assert redirect_location == idp_conf["service"]["idp"]["endpoints"]["single_sign_on_service"][0][0]
60+
assert "SAMLRequest" in parse_qs(parsed.query)
61+
62+
63+
def assert_authn_response(internal_resp):
64+
assert internal_resp.auth_info.auth_class_ref == PASSWORD
65+
expected_data = {'surname': ['Testsson 1'], 'mail': ['[email protected]'],
66+
'displayname': ['Test Testsson'], 'givenname': ['Test 1'],
67+
'edupersontargetedid': ['one!for!all']}
68+
assert expected_data == internal_resp.attributes
69+
70+
71+
def setup_test_config(sp_conf, idp_conf):
72+
idp_metadata_str = create_metadata_from_config_dict(idp_conf)
73+
sp_conf["metadata"]["inline"].append(idp_metadata_str)
74+
idp2_config = idp_conf.copy()
75+
idp2_config["entityid"] = "just_an_extra_idp"
76+
idp_metadata_str2 = create_metadata_from_config_dict(idp2_config)
77+
sp_conf["metadata"]["inline"].append(idp_metadata_str2)
78+
79+
sp_metadata_str = create_metadata_from_config_dict(sp_conf)
80+
idp_conf["metadata"]["inline"] = [sp_metadata_str]
81+
7982

83+
class TestSAMLBackend:
8084
@pytest.fixture(autouse=True)
8185
def create_backend(self, sp_conf, idp_conf):
82-
self.setup_test_config(sp_conf, idp_conf)
86+
setup_test_config(sp_conf, idp_conf)
8387
self.samlbackend = SAMLBackend(Mock(), INTERNAL_ATTRIBUTES, {"sp_config": sp_conf,
8488
"disco_srv": DISCOSRV_URL},
8589
"base_url",
@@ -101,15 +105,15 @@ def get_path_from_url(url):
101105

102106
def test_start_auth_defaults_to_redirecting_to_discovery_server(self, context, sp_conf):
103107
resp = self.samlbackend.start_auth(context, InternalData())
104-
self.assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
108+
assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
105109

106110
def test_discovery_server_set_in_context(self, context, sp_conf):
107111
discosrv_url = 'https://my.org/saml_discovery_service'
108112
context.decorate(
109113
SAMLBackend.KEY_SAML_DISCOVERY_SERVICE_URL, discosrv_url
110114
)
111115
resp = self.samlbackend.start_auth(context, InternalData())
112-
self.assert_redirect_to_discovery_server(resp, sp_conf, discosrv_url)
116+
assert_redirect_to_discovery_server(resp, sp_conf, discosrv_url)
113117

114118
def test_full_flow(self, context, idp_conf, sp_conf):
115119
test_state_key = "test_state_key_456afgrh"
@@ -120,7 +124,7 @@ def test_full_flow(self, context, idp_conf, sp_conf):
120124

121125
# start auth flow (redirecting to discovery server)
122126
resp = self.samlbackend.start_auth(context, InternalData())
123-
self.assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
127+
assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
124128

125129
# fake response from discovery server
126130
disco_resp = parse_qs(urlparse(resp.message).query)
@@ -132,7 +136,7 @@ def test_full_flow(self, context, idp_conf, sp_conf):
132136

133137
# pass discovery response to backend and check that it redirects to the selected IdP
134138
resp = self.samlbackend.disco_response(request_context)
135-
self.assert_redirect_to_idp(resp, idp_conf)
139+
assert_redirect_to_idp(resp, idp_conf)
136140

137141
# fake auth response to the auth request
138142
req_params = dict(parse_qsl(urlparse(resp.message).query))
@@ -151,95 +155,27 @@ def test_full_flow(self, context, idp_conf, sp_conf):
151155
context, internal_resp = self.samlbackend.auth_callback_func.call_args[0]
152156
assert self.samlbackend.name not in context.state
153157
assert context.state[test_state_key] == "my_state"
154-
self.assert_authn_response(internal_resp)
158+
assert_authn_response(internal_resp)
155159

156160
def test_start_auth_redirects_directly_to_mirrored_idp(
157161
self, context, idp_conf):
158162
entityid = idp_conf["entityid"]
159163
context.decorate(Context.KEY_TARGET_ENTITYID, entityid)
160164

161165
resp = self.samlbackend.start_auth(context, InternalData())
162-
self.assert_redirect_to_idp(resp, idp_conf)
166+
assert_redirect_to_idp(resp, idp_conf)
163167

164168
def test_redirect_to_idp_if_only_one_idp_in_metadata(self, context, sp_conf, idp_conf):
165169
sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
166170
# instantiate new backend, without any discovery service configured
167171
samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf}, "base_url", "saml_backend")
168172

169173
resp = samlbackend.start_auth(context, InternalData())
170-
self.assert_redirect_to_idp(resp, idp_conf)
171-
172-
def test_default_redirect_to_discovery_service_if_using_mdq(self, context, sp_conf, idp_conf):
173-
# one IdP in the metadata, but MDQ also configured so should always redirect to the discovery service
174-
sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
175-
sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
176-
samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL,},
177-
"base_url", "saml_backend")
178-
resp = samlbackend.start_auth(context, InternalData())
179-
self.assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
180-
181-
def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_not_set(
182-
self, context, sp_conf, idp_conf
183-
):
184-
sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
185-
sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
186-
187-
backend_conf = {
188-
SAMLBackend.KEY_SP_CONFIG: sp_conf,
189-
SAMLBackend.KEY_DISCO_SRV: DISCOSRV_URL,
190-
SAMLBackend.KEY_MEMORIZE_IDP: True,
191-
}
192-
samlbackend = SAMLBackend(
193-
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
194-
)
195-
resp = samlbackend.start_auth(context, InternalData())
196-
self.assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
197-
198-
context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"]
199-
samlbackend = SAMLBackend(
200-
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
201-
)
202-
resp = samlbackend.start_auth(context, InternalData())
203-
self.assert_redirect_to_idp(resp, idp_conf)
204-
205-
backend_conf[SAMLBackend.KEY_MEMORIZE_IDP] = False
206-
samlbackend = SAMLBackend(
207-
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
208-
)
209-
resp = samlbackend.start_auth(context, InternalData())
210-
self.assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
211-
212-
def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set(
213-
self, context, sp_conf, idp_conf
214-
):
215-
sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
216-
sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
217-
218-
context.decorate(Context.KEY_FORCE_AUTHN, "true")
219-
context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"]
220-
221-
backend_conf = {
222-
SAMLBackend.KEY_SP_CONFIG: sp_conf,
223-
SAMLBackend.KEY_DISCO_SRV: DISCOSRV_URL,
224-
SAMLBackend.KEY_MEMORIZE_IDP: True,
225-
SAMLBackend.KEY_MIRROR_FORCE_AUTHN: True,
226-
}
227-
samlbackend = SAMLBackend(
228-
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
229-
)
230-
resp = samlbackend.start_auth(context, InternalData())
231-
self.assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
232-
233-
backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True
234-
samlbackend = SAMLBackend(
235-
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
236-
)
237-
resp = samlbackend.start_auth(context, InternalData())
238-
self.assert_redirect_to_idp(resp, idp_conf)
174+
assert_redirect_to_idp(resp, idp_conf)
239175

240176
def test_authn_request(self, context, idp_conf):
241177
resp = self.samlbackend.authn_request(context, idp_conf["entityid"])
242-
self.assert_redirect_to_idp(resp, idp_conf)
178+
assert_redirect_to_idp(resp, idp_conf)
243179
req_params = dict(parse_qsl(urlparse(resp.message).query))
244180
assert context.state[self.samlbackend.name]["relay_state"] == req_params["RelayState"]
245181

@@ -257,7 +193,7 @@ def test_authn_response(self, context, idp_conf, sp_conf):
257193
self.samlbackend.authn_response(context, response_binding)
258194

259195
context, internal_resp = self.samlbackend.auth_callback_func.call_args[0]
260-
self.assert_authn_response(internal_resp)
196+
assert_authn_response(internal_resp)
261197
assert self.samlbackend.name not in context.state
262198

263199
@pytest.mark.skipif(
@@ -293,7 +229,7 @@ def test_authn_response_no_name_id(self, context, idp_conf, sp_conf):
293229
backend.authn_response(context, response_binding)
294230

295231
context, internal_resp = backend.auth_callback_func.call_args[0]
296-
self.assert_authn_response(internal_resp)
232+
assert_authn_response(internal_resp)
297233
assert backend.name not in context.state
298234

299235
def test_authn_response_with_encrypted_assertion(self, sp_conf, context):
@@ -396,3 +332,112 @@ def test_get_metadata_desc_with_logo_without_lang(self, sp_conf, idp_conf):
396332
assert ui_info["display_name"] == expected_ui_info["display_name"]
397333
assert ui_info["description"] == expected_ui_info["description"]
398334
assert ui_info["logo"] == expected_ui_info["logo"]
335+
336+
337+
class TestSAMLBackendRedirects:
338+
def test_default_redirect_to_discovery_service_if_using_mdq(
339+
self, context, sp_conf, idp_conf
340+
):
341+
# one IdP in the metadata, but MDQ also configured so should always redirect to the discovery service
342+
sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
343+
sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
344+
samlbackend = SAMLBackend(None, INTERNAL_ATTRIBUTES, {"sp_config": sp_conf, "disco_srv": DISCOSRV_URL,},
345+
"base_url", "saml_backend")
346+
resp = samlbackend.start_auth(context, InternalData())
347+
assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
348+
349+
def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_not_set(
350+
self, context, sp_conf, idp_conf
351+
):
352+
sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
353+
sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
354+
355+
backend_conf = {
356+
SAMLBackend.KEY_SP_CONFIG: sp_conf,
357+
SAMLBackend.KEY_DISCO_SRV: DISCOSRV_URL,
358+
SAMLBackend.KEY_MEMORIZE_IDP: True,
359+
}
360+
samlbackend = SAMLBackend(
361+
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
362+
)
363+
resp = samlbackend.start_auth(context, InternalData())
364+
assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
365+
366+
context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"]
367+
samlbackend = SAMLBackend(
368+
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
369+
)
370+
resp = samlbackend.start_auth(context, InternalData())
371+
assert_redirect_to_idp(resp, idp_conf)
372+
373+
backend_conf[SAMLBackend.KEY_MEMORIZE_IDP] = False
374+
samlbackend = SAMLBackend(
375+
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
376+
)
377+
resp = samlbackend.start_auth(context, InternalData())
378+
assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
379+
380+
context.decorate(Context.KEY_FORCE_AUTHN, "0")
381+
context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"]
382+
backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True
383+
samlbackend = SAMLBackend(
384+
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
385+
)
386+
resp = samlbackend.start_auth(context, InternalData())
387+
assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
388+
389+
def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_true(
390+
self, context, sp_conf, idp_conf
391+
):
392+
sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
393+
sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
394+
395+
context.decorate(Context.KEY_FORCE_AUTHN, "true")
396+
context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"]
397+
398+
backend_conf = {
399+
SAMLBackend.KEY_SP_CONFIG: sp_conf,
400+
SAMLBackend.KEY_DISCO_SRV: DISCOSRV_URL,
401+
SAMLBackend.KEY_MEMORIZE_IDP: True,
402+
SAMLBackend.KEY_MIRROR_FORCE_AUTHN: True,
403+
}
404+
samlbackend = SAMLBackend(
405+
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
406+
)
407+
resp = samlbackend.start_auth(context, InternalData())
408+
assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
409+
410+
backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True
411+
samlbackend = SAMLBackend(
412+
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
413+
)
414+
resp = samlbackend.start_auth(context, InternalData())
415+
assert_redirect_to_idp(resp, idp_conf)
416+
417+
def test_use_of_disco_or_redirect_to_idp_when_using_mdq_and_forceauthn_is_set_1(
418+
self, context, sp_conf, idp_conf
419+
):
420+
sp_conf["metadata"]["inline"] = [create_metadata_from_config_dict(idp_conf)]
421+
sp_conf["metadata"]["mdq"] = ["https://mdq.example.com"]
422+
423+
context.decorate(Context.KEY_FORCE_AUTHN, "1")
424+
context.state[Context.KEY_MEMORIZED_IDP] = idp_conf["entityid"]
425+
426+
backend_conf = {
427+
SAMLBackend.KEY_SP_CONFIG: sp_conf,
428+
SAMLBackend.KEY_DISCO_SRV: DISCOSRV_URL,
429+
SAMLBackend.KEY_MEMORIZE_IDP: True,
430+
SAMLBackend.KEY_MIRROR_FORCE_AUTHN: True,
431+
}
432+
samlbackend = SAMLBackend(
433+
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
434+
)
435+
resp = samlbackend.start_auth(context, InternalData())
436+
assert_redirect_to_discovery_server(resp, sp_conf, DISCOSRV_URL)
437+
438+
backend_conf[SAMLBackend.KEY_USE_MEMORIZED_IDP_WHEN_FORCE_AUTHN] = True
439+
samlbackend = SAMLBackend(
440+
None, INTERNAL_ATTRIBUTES, backend_conf, "base_url", "saml_backend"
441+
)
442+
resp = samlbackend.start_auth(context, InternalData())
443+
assert_redirect_to_idp(resp, idp_conf)

0 commit comments

Comments
 (0)