Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 55f631f

Browse files
authoredApr 29, 2020
Merge branch 'master' into metadata_eid_exception
2 parents c83efe2 + abfe310 commit 55f631f

24 files changed

+263
-253
lines changed
 

‎.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@
44
*.sqp
55
build/
66
dist/
7+
_build/
8+
.pytest_cache
9+
.env
10+
env/
11+
venv
12+
tags
13+
.idea/
14+
.vscode/

‎.hgignore

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

‎.travis.yml

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,35 @@
1+
dist: bionic
12
language: python
23

34
sudo: false
45

56
matrix:
67
include:
7-
- python: 2.7
8-
env: TOX_ENV=py27-django18
9-
- python: 3.4
10-
env: TOX_ENV=py34-django18
11-
- python: 2.7
12-
env: TOX_ENV=py27-django19
13-
- python: 3.4
14-
env: TOX_ENV=py34-django19
158
- python: 3.5
16-
env: TOX_ENV=py35-django19
17-
- python: 2.7
18-
env: TOX_ENV=py27-django110
19-
- python: 3.4
20-
env: TOX_ENV=py34-django110
21-
- python: 3.5
22-
env: TOX_ENV=py35-django110
23-
- python: 2.7
24-
env: TOX_ENV=py27-django111
25-
- python: 3.4
26-
env: TOX_ENV=py34-django111
27-
- python: 3.5
28-
env: TOX_ENV=py35-django111
9+
env: TOX_ENV=py35-django22
2910
- python: 3.6
30-
env: TOX_ENV=py36-django111
31-
- python: 3.5
32-
env: TOX_ENV=py35-djangomaster
11+
env: TOX_ENV=py36-django22
12+
- python: 3.7
13+
env: TOX_ENV=py37-django22
14+
- python: 3.8
15+
env: TOX_ENV=py38-django22
16+
- python: 3.6
17+
env: TOX_ENV=py36-django30
18+
- python: 3.7
19+
env: TOX_ENV=py37-django30
20+
- python: 3.8
21+
env: TOX_ENV=py38-django30
3322
- python: 3.6
3423
env: TOX_ENV=py36-djangomaster
24+
- python: 3.7
25+
env: TOX_ENV=py37-djangomaster
26+
- python: 3.8
27+
env: TOX_ENV=py38-djangomaster
3528
fast_finish: true
3629
allow_failures:
37-
- env: TOX_ENV=py35-djangomaster
3830
- env: TOX_ENV=py36-djangomaster
31+
- env: TOX_ENV=py37-djangomaster
32+
- env: TOX_ENV=py38-djangomaster
3933

4034
addons:
4135
apt:

‎CHANGES

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
Changes
22
=======
3+
0.18.1 (2020-02-15)
4+
----------
5+
- Fixed regression from 0.18.0. Thanks to OskarPersson
6+
7+
0.18.0 (2020-02-14)
8+
----------
9+
- Django 3.0 support. Thanks to OskarPersson
10+
- forceauthn and allowcreate support. Thanks to peppelinux
11+
- Dropped support for Python 3.4
12+
- Also thanks to WebSpider, mhindery, DylannCordel, habi3000 for various fixes and improvements
13+
14+
Thanks to plumdog
315

416
0.17.2 (2018-08-29)
517
----------

‎README.rst

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,13 @@ We will see a typical configuration for protecting a Django project::
192192
saml2.BINDING_HTTP_POST),
193193
],
194194
},
195-
195+
# Mandates that the identity provider MUST authenticate the
196+
# presenter directly rather than rely on a previous security context.
197+
'force_authn': False,
198+
199+
# Enable AllowCreate in NameIDPolicy.
200+
'name_id_format_allow_create': False,
201+
196202
# attributes that this project need to identify a user
197203
'required_attributes': ['uid'],
198204

@@ -309,6 +315,24 @@ setting::
309315
SAML_CONFIG_LOADER = 'python.path.to.your.callable'
310316

311317

318+
Custom error handler
319+
....................
320+
321+
When an error occurs during the authentication flow, djangosaml2 will render
322+
a simple error page with an error message and status code. You can customize
323+
this behaviour by specifying the path to your own error handler in the settings:
324+
325+
SAML_ACS_FAILURE_RESPONSE_FUNCTION = 'python.path.to.your.view'
326+
327+
This should be a view which takes a request, optional exception which occured
328+
and status code, and returns a response to serve the user. E.g. The default
329+
implementation looks like this::
330+
331+
def template_failure(request, exception=None, **kwargs):
332+
""" Renders a simple template with an error message. """
333+
return render(request, 'djangosaml2/login_error.html', {'exception': exception}, status=kwargs.get('status', 403))
334+
335+
312336
User attributes
313337
---------------
314338

‎djangosaml2/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
default_app_config = 'djangosaml2.apps.DjangoSaml2Config'

‎djangosaml2/acs_failures.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,10 @@
33
# This module defines a set of useful ACS failure functions that are used to
44
# produce an output suitable for end user in case of SAML failure.
55
#
6-
from __future__ import unicode_literals
76

8-
from django.core.exceptions import PermissionDenied
97
from django.shortcuts import render
108

119

12-
def template_failure(request, status=403, **kwargs):
13-
""" Renders a SAML-specific template with general authentication error description. """
14-
return render(request, 'djangosaml2/login_error.html', status=status)
15-
16-
17-
def exception_failure(request, exc_class=PermissionDenied, **kwargs):
18-
""" Rather than using a custom SAML specific template that is rendered on failure,
19-
this makes use of a standard exception handling machinery present in Django
20-
and thus ends up rendering a project-wide error page for Permission Denied exceptions.
21-
"""
22-
raise exc_class
10+
def template_failure(request, exception=None, status=403, **kwargs):
11+
""" Renders a simple template with an error message. """
12+
return render(request, 'djangosaml2/login_error.html', {'exception': exception}, status=status)

‎djangosaml2/apps.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.apps import AppConfig
2+
3+
4+
class DjangoSaml2Config(AppConfig):
5+
name = 'djangosaml2'
6+
verbose_name = "DjangoSAML2"
7+
8+
def ready(self):
9+
from . import signals # noqa

‎djangosaml2/backends.py

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,45 +18,28 @@
1818
from django.conf import settings
1919
from django.contrib import auth
2020
from django.contrib.auth.backends import ModelBackend
21-
from django.core.exceptions import (
22-
MultipleObjectsReturned, ImproperlyConfigured,
23-
)
24-
25-
from djangosaml2.signals import pre_user_save
21+
from django.core.exceptions import (ImproperlyConfigured,
22+
MultipleObjectsReturned)
2623

24+
from .signals import pre_user_save
2725

2826
logger = logging.getLogger('djangosaml2')
2927

3028

3129
def get_model(model_path):
30+
from django.apps import apps
3231
try:
33-
from django.apps import apps
3432
return apps.get_model(model_path)
35-
except ImportError:
36-
# Django < 1.7 (cannot use the new app loader)
37-
from django.db.models import get_model as django_get_model
38-
try:
39-
app_label, model_name = model_path.split('.')
40-
except ValueError:
41-
raise ImproperlyConfigured("SAML_USER_MODEL must be of the form "
42-
"'app_label.model_name'")
43-
user_model = django_get_model(app_label, model_name)
44-
if user_model is None:
45-
raise ImproperlyConfigured("SAML_USER_MODEL refers to model '%s' "
46-
"that has not been installed" % model_path)
47-
return user_model
33+
except LookupError:
34+
raise ImproperlyConfigured("SAML_USER_MODEL refers to model '%s' that has not been installed" % model_path)
35+
except ValueError:
36+
raise ImproperlyConfigured("SAML_USER_MODEL must be of the form 'app_label.model_name'")
4837

4938

5039
def get_saml_user_model():
51-
try:
52-
# djangosaml2 custom user model
40+
if hasattr(settings, 'SAML_USER_MODEL'):
5341
return get_model(settings.SAML_USER_MODEL)
54-
except AttributeError:
55-
try:
56-
# Django 1.5 Custom user model
57-
return auth.get_user_model()
58-
except AttributeError:
59-
return auth.models.User
42+
return auth.get_user_model()
6043

6144

6245
class Saml2Backend(ModelBackend):
@@ -89,7 +72,9 @@ def authenticate(self, request, session_info=None, attribute_mapping=None,
8972
else:
9073
logger.error('The nameid is not available. Cannot find user without a nameid.')
9174
else:
92-
saml_user = self.get_attribute_value(django_user_main_attribute, attributes, attribute_mapping)
75+
saml_user = self.get_attribute_value(django_user_main_attribute,
76+
attributes,
77+
attribute_mapping)
9378

9479
if saml_user is None:
9580
logger.error('Could not find saml_user value')
@@ -111,7 +96,11 @@ def get_attribute_value(self, django_field, attributes, attribute_mapping):
11196
logger.debug('attribute_mapping: %s', attribute_mapping)
11297
for saml_attr, django_fields in attribute_mapping.items():
11398
if django_field in django_fields and saml_attr in attributes:
114-
saml_user = attributes[saml_attr][0]
99+
saml_user = attributes.get(saml_attr, [None])[0]
100+
if not saml_user:
101+
logger.error('attributes[saml_attr] attribute '
102+
'value is missing. Probably the user '
103+
'session is expired.')
115104
return saml_user
116105

117106
def is_authorized(self, attributes, attribute_mapping):

‎djangosaml2/cache.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(self, django_session, key_suffix):
2525
self.session = django_session
2626
self.key = self.key_prefix + key_suffix
2727

28-
super(DjangoSessionCacheAdapter, self).__init__(self._get_objects())
28+
super().__init__(self._get_objects())
2929

3030
def _get_objects(self):
3131
return self.session.get(self.key, {})
@@ -37,9 +37,9 @@ def sync(self):
3737
# Changes in inner objects do not cause session invalidation
3838
# https://docs.djangoproject.com/en/1.9/topics/http/sessions/#when-sessions-are-saved
3939

40-
#add objects to session
40+
# add objects to session
4141
self._set_objects(dict(self))
42-
#invalidate session
42+
# invalidate session
4343
self.session.modified = True
4444

4545

@@ -49,8 +49,7 @@ class OutstandingQueriesCache(object):
4949
"""
5050

5151
def __init__(self, django_session):
52-
self._db = DjangoSessionCacheAdapter(django_session,
53-
'_outstanding_queries')
52+
self._db = DjangoSessionCacheAdapter(django_session, '_outstanding_queries')
5453

5554
def outstanding_queries(self):
5655
return self._db._get_objects()
@@ -86,4 +85,4 @@ class StateCache(DjangoSessionCacheAdapter):
8685
"""
8786

8887
def __init__(self, django_session):
89-
super(StateCache, self).__init__(django_session, '_state')
88+
super().__init__(django_session, '_state')

‎djangosaml2/conf.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@
1818

1919
from django.conf import settings
2020
from django.core.exceptions import ImproperlyConfigured
21-
2221
from saml2.config import SPConfig
2322

24-
from djangosaml2.utils import get_custom_setting
23+
from .utils import get_custom_setting
2524

2625

2726
def get_config_loader(path, request=None):

‎djangosaml2/exceptions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class IdPConfigurationMissing(Exception):
2+
pass

‎djangosaml2/models.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎djangosaml2/overrides.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ def do_logout(self, *args, **kwargs):
2121
except AttributeError:
2222
logger.warning('SAML_LOGOUT_REQUEST_PREFERRED_BINDING setting is'
2323
' not defined. Default binding will be used.')
24-
return super(Saml2Client, self).do_logout(*args, **kwargs)
24+
return super().do_logout(*args, **kwargs)

‎djangosaml2/signals.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,5 @@
1414

1515
import django.dispatch
1616

17-
18-
pre_user_save = django.dispatch.Signal(providing_args=['attributes',
19-
'user_modified'])
17+
pre_user_save = django.dispatch.Signal(providing_args=['attributes', 'user_modified'])
2018
post_authenticated = django.dispatch.Signal(providing_args=['session_info'])

‎djangosaml2/templates/djangosaml2/example_post_binding_form.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</p>
1010
<form method="post" action="{{ target_url }}" name="SSO_Login">
1111
{% for key, value in params.items %}
12-
<input type="hidden" name="{{ key|safe }}" value="{{ value|safe }}" />
12+
<input type="hidden" name="{{ key }}" value="{{ value }}" />
1313
{% endfor %}
1414
<input type="submit" value="Log in" />
1515
</form>

‎djangosaml2/tests/__init__.py

Lines changed: 23 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,44 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
import datetime
1817
import base64
18+
import datetime
1919
import re
2020
from unittest import skip
21-
import sys
2221

2322
from django.conf import settings
2423
from django.contrib.auth import SESSION_KEY, get_user_model
2524
from django.contrib.auth.models import AnonymousUser
2625
from django.contrib.sessions.middleware import SessionMiddleware
26+
from django.template import Context, Template
27+
from django.test import TestCase
28+
from django.test.client import RequestFactory
29+
from djangosaml2 import views
30+
from djangosaml2.cache import OutstandingQueriesCache
31+
from djangosaml2.conf import get_config
32+
from djangosaml2.signals import post_authenticated
33+
from djangosaml2.tests import conf
34+
from djangosaml2.tests.auth_response import auth_response
35+
from djangosaml2.views import finish_logout
36+
from saml2.config import SPConfig
37+
from saml2.s_utils import decode_base64_and_inflate, deflate_and_base64_encode
38+
2739
try:
2840
from django.urls import reverse
2941
except ImportError:
3042
from django.core.urlresolvers import reverse
31-
from django.template import Template, Context
32-
from django.test import TestCase
33-
from django.test.client import RequestFactory
3443
try:
3544
from django.utils.encoding import force_text
3645
except ImportError:
3746
from django.utils.text import force_text
38-
from django.utils.six.moves.urllib.parse import urlparse, parse_qs
39-
40-
from saml2.config import SPConfig
41-
from saml2.s_utils import decode_base64_and_inflate, deflate_and_base64_encode
47+
try:
48+
from django.utils.six.moves.urllib.parse import urlparse, parse_qs
49+
except ImportError:
50+
from urllib.parse import urlparse, parse_qs
4251

43-
from djangosaml2 import views
44-
from djangosaml2.cache import OutstandingQueriesCache
45-
from djangosaml2.conf import get_config
46-
from djangosaml2.tests import conf
47-
from djangosaml2.tests.auth_response import auth_response
48-
from djangosaml2.signals import post_authenticated
49-
from djangosaml2.views import finish_logout
5052

5153
User = get_user_model()
5254

53-
PY_VERSION = sys.version_info[:2]
54-
5555

5656
class SAML2Tests(TestCase):
5757

@@ -141,11 +141,7 @@ def test_login_one_idp(self):
141141
self.assertIn('RelayState', params)
142142

143143
saml_request = params['SAMLRequest'][0]
144-
if PY_VERSION < (3,):
145-
expected_request = """<?xml version='1.0' encoding='UTF-8'?>
146-
<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
147-
else:
148-
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
144+
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
149145

150146
self.assertSAMLRequestsEquals(
151147
decode_base64_and_inflate(saml_request).decode('utf-8'),
@@ -199,11 +195,7 @@ def test_login_several_idps(self):
199195
self.assertIn('RelayState', params)
200196

201197
saml_request = params['SAMLRequest'][0]
202-
if PY_VERSION < (3,):
203-
expected_request = """<?xml version='1.0' encoding='UTF-8'?>
204-
<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
205-
else:
206-
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
198+
expected_request = """<samlp:AuthnRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" AssertionConsumerServiceURL="http://sp.example.com/saml2/acs/" Destination="https://idp2.example.com/simplesaml/saml2/idp/SSOService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:NameIDPolicy AllowCreate="false" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" /></samlp:AuthnRequest>"""
207199

208200
self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'),
209201
expected_request)
@@ -339,11 +331,7 @@ def test_logout(self):
339331
self.assertIn('SAMLRequest', params)
340332

341333
saml_request = params['SAMLRequest'][0]
342-
if PY_VERSION < (3,):
343-
expected_request = """<?xml version='1.0' encoding='UTF-8'?>
344-
<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
345-
else:
346-
expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
334+
expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
347335
self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'),
348336
expected_request)
349337

@@ -369,11 +357,7 @@ def test_logout_service_local(self):
369357
self.assertIn('SAMLRequest', params)
370358

371359
saml_request = params['SAMLRequest'][0]
372-
if PY_VERSION < (3,):
373-
expected_request = """<?xml version='1.0' encoding='UTF-8'?>
374-
<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
375-
else:
376-
expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
360+
expected_request = """<samlp:LogoutRequest xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="XXXXXXXXXXXXXXXXXXXXXX" IssueInstant="2010-01-01T00:00:00Z" Reason="" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" SPNameQualifier="http://sp.example.com/saml2/metadata/">58bcc81ea14700f66aeb707a0eff1360</saml:NameID><samlp:SessionIndex>a0123456789abcdef0123456789abcdef</samlp:SessionIndex></samlp:LogoutRequest>"""
377361
self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_request).decode('utf-8'),
378362
expected_request)
379363

@@ -421,11 +405,7 @@ def test_logout_service_global(self):
421405
self.assertIn('SAMLResponse', params)
422406

423407
saml_response = params['SAMLResponse'][0]
424-
if PY_VERSION < (3,):
425-
expected_response = """<?xml version='1.0' encoding='UTF-8'?>
426-
<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" IssueInstant="2010-09-05T09:10:12Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>"""
427-
else:
428-
expected_response = """<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" IssueInstant="2010-09-05T09:10:12Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>"""
408+
expected_response = """<samlp:LogoutResponse xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php" ID="a140848e7ce2bce834d7264ecdde0151" InResponseTo="_9961abbaae6d06d251226cb25e38bf8f468036e57e" IssueInstant="2010-09-05T09:10:12Z" Version="2.0"><saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://sp.example.com/saml2/metadata/</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status></samlp:LogoutResponse>"""
429409
self.assertSAMLRequestsEquals(decode_base64_and_inflate(saml_response).decode('utf-8'),
430410
expected_response)
431411

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
X500ATTR_OID = 'urn:oid:2.5.4.'
2+
PKCS_9 = 'urn:oid:1.2.840.113549.1.9.1.'
3+
UCL_DIR_PILOT = 'urn:oid:0.9.2342.19200300.100.1.'
4+
5+
MAP = {
6+
'identifier': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
7+
'fro': {
8+
X500ATTR_OID+'3': 'first_name', # cn
9+
X500ATTR_OID+'4': 'last_name', # sn
10+
PKCS_9+'1': 'email',
11+
UCL_DIR_PILOT+'1': 'uid',
12+
},
13+
'to': {
14+
'first_name': X500ATTR_OID+'3',
15+
'last_name': X500ATTR_OID+'4',
16+
'email' : PKCS_9+'1',
17+
'uid': UCL_DIR_PILOT+'1',
18+
}
19+
}

‎djangosaml2/urls.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
from django.conf.urls import url
17-
from djangosaml2 import views
16+
from django.urls import path
1817

18+
from . import views
1919

2020
urlpatterns = [
21-
url(r'^login/$', views.login, name='saml2_login'),
22-
url(r'^acs/$', views.assertion_consumer_service, name='saml2_acs'),
23-
url(r'^logout/$', views.logout, name='saml2_logout'),
24-
url(r'^ls/$', views.logout_service, name='saml2_ls'),
25-
url(r'^ls/post/$', views.logout_service_post, name='saml2_ls_post'),
26-
url(r'^metadata/$', views.metadata, name='saml2_metadata'),
21+
path('login/', views.login, name='saml2_login'),
22+
path('acs/', views.assertion_consumer_service, name='saml2_acs'),
23+
path('logout/', views.logout, name='saml2_logout'),
24+
path('ls/', views.logout_service, name='saml2_ls'),
25+
path('ls/post/', views.logout_service_post, name='saml2_ls_post'),
26+
path('metadata/', views.metadata, name='saml2_metadata'),
2727
]

‎djangosaml2/utils.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import django
1615
from django.conf import settings
1716
from django.core.exceptions import ImproperlyConfigured
18-
from django.utils.http import is_safe_url
1917
from django.utils.module_loading import import_string
2018
from saml2.s_utils import UnknownSystemEntity
2119

@@ -33,23 +31,25 @@ def available_idps(config, langpref=None):
3331
for metadata_name, metadata in config.metadata.metadata.items():
3432
result = metadata.any('idpsso_descriptor', 'single_sign_on_service')
3533
if result:
36-
idps = idps.union(set(result.keys()))
34+
idps.update(result.keys())
3735

38-
return dict([(idp, config.metadata.name(idp, langpref)) for idp in idps])
36+
return {
37+
idp: config.metadata.name(idp, langpref)
38+
for idp in idps
39+
}
3940

4041

4142
def get_idp_sso_supported_bindings(idp_entity_id=None, config=None):
4243
"""Returns the list of bindings supported by an IDP
4344
This is not clear in the pysaml2 code, so wrapping it in a util"""
4445
if config is None:
4546
# avoid circular import
46-
from djangosaml2.conf import get_config
47+
from .conf import get_config
4748
config = get_config()
4849
# load metadata store from config
4950
meta = getattr(config, 'metadata', {})
5051
# if idp is None, assume only one exists so just use that
5152
if idp_entity_id is None:
52-
# .keys() returns dict_keys in python3.5+
5353
try:
5454
idp_entity_id = list(available_idps(config).keys())[0]
5555
except IndexError:
@@ -80,11 +80,3 @@ def fail_acs_response(request, *args, **kwargs):
8080
failure_function = import_string(get_custom_setting('SAML_ACS_FAILURE_RESPONSE_FUNCTION',
8181
'djangosaml2.acs_failures.template_failure'))
8282
return failure_function(request, *args, **kwargs)
83-
84-
85-
def is_safe_url_compat(url, allowed_hosts=None, require_https=False):
86-
if django.VERSION >= (1, 11):
87-
return is_safe_url(url, allowed_hosts=allowed_hosts, require_https=require_https)
88-
assert len(allowed_hosts) == 1
89-
host = allowed_hosts.pop()
90-
return is_safe_url(url, host=host)

‎djangosaml2/views.py

Lines changed: 87 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -19,44 +19,47 @@
1919
from django.conf import settings
2020
from django.contrib import auth
2121
from django.contrib.auth.decorators import login_required
22-
try:
23-
from django.contrib.auth.views import LogoutView
24-
django_logout = LogoutView.as_view()
25-
except ImportError:
26-
from django.contrib.auth.views import logout as django_logout
2722
from django.core.exceptions import PermissionDenied, SuspiciousOperation
28-
from django.http import Http404, HttpResponse
29-
from django.http import HttpResponseRedirect # 30x
3023
from django.http import HttpResponseBadRequest # 40x
24+
from django.http import HttpResponseRedirect # 30x
3125
from django.http import HttpResponseServerError # 50x
32-
from django.views.decorators.http import require_POST
26+
from django.http import Http404, HttpResponse
3327
from django.shortcuts import render
3428
from django.template import TemplateDoesNotExist
35-
from django.utils.six import text_type, binary_type, PY3
29+
from django.utils.http import is_safe_url
3630
from django.views.decorators.csrf import csrf_exempt
37-
3831
from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
32+
from saml2.client_base import LogoutError
3933
from saml2.metadata import entity_descriptor
4034
from saml2.ident import code, decode
41-
from saml2.sigver import MissingKey
35+
from saml2.metadata import entity_descriptor
36+
from saml2.response import (SignatureError, StatusAuthnFailed, StatusError,
37+
StatusNoAuthnContext, StatusRequestDenied,
38+
UnsolicitedResponse)
4239
from saml2.s_utils import UnsupportedBinding
4340
from saml2.response import (
4441
StatusError, StatusAuthnFailed, SignatureError, StatusRequestDenied,
4542
UnsolicitedResponse, StatusNoAuthnContext,
4643
)
4744
from saml2.mdstore import SourceNotFound
45+
from saml2.sigver import MissingKey
4846
from saml2.validate import ResponseLifetimeExceed, ToEarly
49-
from saml2.xmldsig import SIG_RSA_SHA1, SIG_RSA_SHA256 # support for SHA1 is required by spec
50-
51-
from djangosaml2.cache import IdentityCache, OutstandingQueriesCache
52-
from djangosaml2.cache import StateCache
53-
from djangosaml2.conf import get_config
54-
from djangosaml2.overrides import Saml2Client
55-
from djangosaml2.signals import post_authenticated
56-
from djangosaml2.utils import (
57-
available_idps, fail_acs_response, get_custom_setting,
58-
get_idp_sso_supported_bindings, get_location, is_safe_url_compat,
59-
)
47+
from saml2.xmldsig import ( # support for SHA1 is required by spec
48+
SIG_RSA_SHA1, SIG_RSA_SHA256)
49+
50+
from .cache import IdentityCache, OutstandingQueriesCache, StateCache
51+
from .conf import get_config
52+
from .exceptions import IdPConfigurationMissing
53+
from .overrides import Saml2Client
54+
from .signals import post_authenticated
55+
from .utils import (available_idps, fail_acs_response, get_custom_setting,
56+
get_idp_sso_supported_bindings, get_location)
57+
58+
try:
59+
from django.contrib.auth.views import LogoutView
60+
django_logout = LogoutView.as_view()
61+
except ImportError:
62+
from django.contrib.auth.views import logout as django_logout
6063

6164

6265
logger = logging.getLogger('djangosaml2')
@@ -73,16 +76,6 @@ def _get_subject_id(session):
7376
return None
7477

7578

76-
def callable_bool(value):
77-
""" A compatibility wrapper for pre Django 1.10 User model API that used
78-
is_authenticated() and is_anonymous() methods instead of attributes
79-
"""
80-
if callable(value):
81-
return value()
82-
else:
83-
return value
84-
85-
8679
def login(request,
8780
config_loader_path=None,
8881
wayf_template='djangosaml2/wayf.html',
@@ -111,7 +104,7 @@ def login(request,
111104
came_from = settings.LOGIN_REDIRECT_URL
112105

113106
# Ensure the user-originating redirection url is safe.
114-
if not is_safe_url_compat(url=came_from, allowed_hosts={request.get_host()}):
107+
if not is_safe_url(url=came_from, allowed_hosts={request.get_host()}):
115108
came_from = settings.LOGIN_REDIRECT_URL
116109

117110
# if the user is already authenticated that maybe because of two reasons:
@@ -124,7 +117,7 @@ def login(request,
124117
# SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN setting. If that setting
125118
# is True (default value) we will redirect him to the came_from view.
126119
# Otherwise, we will show an (configurable) authorization error.
127-
if callable_bool(request.user.is_authenticated):
120+
if request.user.is_authenticated:
128121
redirect_authenticated_user = getattr(settings, 'SAML_IGNORE_AUTHENTICATED_USERS_ON_LOGIN', True)
129122
if redirect_authenticated_user:
130123
return HttpResponseRedirect(came_from)
@@ -145,6 +138,13 @@ def login(request,
145138
'technical support.')),
146139
status=500)
147140

141+
kwargs = {}
142+
# pysaml needs a string otherwise: "cannot serialize True (type bool)"
143+
if getattr(conf, '_sp_force_authn', False):
144+
kwargs['force_authn'] = "true"
145+
if getattr(conf, '_sp_allow_create', False):
146+
kwargs['allow_create'] = "true"
147+
148148
# is a embedded wayf needed?
149149
idps = available_idps(conf)
150150
if selected_idp is None and len(idps) > 1:
@@ -153,7 +153,14 @@ def login(request,
153153
'available_idps': idps.items(),
154154
'came_from': came_from,
155155
})
156-
156+
else:
157+
# is the first one, otherwise next logger message will print None
158+
if not idps:
159+
raise IdPConfigurationMissing(('IdP configuration is missing or '
160+
'its metadata is expired.'))
161+
if selected_idp is None:
162+
selected_idp = list(idps.keys())[0]
163+
157164
# choose a binding to try first
158165
sign_requests = getattr(conf, '_sp_authn_requests_signed', False)
159166
binding = BINDING_HTTP_POST if sign_requests else BINDING_HTTP_REDIRECT
@@ -193,10 +200,10 @@ def login(request,
193200
session_id, result = client.prepare_for_authenticate(
194201
entityid=selected_idp, relay_state=came_from,
195202
binding=binding, sign=False, sigalg=sigalg,
196-
nsprefix=nsprefix)
203+
nsprefix=nsprefix, **kwargs)
197204
except TypeError as e:
198205
logger.error('Unable to know which IdP to use')
199-
return HttpResponse(text_type(e))
206+
return HttpResponse(str(e))
200207
else:
201208
http_response = HttpResponseRedirect(get_location(result))
202209
elif binding == BINDING_HTTP_POST:
@@ -206,15 +213,13 @@ def login(request,
206213
location = client.sso_location(selected_idp, binding)
207214
except TypeError as e:
208215
logger.error('Unable to know which IdP to use')
209-
return HttpResponse(text_type(e))
216+
return HttpResponse(str(e))
210217
session_id, request_xml = client.create_authn_request(
211218
location,
212-
binding=binding)
219+
binding=binding,
220+
**kwargs)
213221
try:
214-
if PY3:
215-
saml_request = base64.b64encode(binary_type(request_xml, 'UTF-8'))
216-
else:
217-
saml_request = base64.b64encode(binary_type(request_xml))
222+
saml_request = base64.b64encode(bytes(request_xml, 'UTF-8')).decode('utf-8')
218223

219224
http_response = render(request, post_binding_form_template, {
220225
'target_url': location,
@@ -234,7 +239,7 @@ def login(request,
234239
binding=binding)
235240
except TypeError as e:
236241
logger.error('Unable to know which IdP to use')
237-
return HttpResponse(text_type(e))
242+
return HttpResponse(str(e))
238243
else:
239244
http_response = HttpResponse(result['data'])
240245
else:
@@ -278,34 +283,34 @@ def assertion_consumer_service(request,
278283

279284
try:
280285
response = client.parse_authn_request_response(xmlstr, BINDING_HTTP_POST, outstanding_queries)
281-
except (StatusError, ToEarly):
286+
except (StatusError, ToEarly) as e:
282287
logger.exception("Error processing SAML Assertion.")
283-
return fail_acs_response(request)
284-
except ResponseLifetimeExceed:
288+
return fail_acs_response(request, exception=e)
289+
except ResponseLifetimeExceed as e:
285290
logger.info("SAML Assertion is no longer valid. Possibly caused by network delay or replay attack.", exc_info=True)
286-
return fail_acs_response(request)
287-
except SignatureError:
291+
return fail_acs_response(request, exception=e)
292+
except SignatureError as e:
288293
logger.info("Invalid or malformed SAML Assertion.", exc_info=True)
289-
return fail_acs_response(request)
290-
except StatusAuthnFailed:
294+
return fail_acs_response(request, exception=e)
295+
except StatusAuthnFailed as e:
291296
logger.info("Authentication denied for user by IdP.", exc_info=True)
292-
return fail_acs_response(request)
293-
except StatusRequestDenied:
297+
return fail_acs_response(request, exception=e)
298+
except StatusRequestDenied as e:
294299
logger.warning("Authentication interrupted at IdP.", exc_info=True)
295-
return fail_acs_response(request)
296-
except StatusNoAuthnContext:
300+
return fail_acs_response(request, exception=e)
301+
except StatusNoAuthnContext as e:
297302
logger.warning("Missing Authentication Context from IdP.", exc_info=True)
298-
return fail_acs_response(request)
299-
except MissingKey:
303+
return fail_acs_response(request, exception=e)
304+
except MissingKey as e:
300305
logger.exception("SAML Identity Provider is not configured correctly: certificate key is missing!")
301-
return fail_acs_response(request)
302-
except UnsolicitedResponse:
306+
return fail_acs_response(request, exception=e)
307+
except UnsolicitedResponse as e:
303308
logger.exception("Received SAMLResponse when no request has been made.")
304-
return fail_acs_response(request)
309+
return fail_acs_response(request, exception=e)
305310

306311
if response is None:
307312
logger.warning("Invalid SAML Assertion received (unknown error).")
308-
return fail_acs_response(request, status=400, exc_class=SuspiciousOperation)
313+
return fail_acs_response(request, status=400, exception=SuspiciousOperation('Unknown SAML2 error'))
309314

310315
session_id = response.session_id()
311316
oq_cache.delete(session_id)
@@ -325,7 +330,7 @@ def assertion_consumer_service(request,
325330
create_unknown_user=create_unknown_user)
326331
if user is None:
327332
logger.warning("Could not authenticate user received in SAML Assertion. Session info: %s", session_info)
328-
raise PermissionDenied
333+
return fail_acs_response(request, exception=PermissionDenied('No user could be authenticated.'))
329334

330335
auth.login(request, user)
331336
_set_subject_id(request.session, session_info['name_id'])
@@ -341,7 +346,7 @@ def assertion_consumer_service(request,
341346
if not relay_state:
342347
logger.warning('The RelayState parameter exists but is empty')
343348
relay_state = default_relay_state
344-
if not is_safe_url_compat(url=relay_state, allowed_hosts={request.get_host()}):
349+
if not is_safe_url(url=relay_state, allowed_hosts={request.get_host()}):
345350
relay_state = settings.LOGIN_REDIRECT_URL
346351
logger.debug('Redirecting to the RelayState: %s', relay_state)
347352
return HttpResponseRedirect(relay_state)
@@ -385,7 +390,13 @@ def logout(request, config_loader_path=None):
385390
'The session does not contain the subject id for user %s',
386391
request.user)
387392

388-
result = client.global_logout(subject_id)
393+
try:
394+
result = client.global_logout(subject_id)
395+
except LogoutError as exp:
396+
logger.exception('Error Handled - SLO not supported by IDP: {}'.format(exp))
397+
auth.logout(request)
398+
state.sync()
399+
return HttpResponseRedirect('/')
389400

390401
state.sync()
391402

@@ -467,7 +478,17 @@ def do_logout_service(request, data, binding, config_loader_path=None, next_page
467478
relay_state=data.get('RelayState', ''))
468479
state.sync()
469480
auth.logout(request)
470-
return HttpResponseRedirect(get_location(http_info))
481+
if (
482+
http_info.get('method', 'GET') == 'POST' and
483+
'data' in http_info and
484+
('Content-type', 'text/html') in http_info.get('headers', [])
485+
):
486+
# need to send back to the IDP a signed POST response with user session
487+
# return HTML form content to browser with auto form validation
488+
# to finally send request to the IDP
489+
return HttpResponse(http_info['data'])
490+
else:
491+
return HttpResponseRedirect(get_location(http_info))
471492
else:
472493
logger.error('No SAMLResponse or SAMLRequest parameter found')
473494
raise Http404('No SAMLResponse or SAMLRequest parameter found')
@@ -491,7 +512,7 @@ def metadata(request, config_loader_path=None, valid_for=None):
491512
"""
492513
conf = get_config(config_loader_path, request)
493514
metadata = entity_descriptor(conf)
494-
return HttpResponse(content=text_type(metadata).encode('utf-8'),
515+
return HttpResponse(content=str(metadata).encode('utf-8'),
495516
content_type="text/xml; charset=utf8")
496517

497518

‎setup.py

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,35 +23,26 @@ def read(*rnames):
2323
return codecs.open(os.path.join(os.path.dirname(__file__), *rnames), encoding='utf-8').read()
2424

2525

26-
extra = {'test': []}
27-
if sys.version_info < (3, 4):
28-
# Necessary to use assertLogs in tests
29-
extra['test'].append('unittest2')
30-
31-
3226
setup(
3327
name='djangosaml2',
34-
version='0.17.2',
28+
version='0.18.1',
3529
description='pysaml2 integration for Django',
36-
long_description='\n\n'.join([read('README.rst'), read('CHANGES')]),
30+
long_description=read('README.rst'),
3731
classifiers=[
38-
"Development Status :: 4 - Beta",
32+
"Development Status :: 5 - Production/Stable",
3933
"Environment :: Web Environment",
4034
"Framework :: Django",
41-
"Framework :: Django :: 1.8",
42-
"Framework :: Django :: 1.9",
43-
"Framework :: Django :: 1.10",
44-
"Framework :: Django :: 1.11",
35+
"Framework :: Django :: 2.2",
36+
"Framework :: Django :: 3.0",
4537
"Intended Audience :: Developers",
4638
"License :: OSI Approved :: Apache Software License",
4739
"Operating System :: OS Independent",
4840
"Programming Language :: Python",
49-
"Programming Language :: Python :: 2",
50-
"Programming Language :: Python :: 2.7",
5141
"Programming Language :: Python :: 3",
52-
"Programming Language :: Python :: 3.4",
5342
"Programming Language :: Python :: 3.5",
5443
"Programming Language :: Python :: 3.6",
44+
"Programming Language :: Python :: 3.7",
45+
"Programming Language :: Python :: 3.8",
5546
"Topic :: Internet :: WWW/HTTP",
5647
"Topic :: Internet :: WWW/HTTP :: WSGI",
5748
"Topic :: Security",
@@ -62,15 +53,14 @@ def read(*rnames):
6253
author_email="lgs@yaco.es",
6354
maintainer="Jozef Knaperek",
6455
url="https://github.com/knaperek/djangosaml2",
65-
download_url="https://pypi.python.org/pypi/djangosaml2",
56+
download_url="https://pypi.org/project/djangosaml2/",
6657
license='Apache 2.0',
6758
packages=find_packages(exclude=["tests", "tests.*"]),
6859
include_package_data=True,
6960
zip_safe=False,
7061
install_requires=[
7162
'defusedxml>=0.4.1',
72-
'Django>=1.8',
63+
'Django>=2.2',
7364
'pysaml2>=4.6.0',
7465
],
75-
extras_require=extra,
7666
)

‎tests/settings.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@
4646
'django.middleware.clickjacking.XFrameOptionsMiddleware',
4747
)
4848

49-
if django.VERSION < (1, 10):
50-
MIDDLEWARE_CLASSES = MIDDLEWARE
51-
5249
ROOT_URLCONF = 'testprofiles.urls'
5350

5451
TEMPLATES = [

‎tox.ini

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
[tox]
22
envlist =
3-
py{27,34,35}-django18
4-
py{27,34,35}-django19
5-
py{27,34,35}-django110
6-
py{27,34,35,36}-django111
7-
py{35,36}-djangomaster
3+
py{35,36,37,38}-django22
4+
py{36,37,38}-django30
5+
py{36,37,38}-djangomaster
86

97
[testenv]
108
commands =
119
python tests/run_tests.py
1210

1311
deps =
14-
django18: Django>=1.8,<1.9
15-
django19: Django>=1.9,<1.10
16-
django110: Django>=1.10,<1.11
17-
django111: Django>=1.11,<2.0
12+
django22: Django>=2.2,<3.0
13+
django30: Django>=3.0,<3.1
1814
djangomaster: https://github.com/django/django/archive/master.tar.gz
1915
.[test]
2016

0 commit comments

Comments
 (0)
Please sign in to comment.