From 7ea562f2f600767171a6bbcd0189a2d1135a1206 Mon Sep 17 00:00:00 2001 From: Giuseppe De Marco Date: Fri, 4 Jun 2021 22:38:50 +0200 Subject: [PATCH] v1.3.0 (#291) * SAML2 Request improved * feat: sso_kwargs now handled with some custom methods ... that can be inherited :) * feat: authn context support, with or without this https://github.com/IdentityPython/pysaml2/pull/807 (better with!) * feat: authn context documentation * fix: Documentation for developers, unit tests * v1.3.0 --- djangosaml2/views.py | 57 ++++++++++++++++++++++++------ docs/source/contents/developer.rst | 17 +++++---- docs/source/contents/setup.rst | 19 +++++----- setup.py | 2 +- tests/settings.py | 2 +- 5 files changed, 68 insertions(+), 29 deletions(-) diff --git a/djangosaml2/views.py b/djangosaml2/views.py index 7ffd1149..feff71b6 100644 --- a/djangosaml2/views.py +++ b/djangosaml2/views.py @@ -43,6 +43,8 @@ UnsolicitedResponse) from saml2.s_utils import UnsupportedBinding from saml2.saml import SCM_BEARER +from saml2.saml import AuthnContextClassRef +from saml2.samlp import RequestedAuthnContext from saml2.samlp import AuthnRequest, IDPEntry, IDPList, Scoping from saml2.sigver import MissingKey from saml2.validate import ResponseLifetimeExceed, ToEarly @@ -133,6 +135,41 @@ def unknown_idp(self, request, idp): msg.format('Please contact technical support.'), status=403 ) + def load_sso_kwargs_scoping(self, sso_kwargs): + """ Performs IdP Scoping if scoping param is present. """ + idp_scoping_param = self.request.GET.get('scoping', None) + if idp_scoping_param: + idp_scoping = Scoping() + idp_scoping.idp_list = IDPList() + idp_scoping.idp_list.idp_entry.append( + IDPEntry(provider_id = idp_scoping_param) + ) + sso_kwargs['scoping'] = idp_scoping + + def load_sso_kwargs_authn_context(self, sso_kwargs): + # this would work when https://github.com/IdentityPython/pysaml2/pull/807 + ac = getattr(self.conf, '_sp_requested_authn_context', {}) + + # this works even without https://github.com/IdentityPython/pysaml2/pull/807 + # hopefully to be removed soon ! + if not ac: + scs = getattr( + settings, 'SAML_CONFIG', {} + ).get('service', {}).get('sp', {}) + ac = scs.get('requested_authn_context', {}) + # end transitional things to be removed soon ! + + if ac: + sso_kwargs["requested_authn_context"] = RequestedAuthnContext( + authn_context_class_ref=[ + AuthnContextClassRef(ac['authn_context_class_ref']), + ], + comparison = ac.get('comparison', "minimum"), + ) + + def load_sso_kwargs(self, sso_kwargs): + """ Inherit me if you want to put your desidered things in sso_kwargs """ + def get(self, request, *args, **kwargs): logger.debug('Login process started') next_path = self.get_next_path(request) @@ -166,6 +203,7 @@ def get(self, request, *args, **kwargs): configured_idps = available_idps(conf) selected_idp = request.GET.get('idp', None) + self.conf = conf sso_kwargs = {} # Do we have a Discovery Service? @@ -200,16 +238,6 @@ def get(self, request, *args, **kwargs): if selected_idp is None: selected_idp = list(configured_idps.keys())[0] - # perform IdP Scoping if scoping param is present - idp_scoping_param = request.GET.get('scoping', None) - if idp_scoping_param: - idp_scoping = Scoping() - idp_scoping.idp_list = IDPList() - idp_scoping.idp_list.idp_entry.append( - IDPEntry(provider_id = idp_scoping_param) - ) - sso_kwargs['scoping'] = idp_scoping - # choose a binding to try first binding = getattr(settings, 'SAML_DEFAULT_BINDING', saml2.BINDING_HTTP_POST) @@ -267,6 +295,15 @@ def get(self, request, *args, **kwargs): # custom nsprefixes sso_kwargs['nsprefix'] = get_namespace_prefixes() + + # Enrich sso_kwargs ... + # idp scoping + self.load_sso_kwargs_scoping(sso_kwargs) + # authn context + self.load_sso_kwargs_authn_context(sso_kwargs) + # other customization to be inherited + self.load_sso_kwargs(sso_kwargs) + logger.debug(f'Redirecting user to the IdP via {binding} binding.') _msg = 'Unable to know which IdP to use' http_response = None diff --git a/docs/source/contents/developer.rst b/docs/source/contents/developer.rst index 515d44c1..b447225f 100644 --- a/docs/source/contents/developer.rst +++ b/docs/source/contents/developer.rst @@ -21,24 +21,23 @@ a link to do a global logout. Unit tests ========== -You can also run the unit tests as follows:: +Djangosaml2 have a legacy way to do tests, using an example project in `tests` directory. +This means that to run tests you have to clone the repository, then install djangosaml2, then run tests using the example project. + +example:: pip install -r requirements-dev.txt # or pip install djangosaml2[test] - python3 tests/manage.py migrate - -then:: - python tests/run_tests.py -or:: - - cd tests/ +then:: + cd tests + ./manage.py migrate ./manage.py test djangosaml2 -If you have `tox`_ installed you can simply call tox inside the root directory +If you have `tox`_ installed you can simply call `tox` inside the root directory and it will run the tests in multiple versions of Python. .. _`tox`: http://pypi.python.org/pypi/tox diff --git a/docs/source/contents/setup.rst b/docs/source/contents/setup.rst index 7f2a301a..75acca80 100644 --- a/docs/source/contents/setup.rst +++ b/docs/source/contents/setup.rst @@ -48,14 +48,6 @@ installed apps:: 'djangosaml2', # new application ) -.. Note:: - - When you finish the configuration you can run the djangosaml2 test suite as - you run any other Django application test suite. Just type ``python manage.py - test djangosaml2``. - - Python users need to ``pip install djangosaml2[test]`` in order to run the - tests. SameSite cookie =============== @@ -221,6 +213,17 @@ This parameter can be combined with the IdP parameter if multiple IdPs are prese Currently there is support for a single IDPEntry in the IDPList. +Authn Context +============= + +We can define the authentication context in settings.SAML_CONFIG['service']['sp'] as follows:: + + 'requested_authn_context': { + 'authn_context_class_ref': saml2.saml.AUTHN_PASSWORD_PROTECTED, + 'comparison': "exact" + } + + Custom and dynamic configuration loading ======================================== diff --git a/setup.py b/setup.py index ef86ba4b..1eaad3e2 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def read(*rnames): setup( name='djangosaml2', - version='1.2.2', + version='1.3.0', description='pysaml2 integration for Django', long_description=read('README.md'), long_description_content_type='text/markdown', diff --git a/tests/settings.py b/tests/settings.py index aea014c9..ba8a7d83 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -75,7 +75,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'NAME': os.path.join(BASE_DIR, 'tests/db.sqlite3'), } }