Skip to content

Commit 6e105af

Browse files
committed
Refactor ACS for better extensibility
1 parent 443bf43 commit 6e105af

File tree

3 files changed

+71
-28
lines changed

3 files changed

+71
-28
lines changed

djangosaml2/acs_failures.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# This module defines a set of useful ACS failure functions that are used to
4+
# produce an output suitable for end user in case of SAML failure.
5+
#
6+
from __future__ import unicode_literals
7+
8+
from django.core.exceptions import PermissionDenied
9+
from django.shortcuts import render
10+
11+
12+
def template_failure(request, status=403, **kwargs):
13+
""" Renders a SAML-specific template with general authentication error description. """
14+
return render(request, 'djangosaml2/login_error.html', status=status)
15+
16+
17+
def exception_failure(request, exc_class=PermissionDenied, **kwargs):
18+
""" Rather than using a custom SAML specific template that is rendered on failure,
19+
this makes use of a standard exception handling machinery present in Django
20+
and thus ends up rendering a project-wide error page for Permission Denied exceptions.
21+
"""
22+
raise exc_class

djangosaml2/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from importlib import import_module
1516
from django.conf import settings
1617
from django.core.exceptions import ImproperlyConfigured
1718
from saml2.s_utils import UnknownSystemEntity
@@ -66,3 +67,16 @@ def get_location(http_info):
6667
header_name, header_value = headers[0]
6768
assert header_name == 'Location'
6869
return header_value
70+
71+
72+
def fail_acs_response(request, *args, **kwargs):
73+
""" Serves as a common mechanism for ending ACS in case of any SAML related failure.
74+
Handling can be configured by setting the SAML_ACS_FAILURE_RESPONSE_FUNCTION as
75+
suitable for the project.
76+
77+
The default behavior uses SAML specific template that is rendered on any ACS error,
78+
but this can be simply changed so that PermissionDenied exception is raised instead.
79+
"""
80+
failure_function = import_module(get_custom_setting('SAML_ACS_FAILURE_RESPONSE_FUNCTION',
81+
'djangosaml2.acs_failures.template_failure'))
82+
return failure_function(request, *args, **kwargs)

djangosaml2/views.py

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from django.contrib import auth
2626
from django.contrib.auth.decorators import login_required
2727
from django.contrib.auth.views import logout as django_logout
28-
from django.core.exceptions import PermissionDenied
28+
from django.core.exceptions import PermissionDenied, SuspiciousOperation
2929
from django.http import Http404, HttpResponse
3030
from django.http import HttpResponseRedirect # 30x
3131
from django.http import HttpResponseBadRequest, HttpResponseForbidden # 40x
@@ -43,15 +43,15 @@
4343
from saml2.ident import code, decode
4444
from saml2.sigver import MissingKey
4545
from saml2.s_utils import UnsupportedBinding
46-
from saml2.response import StatusError
46+
from saml2.response import StatusError, StatusAuthnFailed, SignatureError, StatusRequestDenied
4747
from saml2.validate import ResponseLifetimeExceed, ToEarly
4848
from saml2.xmldsig import SIG_RSA_SHA1, SIG_RSA_SHA256 # support for SHA1 is required by spec
4949

5050
from djangosaml2.cache import IdentityCache, OutstandingQueriesCache
5151
from djangosaml2.cache import StateCache
5252
from djangosaml2.conf import get_config
5353
from djangosaml2.signals import post_authenticated
54-
from djangosaml2.utils import get_custom_setting, available_idps, get_location, get_idp_sso_supported_bindings
54+
from djangosaml2.utils import fail_acs_response, get_custom_setting, available_idps, get_location, get_idp_sso_supported_bindings
5555

5656

5757
logger = logging.getLogger('djangosaml2')
@@ -235,38 +235,44 @@ def assertion_consumer_service(request,
235235
djangosaml2.backends.Saml2Backend that should be
236236
enabled in the settings.py
237237
"""
238-
attribute_mapping = attribute_mapping or get_custom_setting(
239-
'SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )})
240-
create_unknown_user = create_unknown_user or get_custom_setting(
241-
'SAML_CREATE_UNKNOWN_USER', True)
242-
logger.debug('Assertion Consumer Service started')
243-
238+
attribute_mapping = attribute_mapping or get_custom_setting('SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )})
239+
create_unknown_user = create_unknown_user or get_custom_setting('SAML_CREATE_UNKNOWN_USER', True)
244240
conf = get_config(config_loader_path, request)
245-
if 'SAMLResponse' not in request.POST:
246-
return HttpResponseBadRequest(
247-
'Couldn\'t find "SAMLResponse" in POST data.')
248-
xmlstr = request.POST['SAMLResponse']
241+
try:
242+
xmlstr = request.POST['SAMLResponse']
243+
except KeyError:
244+
logger.warning('Missing "SAMLResponse" parameter in POST data.')
245+
raise SuspiciousOperation
246+
249247
client = Saml2Client(conf, identity_cache=IdentityCache(request.session))
250248

251249
oq_cache = OutstandingQueriesCache(request.session)
252250
outstanding_queries = oq_cache.outstanding_queries()
253251

254252
try:
255-
response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST,
256-
outstanding_queries)
257-
except (StatusError, ResponseLifetimeExceed, ToEarly):
258-
logger.exception('Error processing SAML Assertion')
259-
return render(request, 'djangosaml2/login_error.html', status=403)
260-
253+
response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST, outstanding_queries)
254+
except (StatusError, ToEarly):
255+
logger.exception("Error processing SAML Assertion.")
256+
return fail_acs_response(request)
257+
except ResponseLifetimeExceed:
258+
logger.info("SAML Assertion is no longer valid. Possibly caused by network delay or replay attack.", exc_info=True)
259+
return fail_acs_response(request)
260+
except SignatureError:
261+
logger.info("Invalid or malformed SAML Assertion.", exc_info=True)
262+
return fail_acs_response(request)
263+
except StatusAuthnFailed:
264+
logger.info("Authentication denied for user by IdP.", exc_info=True)
265+
return fail_acs_response(request)
266+
except StatusRequestDenied:
267+
logger.warning("Authentication interrupted at IdP.", exc_info=True)
268+
return fail_acs_response(request)
261269
except MissingKey:
262-
logger.error('MissingKey error in ACS')
263-
return HttpResponseForbidden(
264-
"The Identity Provider is not configured correctly: "
265-
"the certificate key is missing")
270+
logger.exception("SAML Identity Provider is not configured correctly: certificate key is missing!")
271+
return fail_acs_response(request)
272+
266273
if response is None:
267-
logger.error('SAML response is None')
268-
return HttpResponseBadRequest(
269-
"SAML response has errors. Please check the logs")
274+
logger.warning("Invalid SAML Assertion received (unknown error).")
275+
return fail_acs_response(request, status=400, exc_class=SuspiciousOperation)
270276

271277
session_id = response.session_id()
272278
oq_cache.delete(session_id)
@@ -279,17 +285,18 @@ def assertion_consumer_service(request,
279285
if callable(create_unknown_user):
280286
create_unknown_user = create_unknown_user()
281287

282-
logger.debug('Trying to authenticate the user')
288+
logger.debug('Trying to authenticate the user. Session info: %s', session_info)
283289
user = auth.authenticate(request=request,
284290
session_info=session_info,
285291
attribute_mapping=attribute_mapping,
286292
create_unknown_user=create_unknown_user)
287293
if user is None:
288-
logger.error('The user is None')
294+
logger.warning("Could not authenticate user received in SAML Assertion. Session info: %s", session_info)
289295
raise PermissionDenied
290296

291297
auth.login(request, user)
292298
_set_subject_id(request.session, session_info['name_id'])
299+
logger.debug("User %s authenticated via SSO.", user)
293300

294301
logger.debug('Sending the post_authenticated signal')
295302
post_authenticated.send_robust(sender=user, session_info=session_info)

0 commit comments

Comments
 (0)