This commit is contained in:
Benjamin Dauvergne 2016-04-06 01:04:34 +02:00
parent 1ad8264621
commit 7db1d7d7ed
3 changed files with 55 additions and 58 deletions

View File

@ -4,14 +4,13 @@ import django
from . import views
urlpatterns = [
url('login/$', views.login,
name='mellon_login'),
url('logout/$', views.logout,
name='mellon_logout'),
url('metadata/$', views.metadata,
name='mellon_metadata')
url('login/$', views.login,
name='mellon_login'),
url('logout/$', views.logout,
name='mellon_logout'),
url('metadata/$', views.metadata,
name='mellon_metadata')
]
if django.VERSION < (1,9):
if django.VERSION < (1, 9):
urlpatterns = patterns('', *urlpatterns)

View File

@ -98,7 +98,7 @@ class LoginView(ProfileMixin, LogMixin, View):
login.acceptSso()
except lasso.ProfileCannotVerifySignatureError:
self.log.warning('SAML authentication failed: signature validation failed for %r',
login.remoteProviderId)
login.remoteProviderId)
except lasso.ParamError:
self.log.exception('lasso param error')
except (lasso.LoginStatusNotSuccessError,
@ -116,8 +116,7 @@ class LoginView(ProfileMixin, LogMixin, View):
args.append(status.statusMessage)
self.log.warning(*args)
except lasso.Error, e:
return HttpResponseBadRequest('error processing the authentication '
'response: %r' % e)
return HttpResponseBadRequest('error processing the authentication response: %r' % e)
else:
if 'RelayState' in request.POST and utils.is_nonnull(request.POST['RelayState']):
login.msgRelayState = request.POST['RelayState']
@ -132,16 +131,17 @@ class LoginView(ProfileMixin, LogMixin, View):
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,
})
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 sso_success(self, request, login):
attributes = {}
@ -156,9 +156,11 @@ class LoginView(ProfileMixin, LogMixin, View):
attributes['issuer'] = login.remoteProviderId
if login.nameIdentifier:
name_id = login.nameIdentifier
name_id_format = unicode(name_id.format
or lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED)
attributes.update({
'name_id_content': name_id.content.decode('utf8'),
'name_id_format': unicode(name_id.format or lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED),
'name_id_format': name_id_format
})
if name_id.nameQualifier:
attributes['name_id_name_qualifier'] = unicode(name_id.nameQualifier)
@ -168,7 +170,8 @@ class LoginView(ProfileMixin, LogMixin, View):
if authn_statement.authnInstant:
attributes['authn_instant'] = utils.iso8601_to_datetime(authn_statement.authnInstant)
if authn_statement.sessionNotOnOrAfter:
attributes['session_not_on_or_after'] = utils.iso8601_to_datetime(authn_statement.sessionNotOnOrAfter)
attributes['session_not_on_or_after'] = utils.iso8601_to_datetime(
authn_statement.sessionNotOnOrAfter)
if authn_statement.sessionIndex:
attributes['session_index'] = authn_statement.sessionIndex
attributes['authn_context_class_ref'] = ()
@ -186,19 +189,21 @@ class LoginView(ProfileMixin, LogMixin, View):
if user is not None:
if user.is_active:
auth.login(request, user)
self.log.info('user %r (NameID is %r) logged in using SAML',
unicode(user), attributes['name_id_content'])
self.log.info('user %r (NameID is %r) logged in using SAML', unicode(user),
attributes['name_id_content'])
request.session['mellon_session'] = utils.flatten_datetime(attributes)
if ('session_not_on_or_after' in attributes and
not settings.SESSION_EXPIRE_AT_BROWSER_CLOSE):
request.session.set_expiry(utils.get_seconds_expiry(attributes['session_not_on_or_after']))
not settings.SESSION_EXPIRE_AT_BROWSER_CLOSE):
request.session.set_expiry(
utils.get_seconds_expiry(
attributes['session_not_on_or_after']))
else:
return render(request, 'mellon/inactive_user.html', {
'user': user,
'saml_attributes': attributes})
else:
return render(request, 'mellon/user_not_found.html', {
'saml_attributes': attributes })
return render(request, 'mellon/user_not_found.html',
{'saml_attributes': attributes})
request.session['lasso_session_dump'] = login.session.dump()
return HttpResponseRedirect(next_url)
@ -229,14 +234,14 @@ class LoginView(ProfileMixin, LogMixin, View):
login.buildRequestMsg()
try:
result = requests.post(login.msgUrl, data=login.msgBody,
headers={'content-type': 'text/xml'},
verify=verify_ssl_certificate)
headers={'content-type': 'text/xml'},
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))
if result.status_code != 200:
self.log.warning('SAML authentication failed: '\
'IdP returned %s when given artifact', result.status_code)
self.log.warning('SAML authentication failed: IdP returned %s when given artifact',
result.status_code)
return self.sso_failure(request, login, idp_message, status_codes)
try:
@ -251,7 +256,7 @@ class LoginView(ProfileMixin, LogMixin, View):
return HttpResponseBadRequest('ArtififactResolveResponse is malformed')
except lasso.ProfileCannotVerifySignatureError:
self.log.warning('SAML authentication failed: signature validation failed for %r',
login.remoteProviderId)
login.remoteProviderId)
except lasso.ParamError:
self.log.exception('lasso param error')
except (lasso.ProfileStatusNotSuccessError, lasso.ProfileRequestDeniedError):
@ -268,8 +273,7 @@ class LoginView(ProfileMixin, LogMixin, View):
self.log.warning(*args)
except lasso.Error, e:
self.log.exception('unexpected lasso error')
return HttpResponseBadRequest('error processing the authentication '
'response: %r' % e)
return HttpResponseBadRequest('error processing the authentication response: %r' % e)
else:
if 'RelayState' in request.GET and utils.is_nonnull(request.GET['RelayState']):
login.msgRelayState = request.GET['RelayState']
@ -296,8 +300,8 @@ class LoginView(ProfileMixin, LogMixin, View):
# redirect to discovery service if needed
if (not 'entityID' in request.GET
and not 'nodisco' in request.GET
and app_settings.DISCOVERY_SERVICE_URL):
and not 'nodisco' in request.GET
and app_settings.DISCOVERY_SERVICE_URL):
return self.request_discovery_service(
request, is_passive=request.GET.get('passive') == '1')
@ -308,8 +312,7 @@ class LoginView(ProfileMixin, LogMixin, View):
self.profile = login = utils.create_login(request)
self.log.debug('authenticating to %r', idp['ENTITY_ID'])
try:
login.initAuthnRequest(idp['ENTITY_ID'],
lasso.HTTP_METHOD_REDIRECT)
login.initAuthnRequest(idp['ENTITY_ID'], lasso.HTTP_METHOD_REDIRECT)
authn_request = login.request
# configure NameID policy
policy = authn_request.nameIdPolicy
@ -329,14 +332,14 @@ class LoginView(ProfileMixin, LogMixin, View):
self.set_next_url(next_url)
login.buildAuthnRequestMsg()
except lasso.Error, e:
return HttpResponseBadRequest('error initializing the '
'authentication request: %r' % e)
return HttpResponseBadRequest('error initializing the authentication request: %r' % e)
self.log.debug('sending authn request %r', authn_request.dump())
self.log.debug('to url %r', login.msgUrl)
return HttpResponseRedirect(login.msgUrl)
login = csrf_exempt(LoginView.as_view())
class LogoutView(ProfileMixin, LogMixin, View):
def get(self, request):
if 'SAMLRequest' in request.GET:
@ -359,8 +362,7 @@ class LogoutView(ProfileMixin, LogMixin, View):
self.log.warning('error validating logout request: %r' % e)
issuer = request.session.get('mellon_session', {}).get('issuer')
if issuer == logout.remoteProviderId:
self.log.info('user %r logged out by SLO request',
unicode(request.user))
self.log.info(u'user logged out by IdP SLO request')
auth.logout(request)
try:
logout.buildResponseMsg()
@ -380,10 +382,8 @@ class LogoutView(ProfileMixin, LogMixin, View):
if issuer:
self.profile = logout = utils.create_logout(request)
try:
if request.session.has_key('lasso_session_dump'):
logout.setSessionFromDump(
request.session['lasso_session_dump']
)
if 'lasso_session_dump' in request.session:
logout.setSessionFromDump(request.session['lasso_session_dump'])
else:
self.log.error('unable to find lasso session dump')
logout.initRequest(issuer, lasso.HTTP_METHOD_REDIRECT)
@ -399,11 +399,9 @@ class LogoutView(ProfileMixin, LogMixin, View):
# set next_url after local logout, as the session is wiped by auth.logout
if logout:
self.set_next_url(next_url)
self.log.info('user %r logged out, SLO request sent',
unicode(request.user))
self.log.info(u'user logged out, SLO request sent to IdP')
else:
self.log.warning('logout refused referer %r is not of the '
'same origin', referer)
self.log.warning('logout refused referer %r is not of the same origin', referer)
return HttpResponseRedirect(next_url)
def sp_logout_response(self, request):

View File

@ -174,7 +174,7 @@ def test_sp_initiated_login(private_settings, client):
assert set(params.keys()) == set(['SAMLRequest', 'RelayState'])
assert len(params['SAMLRequest']) == 1
assert base64.b64decode(params['SAMLRequest'][0])
assert params['RelayState'] == ['/whatever']
assert client.session['mellon_next_url_%s' % params['RelayState'][0]] == '/whatever'
def test_sp_initiated_login_chosen(private_settings, client):
@ -192,7 +192,7 @@ def test_sp_initiated_login_chosen(private_settings, client):
assert set(params.keys()) == set(['SAMLRequest', 'RelayState'])
assert len(params['SAMLRequest']) == 1
assert base64.b64decode(params['SAMLRequest'][0])
assert params['RelayState'] == ['/whatever']
assert client.session['mellon_next_url_%s' % params['RelayState'][0]] == '/whatever'
def test_sp_initiated_login_requested_authn_context(private_settings, client):
@ -211,7 +211,7 @@ def test_sp_initiated_login_requested_authn_context(private_settings, client):
request = lasso.Samlp2AuthnRequest()
assert request.initFromQuery(urlparse(response['Location']).query)
assert request.requestedAuthnContext.authnContextClassRef == (
'urn:be:fedict:iam:fas:citizen:eid', 'urn:be:fedict:iam:fas:citizen:token')
'urn:be:fedict:iam:fas:citizen:eid', 'urn:be:fedict:iam:fas:citizen:token')
def test_malfortmed_artifact(private_settings, client, caplog):
@ -227,7 +227,7 @@ def test_malfortmed_artifact(private_settings, client, caplog):
def artifact():
entity_id = 'https://cresson.entrouvert.org/idp/saml2/metadata'
token = 'x' * 20
return base64.b64encode('\x00\x04\x00\x00' + hashlib.sha1(entity_id).digest() + token)
return base64.b64encode('\x00\x04\x00\x00' + hashlib.sha1(entity_id).digest() + token)
def test_error_500_on_artifact_resolve(private_settings, client, caplog, artifact):
@ -235,7 +235,7 @@ def test_error_500_on_artifact_resolve(private_settings, client, caplog, artifac
'METADATA': open('tests/metadata.xml').read(),
}]
with HTTMock(error_500):
response = client.get('/login/?SAMLart=%s' % artifact)
client.get('/login/?SAMLart=%s' % artifact)
assert 'IdP returned 500' in caplog.text()
@ -244,5 +244,5 @@ def test_invalid_msg_on_artifact_resolve(private_settings, client, caplog, artif
'METADATA': open('tests/metadata.xml').read(),
}]
with HTTMock(html_response):
response = client.get('/login/?SAMLart=%s' % artifact)
client.get('/login/?SAMLart=%s' % artifact)
assert 'ArtifactResolveResponse is malformed' in caplog.text()