Skip to content

Commit b8cbc22

Browse files
authored
RelayState, fixes and cleanup (#218)
1 parent e9ef3d5 commit b8cbc22

File tree

6 files changed

+103
-112
lines changed

6 files changed

+103
-112
lines changed

djangosaml2/middleware.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ def process_response(self, request, response):
6767
max_age=max_age,
6868
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
6969
path=settings.SESSION_COOKIE_PATH,
70-
secure=settings.SESSION_COOKIE_SECURE or None,
71-
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
72-
samesite=None
70+
secure=settings.SESSION_COOKIE_SECURE,
71+
httponly=settings.SESSION_COOKIE_HTTPONLY,
72+
samesite=settings.SESSION_COOKIE_SAMESITE
7373
)
7474
return response

djangosaml2/tests/__init__.py

Lines changed: 83 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,15 @@
1818
import re
1919
import sys
2020
from importlib import import_module
21-
from unittest import mock, skip
21+
from unittest import mock
2222
from urllib.parse import parse_qs, urlparse
2323

2424
from django.conf import settings
2525
from django.contrib.auth import SESSION_KEY, get_user_model
2626
from django.contrib.auth.models import AnonymousUser
2727
from django.core.exceptions import ImproperlyConfigured
28-
from django.http.request import HttpRequest
2928
from django.template import Context, Template
30-
from django.test import Client, TestCase
29+
from django.test import Client, TestCase, override_settings
3130
from django.test.client import RequestFactory
3231
from django.urls import reverse
3332
from django.utils.encoding import force_text
@@ -36,8 +35,7 @@
3635
from djangosaml2.conf import get_config
3736
from djangosaml2.middleware import SamlSessionMiddleware
3837
from djangosaml2.tests import conf
39-
from djangosaml2.utils import (get_custom_setting,
40-
get_idp_sso_supported_bindings,
38+
from djangosaml2.utils import (get_idp_sso_supported_bindings,
4139
get_session_id_from_saml2,
4240
get_subject_id_from_saml2,
4341
saml2_from_httpredirect_request)
@@ -81,35 +79,6 @@ class SAML2Tests(TestCase):
8179

8280
urls = 'djangosaml2.tests.urls'
8381

84-
def setUp(self):
85-
if hasattr(settings, 'SAML_ATTRIBUTE_MAPPING'):
86-
self.actual_attribute_mapping = settings.SAML_ATTRIBUTE_MAPPING
87-
del settings.SAML_ATTRIBUTE_MAPPING
88-
if hasattr(settings, 'SAML_CONFIG_LOADER'):
89-
self.actual_conf_loader = settings.SAML_CONFIG_LOADER
90-
del settings.SAML_CONFIG_LOADER
91-
92-
def tearDown(self):
93-
if hasattr(self, 'actual_attribute_mapping'):
94-
settings.SAML_ATTRIBUTE_MAPPING = self.actual_attribute_mapping
95-
if hasattr(self, 'actual_conf_loader'):
96-
settings.SAML_CONFIG_LOADER = self.actual_conf_loader
97-
98-
def assertSAMLRequestsEquals(self, real_xml, expected_xmls):
99-
100-
def remove_variable_attributes(xml_string):
101-
xml_string = re.sub(r' ID=".*?" ', ' ', xml_string)
102-
xml_string = re.sub(r' IssueInstant=".*?" ', ' ', xml_string)
103-
xml_string = re.sub(
104-
r'<saml:NameID(.*)>.*</saml:NameID>',
105-
r'<saml:NameID\1></saml:NameID>',
106-
xml_string)
107-
108-
return xml_string
109-
110-
self.assertEqual(remove_variable_attributes(real_xml),
111-
remove_variable_attributes(expected_xmls))
112-
11382
def init_cookies(self):
11483
self.client.cookies[settings.SESSION_COOKIE_NAME] = 'testing'
11584

@@ -120,7 +89,7 @@ def add_outstanding_query(self, session_id, came_from):
12089
self.saml_session.save()
12190
self.oq_cache = OutstandingQueriesCache(self.saml_session)
12291

123-
self.oq_cache.set(session_id \
92+
self.oq_cache.set(session_id
12493
if isinstance(session_id, str) else session_id.decode(),
12594
came_from)
12695
self.saml_session.save()
@@ -181,8 +150,7 @@ def test_unsigned_post_authn_request(self):
181150
saml_request = response_parser.saml_request_value
182151

183152
self.assertIsNotNone(saml_request)
184-
if 'AuthnRequest xmlns' not in base64.b64decode(saml_request).decode('utf-8'):
185-
raise Exception('test_unsigned_post_authn_request: Not a valid AuthnRequest')
153+
self.assertIn('AuthnRequest xmlns', base64.b64decode(saml_request).decode('utf-8'))
186154

187155
def test_login_evil_redirect(self):
188156
"""
@@ -241,8 +209,7 @@ def test_login_one_idp(self):
241209
self.assertIn('RelayState', params)
242210

243211
saml_request = params['SAMLRequest'][0]
244-
if 'AuthnRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
245-
raise Exception('Not a valid AuthnRequest')
212+
self.assertIn('AuthnRequest xmlns', decode_base64_and_inflate(saml_request).decode('utf-8'))
246213

247214
# if we set a next arg in the login view, it is preserverd
248215
# in the RelayState argument
@@ -292,8 +259,7 @@ def test_login_several_idps(self):
292259
self.assertIn('RelayState', params)
293260

294261
saml_request = params['SAMLRequest'][0]
295-
if 'AuthnRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
296-
raise Exception('Not a valid AuthnRequest')
262+
self.assertIn('AuthnRequest xmlns', decode_base64_and_inflate(saml_request).decode('utf-8'))
297263

298264
def test_assertion_consumer_service(self):
299265
# Get initial number of users
@@ -440,7 +406,6 @@ def do_login(self):
440406
self.assertEqual(response.status_code, 302)
441407
return subject_id
442408

443-
@skip("This is a known issue caused by pysaml2. Needs more investigation. Fixes are welcome.")
444409
def test_logout(self):
445410
settings.SAML_CONFIG = conf.create_conf(
446411
sp_host='sp.example.com',
@@ -466,8 +431,6 @@ def test_logout(self):
466431
if 'LogoutRequest xmlns' not in decode_base64_and_inflate(saml_request).decode('utf-8'):
467432
raise Exception('Not a valid LogoutRequest')
468433

469-
470-
471434
def test_logout_service_local(self):
472435
settings.SAML_CONFIG = conf.create_conf(
473436
sp_host='sp.example.com',
@@ -562,43 +525,6 @@ def test_finish_logout_renders_error_template(self):
562525
response = finish_logout(request, None)
563526
self.assertContains(response, "<h1>Logout error</h1>", status_code=200)
564527

565-
def _test_metadata(self):
566-
settings.SAML_CONFIG = conf.create_conf(
567-
sp_host='sp.example.com',
568-
idp_hosts=['idp.example.com'],
569-
metadata_file='remote_metadata_one_idp.xml',
570-
)
571-
valid_until = datetime.datetime.utcnow() + datetime.timedelta(hours=24)
572-
valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ")
573-
expected_metadata = """<?xml version='1.0' encoding='UTF-8'?>
574-
<md:EntityDescriptor entityID="http://sp.example.com/saml2/metadata/" validUntil="%s" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"><md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>MIIDPjCCAiYCCQCkHjPQlll+mzANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJF
575-
UzEQMA4GA1UECBMHU2V2aWxsYTEbMBkGA1UEChMSWWFjbyBTaXN0ZW1hcyBTLkwu
576-
MRAwDgYDVQQHEwdTZXZpbGxhMREwDwYDVQQDEwh0aWNvdGljbzAeFw0wOTEyMDQx
577-
OTQzNTJaFw0xMDEyMDQxOTQzNTJaMGExCzAJBgNVBAYTAkVTMRAwDgYDVQQIEwdT
578-
ZXZpbGxhMRswGQYDVQQKExJZYWNvIFNpc3RlbWFzIFMuTC4xEDAOBgNVBAcTB1Nl
579-
dmlsbGExETAPBgNVBAMTCHRpY290aWNvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
580-
MIIBCgKCAQEA7rMOMOaIZ/YYD5hYS6Hpjpovcu4k8gaIY+om9zCxLV5F8BLEfkxo
581-
Pk9IA3cRQNRxf7AXCFxEOH3nKy56AIi1gU7X6fCT30JBT8NQlYdgOVMLlR+tjy1b
582-
YV07tDa9U8gzjTyKQHgVwH0436+rmSPnacGj3fMwfySTMhtmrJmax0bIa8EB+gY1
583-
77DBtvf8dIZIXLlGMQFloZeUspvHOrgNoEA9xU4E9AanGnV9HeV37zv3mLDUOQLx
584-
4tk9sMQmylCpij7WZmcOV07DyJ/cEmnvHSalBTcyIgkcwlhmjtSgfCy6o5zuWxYd
585-
T9ia80SZbWzn8N6B0q+nq23+Oee9H0lvcwIDAQABMA0GCSqGSIb3DQEBBQUAA4IB
586-
AQCQBhKOqucJZAqGHx4ybDXNzpPethszonLNVg5deISSpWagy55KlGCi5laio/xq
587-
hHRx18eTzeCeLHQYvTQxw0IjZOezJ1X30DD9lEqPr6C+IrmZc6bn/pF76xsvdaRS
588-
gduNQPT1B25SV2HrEmbf8wafSlRARmBsyUHh860TqX7yFVjhYIAUF/El9rLca51j
589-
ljCIqqvT+klPdjQoZwODWPFHgute2oNRmoIcMjSnoy1+mxOC2Q/j7kcD8/etulg2
590-
XDxB3zD81gfdtT8VBFP+G4UrBa+5zFk6fT6U8a7ZqVsyH+rCXAdCyVlEC4Y5fZri
591-
ID4zT0FcZASGuthM56rRJJSx
592-
</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://sp.example.com/saml2/ls/" /><md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://sp.example.com/saml2/acs/" index="1" /><md:AttributeConsumingService index="1"><md:ServiceName xml:lang="en">Test SP</md:ServiceName><md:RequestedAttribute FriendlyName="uid" Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="true" /><md:RequestedAttribute FriendlyName="eduPersonAffiliation" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" isRequired="false" /></md:AttributeConsumingService></md:SPSSODescriptor><md:Organization><md:OrganizationName xml:lang="es">Ejemplo S.A.</md:OrganizationName><md:OrganizationName xml:lang="en">Example Inc.</md:OrganizationName><md:OrganizationDisplayName xml:lang="es">Ejemplo</md:OrganizationDisplayName><md:OrganizationDisplayName xml:lang="en">Example</md:OrganizationDisplayName><md:OrganizationURL xml:lang="es">http://www.example.es</md:OrganizationURL><md:OrganizationURL xml:lang="en">http://www.example.com</md:OrganizationURL></md:Organization><md:ContactPerson contactType="technical"><md:Company>Example Inc.</md:Company><md:GivenName>Technical givenname</md:GivenName><md:SurName>Technical surname</md:SurName><md:EmailAddress>[email protected]</md:EmailAddress></md:ContactPerson><md:ContactPerson contactType="administrative"><md:Company>Example Inc.</md:Company><md:GivenName>Administrative givenname</md:GivenName><md:SurName>Administrative surname</md:SurName><md:EmailAddress>[email protected]</md:EmailAddress></md:ContactPerson></md:EntityDescriptor>"""
593-
594-
expected_metadata = expected_metadata % valid_until
595-
596-
response = self.client.get(reverse('saml2_metadata'))
597-
self.assertEqual(response['Content-type'], 'text/xml; charset=utf8')
598-
self.assertEqual(response.status_code, 200)
599-
self.assertEqual(response.content, expected_metadata)
600-
601-
602528
def test_sigalg_not_passed_when_not_signing_request(self):
603529
# monkey patch SAML configuration
604530
settings.SAML_CONFIG = conf.create_conf(
@@ -690,3 +616,79 @@ def test_custom_conf_loader_from_view(self):
690616
url = urlparse(location)
691617
self.assertEqual(url.hostname, 'idp.example.com')
692618
self.assertEqual(url.path, '/simplesaml/saml2/idp/SSOService.php')
619+
620+
621+
class SessionEnabledTestCase(TestCase):
622+
def get_session(self):
623+
if self.client.session:
624+
session = self.client.session
625+
else:
626+
engine = import_module(settings.SESSION_ENGINE)
627+
session = engine.SessionStore()
628+
return session
629+
630+
def set_session_cookies(self, session):
631+
# Set the cookie to represent the session
632+
session_cookie = settings.SESSION_COOKIE_NAME
633+
self.client.cookies[session_cookie] = session.session_key
634+
cookie_data = {
635+
'max-age': None,
636+
'path': '/',
637+
'domain': settings.SESSION_COOKIE_DOMAIN,
638+
'secure': settings.SESSION_COOKIE_SECURE or None,
639+
'expires': None}
640+
self.client.cookies[session_cookie].update(cookie_data)
641+
642+
643+
class MiddlewareTests(SessionEnabledTestCase):
644+
def test_middleware_cookie_expireatbrowserclose(self):
645+
with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=True):
646+
session = self.get_session()
647+
session.save()
648+
self.set_session_cookies(session)
649+
650+
config_loader_path = 'djangosaml2.tests.test_config_loader_with_real_conf'
651+
request = RequestFactory().get('/login/')
652+
request.user = AnonymousUser()
653+
request.session = session
654+
middleware = SamlSessionMiddleware()
655+
middleware.process_request(request)
656+
657+
saml_session_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
658+
getattr(request, saml_session_name).save()
659+
660+
response = views.LoginView.as_view(config_loader_path=config_loader_path)(request)
661+
662+
response = middleware.process_response(request, response)
663+
664+
cookie = response.cookies[saml_session_name]
665+
666+
self.assertEqual(cookie['expires'], '')
667+
self.assertEqual(cookie['max-age'], '')
668+
669+
def test_middleware_cookie_with_expiry(self):
670+
with override_settings(SESSION_EXPIRE_AT_BROWSER_CLOSE=False):
671+
session = self.get_session()
672+
session.save()
673+
self.set_session_cookies(session)
674+
675+
config_loader_path = 'djangosaml2.tests.test_config_loader_with_real_conf'
676+
request = RequestFactory().get('/login/')
677+
request.user = AnonymousUser()
678+
request.session = session
679+
middleware = SamlSessionMiddleware()
680+
middleware.process_request(request)
681+
682+
saml_session_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
683+
getattr(request, saml_session_name).save()
684+
685+
response = views.LoginView.as_view(config_loader_path=config_loader_path)(request)
686+
687+
response = middleware.process_response(request, response)
688+
689+
cookie = response.cookies[saml_session_name]
690+
691+
self.assertIsNotNone(cookie['expires'])
692+
693+
self.assertNotEqual(cookie['expires'], '')
694+
self.assertNotEqual(cookie['max-age'], '')

djangosaml2/tests/urls.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

djangosaml2/views.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,13 @@ def get_next_path(self, request: HttpRequest) -> str:
108108
''' Returns the path to put in the RelayState to redirect the user to after having logged in.
109109
If the user is already logged in (and if allowed), he will redirect to there immediately.
110110
'''
111-
next_path = request.GET.get('next', settings.LOGIN_REDIRECT_URL) or settings.LOGIN_REDIRECT_URL
111+
112+
next_path = settings.LOGIN_REDIRECT_URL
113+
if 'next' in request.GET:
114+
next_path = request.GET['next']
115+
elif 'RelayState' in request.GET:
116+
next_path = request.GET['RelayState']
117+
112118
next_path = validate_referral_url(request, next_path)
113119
return next_path
114120

@@ -399,7 +405,7 @@ def get(self, request, *args, **kwargs):
399405
except AttributeError:
400406
return HttpResponse("No active SAML identity found. Are you sure you have logged in via SAML?")
401407

402-
return render(request, template, {'attributes': identity[0]})
408+
return render(request, 'djangosaml2/echo_attributes.html', {'attributes': identity[0]})
403409

404410

405411
class LogoutInitView(LoginRequiredMixin, SPConfigMixin, View):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def read(*rnames):
2424

2525
setup(
2626
name='djangosaml2',
27-
version='0.40.0',
27+
version='1.0.0',
2828
description='pysaml2 integration for Django',
2929
long_description=read('README.rst'),
3030
classifiers=[

tests/testprofiles/tests.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,6 @@ def test_clean_attributes(self):
379379
def test_clean_user_main_attribute(self):
380380
self.assertEqual(self.backend.clean_user_main_attribute('va--l__ u -e'), 'va__l___u__e')
381381

382-
383382
def test_authenticate(self):
384383
attribute_mapping = {
385384
'uid': ('username', ),
@@ -413,6 +412,14 @@ def test_authenticate(self):
413412
)
414413
self.assertIsNone(user)
415414

415+
with override_settings(SAML_USE_NAME_ID_AS_USERNAME=True):
416+
user = self.backend.authenticate(
417+
None,
418+
session_info={'ava': attributes, 'issuer': 'dummy_entity_id'},
419+
attribute_mapping=attribute_mapping,
420+
)
421+
self.assertIsNone(user)
422+
416423
attributes['is_staff'] = (False, )
417424
user = self.backend.authenticate(
418425
None,

0 commit comments

Comments
 (0)