Skip to content

Commit a382e9e

Browse files
committed
v1.0.0-rc
1 parent c2563ab commit a382e9e

File tree

10 files changed

+135
-11
lines changed

10 files changed

+135
-11
lines changed

CHANGES

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
Changes
22
=======
33

4+
v1.0.0 (2020-10-15)
5+
-------------------
6+
- General refactor with Django ClassViews
7+
8+
0.50.0 (2020-10-15)
9+
-------------------
10+
- Discovery Service support
11+
12+
0.40.1 (2020-09-08)
13+
-------------------
14+
- [BugFix] HTTP-REDIRECT Authn Requests with optional signature now works.
15+
- [BugFix] SameSite - SuspiciousOperation issue in middleware (Issue #220)
16+
417
0.40.0 (2020-08-07)
518
-------------------
619
- Allow a SSO request without any attributes besides the NameID info. Backwards-incompatible changes to allow easier behaviour differentiation, two methods now receive the idp identifier (+ **kwargs were added to introduce possible similar changes in the future with less breaking effect):

README.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,27 @@ For example::
140140
import saml2
141141
SAML_LOGOUT_REQUEST_PREFERRED_BINDING = saml2.BINDING_HTTP_POST
142142

143+
Ignore Logout errors
144+
--------------------
145+
When logging out, a SAML IDP will return an error on invalid conditions, such as the IDP-side session being expired.
146+
Use the following setting to ignore these errors and perform a local Django logout nonetheless::
147+
148+
SAML_IGNORE_LOGOUT_ERRORS = True
149+
143150
Signed Logout Request
144151
------------------------
145152
Idp's like Okta require a signed logout response to validate and logout a user. Here's a sample config with all required SP/IDP settings::
146153

147154
"logout_requests_signed": True,
148155

156+
Discovery Service
157+
-----------------
158+
If you want to use a SAML Discovery Service, all you need is adding:
159+
160+
SAML2_DISCO_URL = 'https://your.ds.example.net/'
161+
162+
Of course, with the real URL of your preferred Discovery Service.
163+
149164

150165
Changes in the urls.py file
151166
---------------------------
@@ -493,6 +508,28 @@ Learn more about Django profile models at:
493508
https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model
494509

495510

511+
Sometimes you need to use special logic to update the user object
512+
depending on the SAML2 attributes and the mapping described above
513+
is simply not enough. For these cases djangosaml2 provides a Django
514+
signal that you can listen to. In order to do so you can add the
515+
following code to your app::
516+
517+
from djangosaml2.signals import pre_user_save
518+
519+
def custom_update_user(sender=User, instance, attributes, user_modified, **kargs)
520+
...
521+
return True # I modified the user object
522+
523+
524+
Your handler will receive the user object, the list of SAML attributes
525+
and a flag telling you if the user is already modified and need
526+
to be saved after your handler is executed. If your handler
527+
modifies the user object it should return True. Otherwise it should
528+
return False. This way djangosaml2 will know if it should save
529+
the user object so you don't need to do it and no more calls to
530+
the save method are issued.
531+
532+
496533
IdP setup
497534
=========
498535
Congratulations, you have finished configuring the SP side of the federation.

djangosaml2/apps.py

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

djangosaml2/backends.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
from django.core.exceptions import (ImproperlyConfigured,
2525
MultipleObjectsReturned)
2626

27+
from .signals import pre_user_save
28+
2729
logger = logging.getLogger('djangosaml2')
2830

2931

djangosaml2/middleware.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ def process_response(self, request, response):
4343
patch_vary_headers(response, ('Cookie',))
4444
# relies and the global one
4545
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
46-
if request.session.get_expire_at_browser_close():
46+
if request.saml_session.get_expire_at_browser_close():
4747
max_age = None
4848
expires = None
4949
else:
50-
max_age = getattr(request, self.cookie_name).get_expiry_age()
50+
max_age = request.saml_session.get_expiry_age()
5151
expires_time = time.time() + max_age
5252
expires = http_date(expires_time)
5353
# Save the session data and refresh the client cookie.
@@ -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,
71-
httponly=settings.SESSION_COOKIE_HTTPONLY,
72-
samesite=settings.SESSION_COOKIE_SAMESITE
70+
secure=settings.SESSION_COOKIE_SECURE or None,
71+
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
72+
samesite=None
7373
)
7474
return response

djangosaml2/signals.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import django.dispatch
16+
17+
pre_user_save = django.dispatch.Signal(providing_args=['attributes', 'user_modified'])
18+
post_authenticated = django.dispatch.Signal(providing_args=['session_info', 'request'])

djangosaml2/templatetags/__init__.py

Whitespace-only changes.

djangosaml2/templatetags/idplist.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from django import template
16+
17+
from djangosaml2.conf import config_settings_loader
18+
from djangosaml2.utils import available_idps
19+
20+
register = template.Library()
21+
22+
23+
class IdPListNode(template.Node):
24+
25+
def __init__(self, variable_name):
26+
self.variable_name = variable_name
27+
28+
def render(self, context):
29+
conf = config_settings_loader()
30+
context[self.variable_name] = available_idps(conf)
31+
return ''
32+
33+
34+
@register.tag
35+
def idplist(parser, token):
36+
try:
37+
tag_name, as_part, variable = token.split_contents()
38+
except ValueError:
39+
raise template.TemplateSyntaxError(
40+
'%r tag requires two arguments' % token.contents.split()[0])
41+
if not as_part == 'as':
42+
raise template.TemplateSyntaxError(
43+
'%r tag first argument must be the literal "as"' % tag_name)
44+
45+
return IdPListNode(variable)

djangosaml2/views.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class LoginView(SPConfigMixin, View):
9999
If set to None or nonexistent template, default form from the saml2 library
100100
will be rendered.
101101
"""
102+
logger.debug('Login process started')
102103

103104
wayf_template = 'djangosaml2/wayf.html'
104105
authorization_error_template = 'djangosaml2/auth_error.html'
@@ -477,11 +478,12 @@ def get(self, request, *args, **kwargs):
477478
logger.debug('Returning form to the IdP to continue the logout process')
478479
body = ''.join(http_info['data'])
479480
return HttpResponse(body)
480-
if binding == BINDING_HTTP_REDIRECT:
481+
elif binding == BINDING_HTTP_REDIRECT:
481482
logger.debug('Redirecting to the IdP to continue the logout process')
482483
return HttpResponseRedirect(get_location(http_info))
483-
logger.error('Unknown binding: %s', binding)
484-
return HttpResponseServerError('Failed to log out')
484+
else:
485+
logger.error('Unknown binding: %s', binding)
486+
return HttpResponseServerError('Failed to log out')
485487
# We must have had a soap logout
486488
return finish_logout(request, logout_info)
487489

@@ -516,7 +518,11 @@ def do_logout_service(self, request, data, binding):
516518

517519
if 'SAMLResponse' in data: # we started the logout
518520
logger.debug('Receiving a logout response from the IdP')
519-
response = client.parse_logout_request_response(data['SAMLResponse'], binding)
521+
try:
522+
response = client.parse_logout_request_response(data['SAMLResponse'], binding)
523+
except StatusError as e:
524+
response = None
525+
logger.warning("Error logging out from remote provider: " + str(e))
520526
state.sync()
521527
return finish_logout(request, response)
522528

@@ -553,7 +559,7 @@ def do_logout_service(self, request, data, binding):
553559

554560

555561
def finish_logout(request, response, next_page=None):
556-
if response and response.status_ok():
562+
if (getattr(settings, 'SAML_IGNORE_LOGOUT_ERRORS', False) or (response and response.status_ok())):
557563
if next_page is None and hasattr(settings, 'LOGOUT_REDIRECT_URL'):
558564
next_page = settings.LOGOUT_REDIRECT_URL
559565
logger.debug('Performing django logout with a next_page of %s', next_page)

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tox]
22
envlist =
3-
py{3.6,3.7,3.8}-django{2.2,3.0,master}
3+
py{3.6,3.7,3.8,3.9}-django{2.2,3.0,master}
44

55
[testenv]
66
commands =

0 commit comments

Comments
 (0)