Skip to content

Commit fa181cd

Browse files
committed
assertion consumer service now more customizable
As a class-based view, the AssertionConsumerServiceView provides additional hooks that subclasses can use to implement custom functionality.
1 parent 5561d11 commit fa181cd

File tree

2 files changed

+137
-87
lines changed

2 files changed

+137
-87
lines changed

djangosaml2/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
urlpatterns = [
2121
url(r'^login/$', views.login, name='saml2_login'),
22-
url(r'^acs/$', views.assertion_consumer_service, name='saml2_acs'),
22+
url(r'^acs/$', views.AssertionConsumerServiceView.as_view(), name='saml2_acs'),
2323
url(r'^logout/$', views.logout, name='saml2_logout'),
2424
url(r'^ls/$', views.logout_service, name='saml2_ls'),
2525
url(r'^ls/post/$', views.logout_service_post, name='saml2_ls_post'),

djangosaml2/views.py

Lines changed: 136 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
from django.template import TemplateDoesNotExist
3535
from django.utils.six import text_type, binary_type, PY3
3636
from django.views.decorators.csrf import csrf_exempt
37+
from django.views.generic import View
38+
from django.utils.decorators import method_decorator
3739

3840
from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
3941
from saml2.metadata import entity_descriptor
@@ -238,101 +240,149 @@ def login(request,
238240
return http_response
239241

240242

241-
@require_POST
242-
@csrf_exempt
243-
def assertion_consumer_service(request,
244-
config_loader_path=None,
245-
attribute_mapping=None,
246-
create_unknown_user=None):
247-
"""SAML Authorization Response endpoint
248-
243+
class AssertionConsumerServiceView(View):
244+
"""
249245
The IdP will send its response to this view, which
250246
will process it with pysaml2 help and log the user
251247
in using the custom Authorization backend
252248
djangosaml2.backends.Saml2Backend that should be
253249
enabled in the settings.py
254250
"""
255-
attribute_mapping = attribute_mapping or get_custom_setting('SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )})
256-
create_unknown_user = create_unknown_user if create_unknown_user is not None else \
257-
get_custom_setting('SAML_CREATE_UNKNOWN_USER', True)
258-
conf = get_config(config_loader_path, request)
259-
try:
260-
xmlstr = request.POST['SAMLResponse']
261-
except KeyError:
262-
logger.warning('Missing "SAMLResponse" parameter in POST data.')
263-
raise SuspiciousOperation
264251

265-
client = Saml2Client(conf, identity_cache=IdentityCache(request.session))
252+
@method_decorator(csrf_exempt)
253+
def dispatch(self, request, *args, **kwargs):
254+
"""
255+
This view needs to be CSRF exempt
256+
"""
257+
return super(AssertionConsumerServiceView, self).dispatch(request, *args, **kwargs)
258+
259+
@method_decorator(csrf_exempt)
260+
def post(self,
261+
request,
262+
config_loader_path=None,
263+
attribute_mapping=None,
264+
create_unknown_user=None):
265+
"""
266+
SAML Authorization Response endpoint
267+
"""
268+
import epdb; epdb.set_trace()
269+
attribute_mapping = attribute_mapping or get_custom_setting('SAML_ATTRIBUTE_MAPPING', {'uid': ('username', )})
270+
create_unknown_user = create_unknown_user if create_unknown_user is not None else \
271+
get_custom_setting('SAML_CREATE_UNKNOWN_USER', True)
272+
conf = get_config(config_loader_path, request)
273+
try:
274+
xmlstr = request.POST['SAMLResponse']
275+
except KeyError:
276+
logger.warning('Missing "SAMLResponse" parameter in POST data.')
277+
raise SuspiciousOperation
266278

267-
oq_cache = OutstandingQueriesCache(request.session)
268-
outstanding_queries = oq_cache.outstanding_queries()
279+
client = Saml2Client(conf, identity_cache=IdentityCache(self.request.session))
280+
281+
oq_cache = OutstandingQueriesCache(self.request.session)
282+
outstanding_queries = oq_cache.outstanding_queries()
283+
284+
try:
285+
response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST, outstanding_queries)
286+
except (StatusError, ToEarly):
287+
logger.exception("Error processing SAML Assertion.")
288+
return fail_acs_response(self.request)
289+
except ResponseLifetimeExceed:
290+
logger.info("SAML Assertion is no longer valid. Possibly caused by network delay or replay attack.", exc_info=True)
291+
return fail_acs_response(self.request)
292+
except SignatureError:
293+
logger.info("Invalid or malformed SAML Assertion.", exc_info=True)
294+
return fail_acs_response(self.request)
295+
except StatusAuthnFailed:
296+
logger.info("Authentication denied for user by IdP.", exc_info=True)
297+
return fail_acs_response(self.request)
298+
except StatusRequestDenied:
299+
logger.warning("Authentication interrupted at IdP.", exc_info=True)
300+
return fail_acs_response(self.request)
301+
except MissingKey:
302+
logger.exception("SAML Identity Provider is not configured correctly: certificate key is missing!")
303+
return fail_acs_response(self.request)
304+
except UnsolicitedResponse:
305+
logger.exception("Received SAMLResponse when no request has been made.")
306+
return fail_acs_response(self.request)
307+
308+
if response is None:
309+
logger.warning("Invalid SAML Assertion received (unknown error).")
310+
return fail_acs_response(self.request, status=400, exc_class=SuspiciousOperation)
311+
312+
session_id = response.session_id()
313+
oq_cache.delete(session_id)
314+
315+
# authenticate the remote user
316+
session_info = response.session_info()
317+
318+
if callable(attribute_mapping):
319+
attribute_mapping = attribute_mapping()
320+
if callable(create_unknown_user):
321+
create_unknown_user = create_unknown_user()
322+
323+
logger.debug('Trying to authenticate the user. Session info: %s', session_info)
324+
user = auth.authenticate(request=self.request,
325+
session_info=session_info,
326+
attribute_mapping=attribute_mapping,
327+
create_unknown_user=create_unknown_user)
328+
if user is None:
329+
logger.warning("Could not authenticate user received in SAML Assertion. Session info: %s", session_info)
330+
raise PermissionDenied
331+
if user is None:
332+
logger.error('The user is None. Redirecting to registration page.')
333+
return HttpResponseRedirect('/')
334+
335+
auth.login(self.request, user)
336+
_set_subject_id(self.request.session, session_info['name_id'])
337+
logger.debug("User %s authenticated via SSO.", user)
338+
logger.debug('Sending the post_authenticated signal')
339+
340+
post_authenticated.send_robust(sender=user, session_info=session_info)
341+
self.customize_session(user, session_info)
342+
343+
custom_redirect_url = self.custom_redirect(user, relay_state)
344+
if custom_redirect_url:
345+
return HttpResponseRedirect(custom_redirect_url)
346+
347+
relay_state = self.build_relay_state()
348+
if not is_safe_url_compat(url=relay_state, allowed_hosts={self.request.get_host()}):
349+
relay_state = settings.LOGIN_REDIRECT_URL
350+
logger.debug('Redirecting to the RelayState: %s', relay_state)
351+
return HttpResponseRedirect(relay_state)
352+
353+
def build_relay_state(self):
354+
"""
355+
The relay state is a URL used to redirect the user to the view where they came from.
356+
"""
357+
default_relay_state = get_custom_setting('ACS_DEFAULT_REDIRECT_URL',
358+
settings.LOGIN_REDIRECT_URL)
359+
relay_state = self.request.POST.get('RelayState', '/')
360+
relay_state = self.customize_relay_state(relay_state)
361+
if not relay_state:
362+
logger.warning('The RelayState parameter exists but is empty')
363+
relay_state = default_relay_state
364+
return relay_state
365+
366+
def customize_session(self, user, session_info):
367+
"""
368+
Subclasses can use this for customized functionality around user sessions.
369+
"""
370+
371+
def customize_relay_state(self, relay_state):
372+
"""
373+
Subclasses may override this method to implement custom logic for relay state.
374+
"""
375+
return relay_state
376+
377+
def custom_redirect(self, user, relay_state):
378+
"""
379+
Subclasses may override this method to implement custom logic for redirect.
380+
381+
For example, some sites may require user registration if the user has not
382+
yet been provisioned.
383+
"""
384+
return None
269385

270-
try:
271-
response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST, outstanding_queries)
272-
except (StatusError, ToEarly):
273-
logger.exception("Error processing SAML Assertion.")
274-
return fail_acs_response(request)
275-
except ResponseLifetimeExceed:
276-
logger.info("SAML Assertion is no longer valid. Possibly caused by network delay or replay attack.", exc_info=True)
277-
return fail_acs_response(request)
278-
except SignatureError:
279-
logger.info("Invalid or malformed SAML Assertion.", exc_info=True)
280-
return fail_acs_response(request)
281-
except StatusAuthnFailed:
282-
logger.info("Authentication denied for user by IdP.", exc_info=True)
283-
return fail_acs_response(request)
284-
except StatusRequestDenied:
285-
logger.warning("Authentication interrupted at IdP.", exc_info=True)
286-
return fail_acs_response(request)
287-
except MissingKey:
288-
logger.exception("SAML Identity Provider is not configured correctly: certificate key is missing!")
289-
return fail_acs_response(request)
290-
except UnsolicitedResponse:
291-
logger.exception("Received SAMLResponse when no request has been made.")
292-
return fail_acs_response(request)
293-
294-
if response is None:
295-
logger.warning("Invalid SAML Assertion received (unknown error).")
296-
return fail_acs_response(request, status=400, exc_class=SuspiciousOperation)
297-
298-
session_id = response.session_id()
299-
oq_cache.delete(session_id)
300-
301-
# authenticate the remote user
302-
session_info = response.session_info()
303-
304-
if callable(attribute_mapping):
305-
attribute_mapping = attribute_mapping()
306-
if callable(create_unknown_user):
307-
create_unknown_user = create_unknown_user()
308-
309-
logger.debug('Trying to authenticate the user. Session info: %s', session_info)
310-
user = auth.authenticate(request=request,
311-
session_info=session_info,
312-
attribute_mapping=attribute_mapping,
313-
create_unknown_user=create_unknown_user)
314-
if user is None:
315-
logger.warning("Could not authenticate user received in SAML Assertion. Session info: %s", session_info)
316-
raise PermissionDenied
317-
318-
auth.login(request, user)
319-
_set_subject_id(request.session, session_info['name_id'])
320-
logger.debug("User %s authenticated via SSO.", user)
321-
322-
logger.debug('Sending the post_authenticated signal')
323-
post_authenticated.send_robust(sender=user, session_info=session_info)
324-
325-
# redirect the user to the view where he came from
326-
default_relay_state = get_custom_setting('ACS_DEFAULT_REDIRECT_URL',
327-
settings.LOGIN_REDIRECT_URL)
328-
relay_state = request.POST.get('RelayState', default_relay_state)
329-
if not relay_state:
330-
logger.warning('The RelayState parameter exists but is empty')
331-
relay_state = default_relay_state
332-
if not is_safe_url_compat(url=relay_state, allowed_hosts={request.get_host()}):
333-
relay_state = settings.LOGIN_REDIRECT_URL
334-
logger.debug('Redirecting to the RelayState: %s', relay_state)
335-
return HttpResponseRedirect(relay_state)
336386

337387

338388
@login_required

0 commit comments

Comments
 (0)