From 055738ff05680769d8a7b4466b638258140f4c42 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 22 Apr 2020 04:14:28 +0200 Subject: [PATCH 4/4] tests: add tests on IdP intiated SLO (#41949) --- tests/test_sso_slo.py | 190 +++++++++++++++++++++++++++++++++++++++++- tests/test_utils.py | 8 +- tests/test_views.py | 6 +- 3 files changed, 195 insertions(+), 9 deletions(-) diff --git tests/test_sso_slo.py tests/test_sso_slo.py index 83a50cc..e81eccf 100644 --- tests/test_sso_slo.py +++ tests/test_sso_slo.py @@ -26,6 +26,8 @@ import lasso import pytest from pytest import fixture +from django.contrib.sessions.models import Session +from django.contrib.auth.models import User from django.urls import reverse from django.utils import six from django.utils.six.moves.urllib import parse as urlparse @@ -78,6 +80,9 @@ def sp_metadata(sp_settings, rf): class MockIdp(object): + session_dump = None + identity_dump = None + def __init__(self, idp_metadata, private_key, sp_metadata): self.server = server = lasso.Server.newFromBuffers(idp_metadata, private_key) self.server.signatureMethod = lasso.SIGNATURE_METHOD_RSA_SHA256 @@ -85,6 +90,10 @@ class MockIdp(object): def process_authn_request_redirect(self, url, auth_result=True, consent=True, msg=None): login = lasso.Login(self.server) + if self.identity_dump: + login.setIdentityFromDump(self.identity_dump) + if self.session_dump: + login.setSessionFromDump(self.session_dump) login.processAuthnRequestMsg(url.split('?', 1)[1]) # See # https://docs.python.org/2/library/zlib.html#zlib.decompress @@ -151,6 +160,14 @@ class MockIdp(object): raise NotImplementedError if login.msgBody: assert b'rsa-sha256' in base64.b64decode(login.msgBody) + if login.identity: + self.identity_dump = login.identity.dump() + else: + self.identity_dump = None + if login.session: + self.session_dump = login.session.dump() + else: + self.session_dump = None return login.msgUrl, login.msgBody, login.msgRelayState def resolve_artifact(self, soap_message): @@ -167,6 +184,27 @@ class MockIdp(object): assert 'rsa-sha256' in login.msgBody return '\n' + login.msgBody + def init_slo(self, full=False, method=lasso.HTTP_METHOD_REDIRECT, relay_state=None): + logout = lasso.Logout(self.server) + logout.setIdentityFromDump(self.identity_dump) + logout.setSessionFromDump(self.session_dump) + logout.initRequest(None, method) + logout.msgRelayState = relay_state + if full: + logout.request.sessionIndexes = () + logout.request.sessionIndex = None + logout.buildRequestMsg() + return logout.msgUrl, logout.msgBody, logout.msgRelayState + + def check_slo_return(self, url=None, body=None): + logout = lasso.Logout(self.server) + logout.setIdentityFromDump(self.identity_dump) + logout.setSessionFromDump(self.session_dump) + if body: + logout.processResponseMsg(force_str(body)) + else: + logout.processResponseMsg(force_str(url.split('?', 1)[-1])) + def mock_artifact_resolver(self): @all_requests def f(url, request): @@ -195,6 +233,156 @@ def test_sso_slo(db, app, idp, caplog, sp_settings): assert urlparse.urlparse(response['Location']).path == '/singleLogout' +def test_sso_idp_slo(db, app, idp, caplog, sp_settings): + assert Session.objects.count() == 0 + assert User.objects.count() == 0 + + # first session + response = app.get(reverse('mellon_login') + '?next=/whatever/') + url, body, relay_state = idp.process_authn_request_redirect(response['Location']) + assert relay_state + assert 'eo:next_url' not in str(idp.request) + assert url.endswith(reverse('mellon_login')) + response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) + assert 'created new user' in caplog.text + assert 'logged in using SAML' in caplog.text + assert urlparse.urlparse(response['Location']).path == '/whatever/' + + # second session + app.cookiejar.clear() + response = app.get(reverse('mellon_login') + '?next=/whatever/') + url, body, relay_state = idp.process_authn_request_redirect(response['Location']) + assert relay_state + assert 'eo:next_url' not in str(idp.request) + assert url.endswith(reverse('mellon_login')) + response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) + assert 'created new user' in caplog.text + assert 'logged in using SAML' in caplog.text + assert urlparse.urlparse(response['Location']).path == '/whatever/' + + assert Session.objects.count() == 2 + assert User.objects.count() == 1 + + # idp logout + url, body, relay_state = idp.init_slo() + response = app.get(url) + assert response.location.startswith('http://idp5/singleLogoutReturn?') + assert Session.objects.count() == 1 + idp.check_slo_return(response.location) + + +def test_sso_idp_slo_soap(db, app, idp, caplog, sp_settings): + assert Session.objects.count() == 0 + assert User.objects.count() == 0 + + # first session + response = app.get(reverse('mellon_login') + '?next=/whatever/') + url, body, relay_state = idp.process_authn_request_redirect(response['Location']) + assert relay_state + assert 'eo:next_url' not in str(idp.request) + assert url.endswith(reverse('mellon_login')) + response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) + assert 'created new user' in caplog.text + assert 'logged in using SAML' in caplog.text + assert urlparse.urlparse(response['Location']).path == '/whatever/' + + # second session + app.cookiejar.clear() + response = app.get(reverse('mellon_login') + '?next=/whatever/') + url, body, relay_state = idp.process_authn_request_redirect(response['Location']) + assert relay_state + assert 'eo:next_url' not in str(idp.request) + assert url.endswith(reverse('mellon_login')) + response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) + assert 'created new user' in caplog.text + assert 'logged in using SAML' in caplog.text + assert urlparse.urlparse(response['Location']).path == '/whatever/' + + assert Session.objects.count() == 2 + assert User.objects.count() == 1 + + # idp logout + url, body, relay_state = idp.init_slo(method=lasso.HTTP_METHOD_SOAP) + response = app.post(url, params=body, headers={'Content-Type': force_str('text/xml')}) + assert Session.objects.count() == 1 + idp.check_slo_return(body=response.content) + + +def test_sso_idp_slo_full(db, app, idp, caplog, sp_settings): + assert Session.objects.count() == 0 + assert User.objects.count() == 0 + + # first session + response = app.get(reverse('mellon_login') + '?next=/whatever/') + url, body, relay_state = idp.process_authn_request_redirect(response['Location']) + assert relay_state + assert 'eo:next_url' not in str(idp.request) + assert url.endswith(reverse('mellon_login')) + response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) + assert 'created new user' in caplog.text + assert 'logged in using SAML' in caplog.text + assert urlparse.urlparse(response['Location']).path == '/whatever/' + + # second session + app.cookiejar.clear() + response = app.get(reverse('mellon_login') + '?next=/whatever/') + url, body, relay_state = idp.process_authn_request_redirect(response['Location']) + assert relay_state + assert 'eo:next_url' not in str(idp.request) + assert url.endswith(reverse('mellon_login')) + response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) + assert 'created new user' in caplog.text + assert 'logged in using SAML' in caplog.text + assert urlparse.urlparse(response['Location']).path == '/whatever/' + + assert Session.objects.count() == 2 + assert User.objects.count() == 1 + + # idp logout + url, body, relay_state = idp.init_slo(full=True) + response = app.get(url) + assert response.location.startswith('http://idp5/singleLogoutReturn?') + assert Session.objects.count() == 0 + idp.check_slo_return(url=response.location) + + +def test_sso_idp_slo_full_soap(db, app, idp, caplog, sp_settings): + assert Session.objects.count() == 0 + assert User.objects.count() == 0 + + # first session + response = app.get(reverse('mellon_login') + '?next=/whatever/') + url, body, relay_state = idp.process_authn_request_redirect(response['Location']) + assert relay_state + assert 'eo:next_url' not in str(idp.request) + assert url.endswith(reverse('mellon_login')) + response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) + assert 'created new user' in caplog.text + assert 'logged in using SAML' in caplog.text + assert urlparse.urlparse(response['Location']).path == '/whatever/' + + # second session + app.cookiejar.clear() + response = app.get(reverse('mellon_login') + '?next=/whatever/') + url, body, relay_state = idp.process_authn_request_redirect(response['Location']) + assert relay_state + assert 'eo:next_url' not in str(idp.request) + assert url.endswith(reverse('mellon_login')) + response = app.post(reverse('mellon_login'), params={'SAMLResponse': body, 'RelayState': relay_state}) + assert 'created new user' in caplog.text + assert 'logged in using SAML' in caplog.text + assert urlparse.urlparse(response['Location']).path == '/whatever/' + + assert Session.objects.count() == 2 + assert User.objects.count() == 1 + + # idp logout + url, body, relay_state = idp.init_slo(method=lasso.HTTP_METHOD_SOAP, full=True) + response = app.post(url, params=body, headers={'Content-Type': force_str('text/xml')}) + assert Session.objects.count() == 0 + idp.check_slo_return(body=response.content) + + def test_sso(db, app, idp, caplog, sp_settings): response = app.get(reverse('mellon_login')) url, body, relay_state = idp.process_authn_request_redirect(response['Location']) @@ -330,7 +518,7 @@ def test_sso_artifact(db, app, caplog, sp_settings, idp_metadata, idp_private_ke acs_artifact_url = url.split('testserver', 1)[1] with HTTMock(idp.mock_artifact_resolver()): response = app.get(acs_artifact_url, params={'RelayState': relay_state}) - assert 'created new user' in caplog.text + assert 'created new user' not in caplog.text assert 'logged in using SAML' in caplog.text assert urlparse.urlparse(response['Location']).path == '/whatever/' diff --git tests/test_utils.py tests/test_utils.py index 832e7ae..db47fd7 100644 --- tests/test_utils.py +++ tests/test_utils.py @@ -42,9 +42,9 @@ def test_create_metadata(rf, private_settings, caplog): ('/sm:EntityDescriptor[@entityID="http://testserver/metadata/"]', 1, ('/*', 1), ('/sm:SPSSODescriptor', 1, - ('/*', 6), + ('/*', 7), ('/sm:NameIDFormat', 1), - ('/sm:SingleLogoutService', 1), + ('/sm:SingleLogoutService', 2), ('/sm:AssertionConsumerService[@isDefault=\'true\'][@Binding=\'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact\']', 1), ('/sm:AssertionConsumerService[@isDefault=\'true\'][@Binding=\'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\']', 0), @@ -64,11 +64,11 @@ def test_create_metadata(rf, private_settings, caplog): ('/sm:EntityDescriptor[@entityID="http://testserver/metadata/"]', 1, ('/*', 1), ('/sm:SPSSODescriptor', 1, - ('/*', 7), + ('/*', 8), ('/sm:Extensions', 1, ('/idpdisc:DiscoveryResponse', 1)), ('/sm:NameIDFormat', 1), - ('/sm:SingleLogoutService', 1), + ('/sm:SingleLogoutService', 2), ('/sm:AssertionConsumerService[@isDefault=\'true\'][@Binding=\'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact\']', 1), ('/sm:AssertionConsumerService[@isDefault=\'true\'][@Binding=\'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\']', 0), diff --git tests/test_views.py tests/test_views.py index 7964bd9..79a27c8 100644 --- tests/test_views.py +++ tests/test_views.py @@ -20,11 +20,9 @@ import mock import lasso from django.utils.six.moves.urllib.parse import parse_qs, urlparse import base64 -import random import hashlib from httmock import HTTMock -import django from django.urls import reverse from django.utils.encoding import force_text from django.utils.http import urlencode @@ -109,9 +107,9 @@ def test_metadata(private_settings, client): ('/sm:EntityDescriptor[@entityID="http://testserver/metadata/"]', 1, ('/*', 4), ('/sm:SPSSODescriptor', 1, - ('/*', 6), + ('/*', 7), ('/sm:NameIDFormat', 1), - ('/sm:SingleLogoutService', 1), + ('/sm:SingleLogoutService', 2), ('/sm:AssertionConsumerService', None, ('[@isDefault="true"]', None, ('[@Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"]', 1), -- 2.26.0