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:
parent
686221fd65
commit
09ff054f57
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = []
|
||||
|
|
Loading…
Reference in New Issue