Skip to content

Commit 9cbf612

Browse files
authored
Customizabl acs error handler, more test coverage (#219)
1 parent b8cbc22 commit 9cbf612

File tree

4 files changed

+65
-44
lines changed

4 files changed

+65
-44
lines changed

djangosaml2/acs_failures.py

Lines changed: 0 additions & 12 deletions
This file was deleted.

djangosaml2/tests/__init__.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
get_session_id_from_saml2,
4040
get_subject_id_from_saml2,
4141
saml2_from_httpredirect_request)
42-
from djangosaml2.views import finish_logout
42+
from djangosaml2.views import (EchoAttributesView, _set_subject_id,
43+
finish_logout)
4344
from saml2.config import SPConfig
4445
from saml2.s_utils import decode_base64_and_inflate, deflate_and_base64_encode
4546

@@ -95,9 +96,6 @@ def add_outstanding_query(self, session_id, came_from):
9596
self.saml_session.save()
9697
self.client.cookies[settings.SESSION_COOKIE_NAME] = self.saml_session.session_key
9798

98-
def render_template(self, text):
99-
return Template(text).render(Context())
100-
10199
def b64_for_post(self, xml_text, encoding='utf-8'):
102100
return base64.b64encode(xml_text.encode(encoding)).decode('ascii')
103101

@@ -406,6 +404,47 @@ def do_login(self):
406404
self.assertEqual(response.status_code, 302)
407405
return subject_id
408406

407+
def test_echo_view_no_saml_session(self):
408+
settings.SAML_CONFIG = conf.create_conf(
409+
sp_host='sp.example.com',
410+
idp_hosts=['idp.example.com'],
411+
metadata_file='remote_metadata_one_idp.xml',
412+
)
413+
self.do_login()
414+
415+
request = RequestFactory().get('/bar/foo')
416+
request.COOKIES = self.client.cookies
417+
request.user = User.objects.last()
418+
419+
middleware = SamlSessionMiddleware()
420+
middleware.process_request(request)
421+
422+
response = EchoAttributesView.as_view()(request)
423+
self.assertEqual(response.status_code, 200)
424+
self.assertEqual(response.content.decode(), 'No active SAML identity found. Are you sure you have logged in via SAML?')
425+
426+
def test_echo_view_success(self):
427+
settings.SAML_CONFIG = conf.create_conf(
428+
sp_host='sp.example.com',
429+
idp_hosts=['idp.example.com'],
430+
metadata_file='remote_metadata_one_idp.xml',
431+
)
432+
self.do_login()
433+
434+
request = RequestFactory().get('/')
435+
request.user = User.objects.last()
436+
437+
middleware = SamlSessionMiddleware()
438+
middleware.process_request(request)
439+
440+
saml_session_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
441+
getattr(request, saml_session_name)['_saml2_subject_id'] = '1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03'
442+
getattr(request, saml_session_name).save()
443+
444+
response = EchoAttributesView.as_view()(request)
445+
self.assertEqual(response.status_code, 200)
446+
self.assertIn('<h1>SAML attributes</h1>', response.content.decode(), 'Echo page not rendered')
447+
409448
def test_logout(self):
410449
settings.SAML_CONFIG = conf.create_conf(
411450
sp_host='sp.example.com',
@@ -428,8 +467,7 @@ def test_logout(self):
428467

429468
saml_request = params['SAMLRequest'][0]
430469

431-
if 'LogoutRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
432-
raise Exception('Not a valid LogoutRequest')
470+
self.assertIn('LogoutRequest xmlns', decode_base64_and_inflate(saml_request).decode('utf-8'), 'Not a valid LogoutRequest')
433471

434472
def test_logout_service_local(self):
435473
settings.SAML_CONFIG = conf.create_conf(
@@ -453,8 +491,8 @@ def test_logout_service_local(self):
453491
self.assertIn('SAMLRequest', params)
454492

455493
saml_request = params['SAMLRequest'][0]
456-
if 'LogoutRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
457-
raise Exception('Not a valid LogoutRequest')
494+
495+
self.assertIn('LogoutRequest xmlns', decode_base64_and_inflate(saml_request).decode('utf-8'), 'Not a valid LogoutRequest')
458496

459497
# now simulate a logout response sent by the idp
460498
expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="XXXXXXXXXXXXXXXXXXXXXX" Version="2.0" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" Reason=""><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID SPNameQualifier="http://sp.example.com/saml2/metadata/" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">1f87035b4c1325b296a53d92097e6b3fa36d7e30ee82e3fcb0680d60243c1f03</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
@@ -501,8 +539,7 @@ def test_logout_service_global(self):
501539
self.assertIn('SAMLResponse', params)
502540
saml_response = params['SAMLResponse'][0]
503541

504-
if 'Response xmlns' not in decode_base64_and_inflate(saml_response).decode('utf-8'):
505-
raise Exception('Not a valid Response')
542+
self.assertIn('Response xmlns', decode_base64_and_inflate(saml_response).decode('utf-8'), 'Not a valid Response')
506543

507544
def test_incomplete_logout(self):
508545
settings.SAML_CONFIG = conf.create_conf(sp_host='sp.example.com',
@@ -620,11 +657,8 @@ def test_custom_conf_loader_from_view(self):
620657

621658
class SessionEnabledTestCase(TestCase):
622659
def get_session(self):
623-
if self.client.session:
624-
session = self.client.session
625-
else:
626-
engine = import_module(settings.SESSION_ENGINE)
627-
session = engine.SessionStore()
660+
engine = import_module(settings.SESSION_ENGINE)
661+
session = self.client.session or engine.SessionStore()
628662
return session
629663

630664
def set_session_cookies(self, session):

djangosaml2/utils.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,6 @@ def get_location(http_info):
7676
return http_info['url']
7777

7878

79-
def fail_acs_response(request, *args, **kwargs):
80-
""" Serves as a common mechanism for ending ACS in case of any SAML related failure.
81-
Handling can be configured by setting the SAML_ACS_FAILURE_RESPONSE_FUNCTION as
82-
suitable for the project.
83-
84-
The default behavior uses SAML specific template that is rendered on any ACS error,
85-
but this can be simply changed so that PermissionDenied exception is raised instead.
86-
"""
87-
failure_function = import_string(get_custom_setting('SAML_ACS_FAILURE_RESPONSE_FUNCTION',
88-
'djangosaml2.acs_failures.template_failure'))
89-
return failure_function(request, *args, **kwargs)
90-
91-
9279
def validate_referral_url(request, url):
9380
# Ensure the user-originating redirection url is safe.
9481
# By setting SAML_ALLOWED_HOSTS in settings.py the user may provide a list of "allowed"

djangosaml2/views.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
from .conf import get_config
4949
from .exceptions import IdPConfigurationMissing
5050
from .overrides import Saml2Client
51-
from .utils import (available_idps, fail_acs_response, get_custom_setting,
51+
from .utils import (available_idps, get_custom_setting,
5252
get_idp_sso_supported_bindings, get_location,
5353
validate_referral_url)
5454

@@ -269,6 +269,18 @@ class AssertionConsumerServiceView(SPConfigMixin, View):
269269
though some implementations may instead register their own subclasses of Saml2Backend.
270270
"""
271271

272+
def handle_acs_failure(self, request, exception=None, status=403, **kwargs):
273+
""" Error handler if the login attempt fails. Override this to customize the error response.
274+
"""
275+
276+
# Backwards compatibility: if a custom setting was defined, use that one
277+
custom_failure_function = get_custom_setting('SAML_ACS_FAILURE_RESPONSE_FUNCTION')
278+
if custom_failure_function:
279+
failure_function = custom_failure_function if callable(custom_failure_function) else import_string(custom_failure_function)
280+
return failure_function(request, exception, status, **kwargs)
281+
282+
return render(request, 'djangosaml2/login_error.html', {'exception': exception}, status=status)
283+
272284
def post(self, request, attribute_mapping=None, create_unknown_user=None):
273285
""" SAML Authorization Response endpoint
274286
"""
@@ -317,10 +329,10 @@ def post(self, request, attribute_mapping=None, create_unknown_user=None):
317329
logger.exception("Received SAMLResponse when no request has been made.")
318330

319331
if _exception:
320-
return fail_acs_response(request, exception=_exception)
332+
return self.handle_acs_failure(request, exception=_exception)
321333
elif response is None:
322334
logger.warning("Invalid SAML Assertion received (unknown error).")
323-
return fail_acs_response(request, status=400, exception=SuspiciousOperation('Unknown SAML2 error'))
335+
return self.handle_acs_failure(request, status=400, exception=SuspiciousOperation('Unknown SAML2 error'))
324336

325337
session_id = response.session_id()
326338
oq_cache.delete(session_id)
@@ -340,7 +352,7 @@ def post(self, request, attribute_mapping=None, create_unknown_user=None):
340352
create_unknown_user=create_unknown_user)
341353
if user is None:
342354
logger.warning("Could not authenticate user received in SAML Assertion. Session info: %s", session_info)
343-
return fail_acs_response(request, exception=PermissionDenied('No user could be authenticated.'))
355+
return self.handle_acs_failure(request, exception=PermissionDenied('No user could be authenticated.'))
344356

345357
auth.login(self.request, user)
346358
_set_subject_id(request.saml_session, session_info['name_id'])

0 commit comments

Comments
 (0)