Skip to content

Commit

Permalink
v1.0.0-rc
Browse files Browse the repository at this point in the history
  • Loading branch information
peppelinux committed Oct 15, 2020
1 parent c2563ab commit a382e9e
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 11 deletions.
13 changes: 13 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
Changes
=======

v1.0.0 (2020-10-15)
-------------------
- General refactor with Django ClassViews

0.50.0 (2020-10-15)
-------------------
- Discovery Service support

0.40.1 (2020-09-08)
-------------------
- [BugFix] HTTP-REDIRECT Authn Requests with optional signature now works.
- [BugFix] SameSite - SuspiciousOperation issue in middleware (Issue #220)

0.40.0 (2020-08-07)
-------------------
- 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):
Expand Down
37 changes: 37 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,27 @@ For example::
import saml2
SAML_LOGOUT_REQUEST_PREFERRED_BINDING = saml2.BINDING_HTTP_POST

Ignore Logout errors
--------------------
When logging out, a SAML IDP will return an error on invalid conditions, such as the IDP-side session being expired.
Use the following setting to ignore these errors and perform a local Django logout nonetheless::

SAML_IGNORE_LOGOUT_ERRORS = True

Signed Logout Request
------------------------
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::

"logout_requests_signed": True,

Discovery Service
-----------------
If you want to use a SAML Discovery Service, all you need is adding:

SAML2_DISCO_URL = 'https://your.ds.example.net/'

Of course, with the real URL of your preferred Discovery Service.


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


Sometimes you need to use special logic to update the user object
depending on the SAML2 attributes and the mapping described above
is simply not enough. For these cases djangosaml2 provides a Django
signal that you can listen to. In order to do so you can add the
following code to your app::

from djangosaml2.signals import pre_user_save

def custom_update_user(sender=User, instance, attributes, user_modified, **kargs)
...
return True # I modified the user object


Your handler will receive the user object, the list of SAML attributes
and a flag telling you if the user is already modified and need
to be saved after your handler is executed. If your handler
modifies the user object it should return True. Otherwise it should
return False. This way djangosaml2 will know if it should save
the user object so you don't need to do it and no more calls to
the save method are issued.


IdP setup
=========
Congratulations, you have finished configuring the SP side of the federation.
Expand Down
3 changes: 3 additions & 0 deletions djangosaml2/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
class DjangoSaml2Config(AppConfig):
name = 'djangosaml2'
verbose_name = "DjangoSAML2"

def ready(self):
from . import signals # noqa
2 changes: 2 additions & 0 deletions djangosaml2/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from django.core.exceptions import (ImproperlyConfigured,
MultipleObjectsReturned)

from .signals import pre_user_save

logger = logging.getLogger('djangosaml2')


Expand Down
10 changes: 5 additions & 5 deletions djangosaml2/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ def process_response(self, request, response):
patch_vary_headers(response, ('Cookie',))
# relies and the global one
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
if request.session.get_expire_at_browser_close():
if request.saml_session.get_expire_at_browser_close():
max_age = None
expires = None
else:
max_age = getattr(request, self.cookie_name).get_expiry_age()
max_age = request.saml_session.get_expiry_age()
expires_time = time.time() + max_age
expires = http_date(expires_time)
# Save the session data and refresh the client cookie.
Expand All @@ -67,8 +67,8 @@ def process_response(self, request, response):
max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE,
httponly=settings.SESSION_COOKIE_HTTPONLY,
samesite=settings.SESSION_COOKIE_SAMESITE
secure=settings.SESSION_COOKIE_SECURE or None,
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
samesite=None
)
return response
18 changes: 18 additions & 0 deletions djangosaml2/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import django.dispatch

pre_user_save = django.dispatch.Signal(providing_args=['attributes', 'user_modified'])
post_authenticated = django.dispatch.Signal(providing_args=['session_info', 'request'])
Empty file.
45 changes: 45 additions & 0 deletions djangosaml2/templatetags/idplist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright (C) 2011-2012 Yaco Sistemas (http://www.yaco.es)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from django import template

from djangosaml2.conf import config_settings_loader
from djangosaml2.utils import available_idps

register = template.Library()


class IdPListNode(template.Node):

def __init__(self, variable_name):
self.variable_name = variable_name

def render(self, context):
conf = config_settings_loader()
context[self.variable_name] = available_idps(conf)
return ''


@register.tag
def idplist(parser, token):
try:
tag_name, as_part, variable = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
'%r tag requires two arguments' % token.contents.split()[0])
if not as_part == 'as':
raise template.TemplateSyntaxError(
'%r tag first argument must be the literal "as"' % tag_name)

return IdPListNode(variable)
16 changes: 11 additions & 5 deletions djangosaml2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class LoginView(SPConfigMixin, View):
If set to None or nonexistent template, default form from the saml2 library
will be rendered.
"""
logger.debug('Login process started')

wayf_template = 'djangosaml2/wayf.html'
authorization_error_template = 'djangosaml2/auth_error.html'
Expand Down Expand Up @@ -477,11 +478,12 @@ def get(self, request, *args, **kwargs):
logger.debug('Returning form to the IdP to continue the logout process')
body = ''.join(http_info['data'])
return HttpResponse(body)
if binding == BINDING_HTTP_REDIRECT:
elif binding == BINDING_HTTP_REDIRECT:
logger.debug('Redirecting to the IdP to continue the logout process')
return HttpResponseRedirect(get_location(http_info))
logger.error('Unknown binding: %s', binding)
return HttpResponseServerError('Failed to log out')
else:
logger.error('Unknown binding: %s', binding)
return HttpResponseServerError('Failed to log out')
# We must have had a soap logout
return finish_logout(request, logout_info)

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

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

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


def finish_logout(request, response, next_page=None):
if response and response.status_ok():
if (getattr(settings, 'SAML_IGNORE_LOGOUT_ERRORS', False) or (response and response.status_ok())):
if next_page is None and hasattr(settings, 'LOGOUT_REDIRECT_URL'):
next_page = settings.LOGOUT_REDIRECT_URL
logger.debug('Performing django logout with a next_page of %s', next_page)
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
envlist =
py{3.6,3.7,3.8}-django{2.2,3.0,master}
py{3.6,3.7,3.8,3.9}-django{2.2,3.0,master}

[testenv]
commands =
Expand Down

0 comments on commit a382e9e

Please sign in to comment.