retry login when artifact resolution return an empty message (fixes #12795)

This commit also add a test of artifact login.
This commit is contained in:
Benjamin Dauvergne 2016-07-29 11:53:36 +02:00
parent 686221fd65
commit 09ff054f57
3 changed files with 97 additions and 6 deletions

View File

@ -4,6 +4,7 @@ import lasso
import uuid
from requests.exceptions import RequestException
from django.core.urlresolvers import reverse
from django.views.generic import View
from django.http import HttpResponseBadRequest, HttpResponseRedirect, HttpResponse
from django.contrib import auth
@ -13,6 +14,7 @@ from django.shortcuts import render, resolve_url
from django.utils.http import urlencode
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.db import transaction
from django.utils.translation import ugettext as _
from . import app_settings, utils
@ -212,6 +214,14 @@ class LoginView(ProfileMixin, LogMixin, View):
return HttpResponseRedirect(next_url)
def retry_login(self):
'''Retry login if it failed for a temporary error'''
url = reverse('mellon_login')
next_url = self.get_next_url()
if next_url:
url = '%s?%s' % (url, urlencode({REDIRECT_FIELD_NAME: next_url}))
return HttpResponseRedirect(url)
def continue_sso_artifact(self, request, method):
idp_message = None
status_codes = []
@ -220,12 +230,14 @@ class LoginView(ProfileMixin, LogMixin, View):
message = request.META['QUERY_STRING']
artifact = request.GET['SAMLart']
relay_state = request.GET.get('RelayState')
else: # method == lasso.HTTP_METHOD_ARTIFACT_POST:
else: # method == lasso.HTTP_METHOD_ARTIFACT_POST:
message = request.POST['SAMLart']
artifact = request.POST['SAMLart']
relay_state = request.POST.get('RelayState')
self.profile = login = utils.create_login(request)
if relay_state and utils.is_nonnull(relay_state):
login.msgRelayState = relay_state
try:
login.initRequest(message, method)
except lasso.ProfileInvalidArtifactError:
@ -249,7 +261,8 @@ class LoginView(ProfileMixin, LogMixin, View):
verify=verify_ssl_certificate)
except RequestException, e:
self.log.warning('unable to reach %r: %s', login.msgUrl, e)
return HttpResponseBadRequest('unable to reach %r: %s' % (login.msgUrl, e))
return self.sso_failure(request, login, _('IdP is temporarily down, please try again '
'later.'), status_codes)
if result.status_code != 200:
self.log.warning('SAML authentication failed: IdP returned %s when given artifact: %r',
result.status_code, result.content)
@ -258,6 +271,10 @@ class LoginView(ProfileMixin, LogMixin, View):
try:
login.processResponseMsg(result.content)
login.acceptSso()
except lasso.ProfileMissingResponseError:
# artifact is invalid, idp returned no response
self.log.warning('ArtifactResolveResponse is empty: dead artifact %r', artifact)
return self.retry_login()
except lasso.ProfileInvalidMsgError:
self.log.warning('ArtifactResolveResponse is malformed %r', result.content[:200])
if settings.DEBUG:
@ -288,8 +305,6 @@ class LoginView(ProfileMixin, LogMixin, View):
self.log.exception('unexpected lasso error')
return HttpResponseBadRequest('error processing the authentication response: %r' % e)
else:
if relay_state and utils.is_nonnull(relay_state):
login.msgRelayState = relay_state
return self.sso_success(request, login)
return self.sso_failure(request, login, idp_message, status_codes)

View File

@ -1,4 +1,3 @@
import os
import lasso
from pytest import fixture
@ -7,6 +6,10 @@ from django.core.urlresolvers import reverse
from mellon.utils import create_metadata
from httmock import all_requests, HTTMock, response as mock_response
from utils import reset_caplog
@fixture
def idp_metadata():
@ -57,16 +60,43 @@ class MockIdp(object):
try:
login.validateRequestMsg(auth_result, consent)
except lasso.LoginRequestDeniedError:
login.buildAuthnResponseMsg()
pass
else:
login.buildAssertion(lasso.SAML_AUTHENTICATION_METHOD_PASSWORD,
"FIXME",
"FIXME",
"FIXME",
"FIXME")
if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:
login.buildArtifactMsg(lasso.HTTP_METHOD_ARTIFACT_GET)
self.artifact = login.artifact
self.artifact_message = login.artifactMessage
elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
login.buildAuthnResponseMsg()
else:
raise NotImplementedError
return login.msgUrl, login.msgBody
def resolve_artifact(self, soap_message):
login = lasso.Login(self.server)
login.processRequestMsg(soap_message)
if hasattr(self, 'artifact') and self.artifact == login.artifact:
# artifact is known, go on !
login.artifactMessage = self.artifact_message
# forget the artifact
del self.artifact
del self.artifact_message
login.buildResponseMsg()
return login.msgBody
def mock_artifact_resolver(self):
@all_requests
def f(url, request):
content = self.resolve_artifact(request.body)
return mock_response(200, content=content,
headers={'Content-Type': 'application/soap+xml'})
return f
@fixture
def idp(sp_settings, idp_metadata, idp_private_key, sp_metadata):
@ -100,3 +130,44 @@ def test_sso_request_denied(db, app, idp, caplog, sp_settings):
response = app.post(reverse('mellon_login'), {'SAMLResponse': body})
assert "status is not success codes: [u'urn:oasis:names:tc:SAML:2.0:status:Responder',\
u'urn:oasis:names:tc:SAML:2.0:status:RequestDenied']" in caplog.text()
def test_sso_artifact(db, app, caplog, sp_settings, idp_metadata, idp_private_key, rf):
sp_settings.MELLON_DEFAULT_ASSERTION_CONSUMER_BINDING = 'artifact'
request = rf.get('/')
sp_metadata = create_metadata(request)
idp = MockIdp(idp_metadata, idp_private_key, sp_metadata)
response = app.get(reverse('mellon_login'))
url, body = idp.process_authn_request_redirect(response['Location'])
assert body is None
assert reverse('mellon_login') in url
assert 'SAMLart' in url
acs_artifact_url = url.split('testserver', 1)[1]
with HTTMock(idp.mock_artifact_resolver()):
response = app.get(acs_artifact_url)
assert 'created new user' in caplog.text()
assert 'logged in using SAML' in caplog.text()
assert response['Location'].endswith(sp_settings.LOGIN_REDIRECT_URL)
# force delog
app.session.flush()
assert 'dead artifact' not in caplog.text()
with HTTMock(idp.mock_artifact_resolver()):
response = app.get(acs_artifact_url)
# verify retry login was asked
assert 'dead artifact' in caplog.text()
assert response.status_code == 302
assert reverse('mellon_login') in url
response = response.follow()
url, body = idp.process_authn_request_redirect(response['Location'])
reset_caplog(caplog)
# verify caplog has been cleaned
assert 'created new user' not in caplog.text()
assert body is None
assert reverse('mellon_login') in url
assert 'SAMLart' in url
acs_artifact_url = url.split('testserver', 1)[1]
with HTTMock(idp.mock_artifact_resolver()):
response = app.get(acs_artifact_url)
assert 'created new user' in caplog.text()
assert 'logged in using SAML' in caplog.text()
assert response['Location'].endswith(sp_settings.LOGIN_REDIRECT_URL)

View File

@ -14,3 +14,8 @@ def html_response(url, request):
@all_requests
def metadata_response(url, request):
return response(200, content=file('tests/metadata.xml').read())
def reset_caplog(cap):
cap.handler.stream.truncate(0)
cap.handler.records = []