From bcb055263b8ee525e1bc560ec557413a0099ee2c Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 28 May 2014 20:19:54 +0200 Subject: [PATCH] improve handling of authentication errors All errors are logged as warnings, status message and status codes are logged, and if DEBUG is True, shown to the user. --- README | 13 +++++ mellon/app_settings.py | 2 + .../mellon/authentication_failed.html | 44 ++++++++++------ mellon/views.py | 52 ++++++++++++++----- 4 files changed, 83 insertions(+), 28 deletions(-) diff --git a/README b/README index 0ddff95..49092d7 100644 --- a/README +++ b/README @@ -216,3 +216,16 @@ MELLON_CREATE_GROUP ------------------- Whether to create group or only assign existing groups, default is True. + +MELLON_ERROR_URL +================ + +URL for the continue link when authentication fails, default is +None. If not ERROR_URL is None, the RelayState is used. If there is +no RelayState, the LOGIN_REDIRECT_URL, which defaults to /, is used. + +MELLON_ERROR_REDIRECT_AFTER_TIMEOUT +=================================== + +Timeout in seconds before automatically redirecting the user to the +continue URL when authentication has failed. Default is 120 seconds. diff --git a/mellon/app_settings.py b/mellon/app_settings.py index ed071e4..4460b02 100644 --- a/mellon/app_settings.py +++ b/mellon/app_settings.py @@ -21,6 +21,8 @@ class AppSettings(object): 'AUTHN_CLASSREF': (), 'GROUP_ATTRIBUTE': None, 'CREATE_GROUP': True, + 'ERROR_URL': None, + 'ERROR_REDIRECT_AFTER_TIMEOUT': 120, } @property diff --git a/mellon/templates/mellon/authentication_failed.html b/mellon/templates/mellon/authentication_failed.html index 10ddc14..f86b5f1 100644 --- a/mellon/templates/mellon/authentication_failed.html +++ b/mellon/templates/mellon/authentication_failed.html @@ -1,24 +1,38 @@ {% extends "base.html" %} {% load i18n %} +{% block extra_scripts %} + {% if error_redirect_after_timeout %} + -

{% trans "Authentication failed" %}

-

- {% blocktrans %}The authentication has failed, you can return to - the last page you where. - {% endblocktrans %} +

{% trans "Authentication failed" %}

+

+ {% blocktrans %}The authentication has failed.{% endblocktrans %}

-
-
{% trans "Issuer" %}
-
{{ issuer }}
- {% if status_message %} -
{% trans "Message" %}
-
{{ status_message }}
- {% endif %} -
{% trans "Codes" %}
-
{{ status_codes|join:", " }}
-
+

+ {% trans "Continue" %} +

+ {% if debug %} +
+
{% trans "Issuer" %}
+
{{ issuer }}
+ {% if status_message %} +
{% trans "Message" %}
+
{{ status_message }}
+ {% endif %} + {% if status_codes %} +
{% trans "Codes" %}
+
{{ status_codes|join:", " }}
+ {% endif %} + {% if relaystate %} +
{% trans "Relaystate" %}
+
{{ relaystate }}
+ {% endif %} +
+ {% endif %} {% endblock %} diff --git a/mellon/views.py b/mellon/views.py index 00e574a..923a335 100644 --- a/mellon/views.py +++ b/mellon/views.py @@ -7,6 +7,8 @@ from django.conf import settings from django.views.decorators.csrf import csrf_exempt from django.shortcuts import render, redirect, resolve_url from django.utils.http import same_origin +from django.utils.translation import ugettext as _ +from django.contrib import messages import lasso @@ -29,31 +31,55 @@ class LoginView(View): if 'SAMLResponse' not in request.POST: return self.get(request, *args, **kwargs) login = utils.create_login(request) + idp_message = None + status_codes = [] try: login.processAuthnResponseMsg(request.POST['SAMLResponse']) login.acceptSso() - except lasso.ProfileStatusNotSuccessError, e: - status_codes = [] + except lasso.ProfileCannotVerifySignatureError: + log.warning('SAML authentication failed: signature validation failed for %r', + login.remoteProviderId) + except lasso.ParamError: + log.exception('lasso param error') + except (lasso.ProfileStatusNotSuccessError, lasso.ProfileRequestDeniedError): status = login.response.status a = status while a.statusCode: status_codes.append(a.statusCode.value) a = a.statusCode - log.warning('SAML authentication failed, codes: %r', status_codes) + args = ['SAML authentication failed: status is not success codes: %r', status_codes] if status.statusMessage: - log.warning('SAML authentication failed, message: %r', - status.statusMessage) - next_url = login.msgRelayState or \ - resolve_url(settings.LOGIN_REDIRECT_URL) - return render(request, 'mellon/authentication_failed.html', { - 'status_message': status.statusMessage, - 'status_codes': status_codes, - 'issuer': login.remoteProviderId, - 'next_url': next_url, - }) + idp_message = status.statusMessage.decode('utf-8') + args[0] += ' message: %r' + args.append(status.statusMessage) + log.warning(*args) except lasso.Error, e: return HttpResponseBadRequest('error processing the authentication ' 'response: %r' % e) + else: + return self.login_success(request, login) + return self.login_failure(request, login, idp_message, status_codes) + + def login_failure(self, request, login, idp_message, status_codes): + '''show error message to user after a login failure''' + idp = self.get_idp(request) + error_url = utils.get_parameter(idp, 'ERROR_URL') + error_redirect_after_timeout = utils.get_parameter(idp, 'ERROR_REDIRECT_AFTER_TIMEOUT') + if error_url: + error_url = resolve_url(error_url) + next_url = error_url or login.msgRelayState or resolve_url(settings.LOGIN_REDIRECT_URL) + return render(request, 'mellon/authentication_failed.html', { + 'debug': settings.DEBUG, + 'idp_message': idp_message, + 'status_codes': status_codes, + 'issuer': login.remoteProviderId, + 'next_url': next_url, + 'error_url': error_url, + 'relaystate': login.msgRelayState, + 'error_redirect_after_timeout': error_redirect_after_timeout, + }) + + def login_success(self, request, login): name_id = login.nameIdentifier attributes = {} attribute_statements = login.assertion.attributeStatement