Skip to content

Commit

Permalink
SAML session refactor and minor changes in README file
Browse files Browse the repository at this point in the history
  • Loading branch information
peppelinux committed Jul 30, 2020
1 parent 9cfe3a5 commit 4c6e98f
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 44 deletions.
27 changes: 17 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ any data model. The only reason we include it is to be able to run
djangosaml2 test suite from our project, something you should always
do to make sure it is compatible with your Django version and environment.

.. note::
.. 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
Expand Down Expand Up @@ -96,7 +96,7 @@ been authenticated before. We are also telling that when the user closes
his browser, the session should be terminated. This is useful in SAML2
federations where the logout protocol is not always available.

.. note::
.. Note::

The login url starts with ``/saml2/`` as an example but you can change that
if you want. Check the section about changes in the ``urls.py``
Expand Down Expand Up @@ -240,22 +240,28 @@ We will see a typical configuration for protecting a Django project::
},
},

# where the remote metadata is stored
# where the remote metadata is stored, local, remote or mdq server.
# One metadatastore or many ...
'metadata': {
'local': [path.join(BASEDIR, 'remote_metadata.xml')],
'remote': [{"url": "https://idp.testunical.it/idp/shibboleth",
"disable_ssl_certificate_validation": True},],
'mdq': [{"url": "https://ds.testunical.it",
"cert": "certficates/others/ds.testunical.it.cert",
"disable_ssl_certificate_validation": True}]
},

# set to 1 to output debugging information
'debug': 1,

# Signing
'key_file': path.join(BASEDIR, 'mycert.key'), # private part
'cert_file': path.join(BASEDIR, 'mycert.pem'), # public part
'key_file': path.join(BASEDIR, 'private.key'), # private part
'cert_file': path.join(BASEDIR, 'public.pem'), # public part

# Encryption
'encryption_keypairs': [{
'key_file': path.join(BASEDIR, 'my_encryption_key.key'), # private part
'cert_file': path.join(BASEDIR, 'my_encryption_cert.pem'), # public part
'key_file': path.join(BASEDIR, 'private.key'), # private part
'cert_file': path.join(BASEDIR, 'public.pem'), # public part
}],

# own metadata settings
Expand All @@ -277,7 +283,6 @@ We will see a typical configuration for protecting a Django project::
'display_name': [('Yaco', 'es'), ('Yaco', 'en')],
'url': [('http://www.yaco.es', 'es'), ('http://www.yaco.com', 'en')],
},
'valid_for': 24, # how long is our metadata valid
}

.. note::
Expand Down Expand Up @@ -309,11 +314,13 @@ standard x509 certificate. You need it to sign your metadata. For assertion
encryption/decryption support please configure another set of ``key_file`` and
``cert_file``, but as inner attributes of ``encryption_keypairs`` option.

.. note::
.. Note::

Check your openssl documentation to generate a test certificate but don't
forget to order a real one when you go into production.

..
openssl req -nodes -new -x509 -days 3650 -keyout private.key -out public.cert
Custom and dynamic configuration loading
........................................
Expand All @@ -336,7 +343,7 @@ SameSite cookie

By default, djangosaml2 handle the saml2 session in a separate cookie.
The storage linked to it is accessible by default at `request.saml_session`.
You can even configure this using::
You can even configure the SAML cookie name as follows::

SAML_SESSION_COOKIE_NAME = 'saml_session'

Expand Down
25 changes: 13 additions & 12 deletions djangosaml2/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@


class SamlSessionMiddleware(SessionMiddleware):
session_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
cookie_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')

def process_request(self, request):
session_key = request.COOKIES.get(self.session_name, None)
setattr(request, self.session_name, self.SessionStore(session_key))
session_key = request.COOKIES.get(self.cookie_name, None)
request.saml_session = self.SessionStore(session_key)

def process_response(self, request, response):
"""
Expand All @@ -23,16 +23,16 @@ def process_response(self, request, response):
the session cookie if the session has been emptied.
"""
try:
accessed = getattr(request, self.session_name).accessed
modified = getattr(request, self.session_name).modified
empty = getattr(request, self.session_name).is_empty()
accessed = request.saml_session.accessed
modified = request.saml_session.modified
empty = request.saml_session.is_empty()
except AttributeError:
return response
# First check if we need to delete this cookie.
# The session should be deleted only if the session is entirely empty.
if self.session_name in request.COOKIES and empty:
if self.cookie_name in request.COOKIES and empty:
response.delete_cookie(
self.session_name,
self.cookie_name,
path=settings.SESSION_COOKIE_PATH,
domain=settings.SESSION_COOKIE_DOMAIN,
samesite=None,
Expand All @@ -41,28 +41,29 @@ def process_response(self, request, response):
else:
if accessed:
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():
max_age = None
expires = None
else:
max_age = getattr(request, self.session_name).get_expiry_age()
max_age = getattr(request, self.cookie_name).get_expiry_age()
expires_time = time.time() + max_age
expires = http_date(expires_time)
# Save the session data and refresh the client cookie.
# Skip session save for 500 responses, refs #3881.
if response.status_code != 500:
try:
getattr(request, self.session_name).save()
request.saml_session.save()
except UpdateError:
raise SuspiciousOperation(
"The request's session was deleted before the "
"request completed. The user may have logged "
"out in a concurrent request, for example."
)
response.set_cookie(
self.session_name,
getattr(request, self.session_name).session_key,
self.cookie_name,
request.saml_session.session_key,
max_age=max_age,
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
Expand Down
5 changes: 0 additions & 5 deletions djangosaml2/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,6 @@ def validate_referral_url(request, url):
return url


def get_saml_request_session(request):
session_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
return getattr(request, session_name)


def saml2_from_httpredirect_request(url):
urlquery = urllib.parse.urlparse(url).query
b64_inflated_saml2req = urllib.parse.parse_qs(urlquery)['SAMLRequest'][0]
Expand Down
30 changes: 13 additions & 17 deletions djangosaml2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
from .signals import post_authenticated
from .utils import (available_idps, fail_acs_response, get_custom_setting,
get_idp_sso_supported_bindings, get_location,
validate_referral_url, get_saml_request_session)
validate_referral_url)

try:
from django.contrib.auth.views import LogoutView
Expand Down Expand Up @@ -246,9 +246,8 @@ def login(request,
else:
raise UnsupportedBinding('Unsupported binding: %s', binding)

saml_session = get_saml_request_session(request)
# success, so save the session ID and return our response
oq_cache = OutstandingQueriesCache(saml_session)
oq_cache = OutstandingQueriesCache(request.saml_session)
oq_cache.set(session_id, came_from)
logger.debug('Saving the session_id "{}" in the OutstandingQueries cache'.format(oq_cache.__dict__))
return http_response
Expand Down Expand Up @@ -290,9 +289,8 @@ def post(self,
logger.warning('Missing "SAMLResponse" parameter in POST data.')
raise SuspiciousOperation

saml_session = get_saml_request_session(request)
client = Saml2Client(conf, identity_cache=IdentityCache(saml_session))
oq_cache = OutstandingQueriesCache(saml_session)
client = Saml2Client(conf, identity_cache=IdentityCache(request.saml_session))
oq_cache = OutstandingQueriesCache(request.saml_session)
oq_cache.sync()
outstanding_queries = oq_cache.outstanding_queries()

Expand Down Expand Up @@ -321,8 +319,8 @@ def post(self,
logger.warning("Missing Authentication Context from IdP.", exc_info=True)
return fail_acs_response(request, exception=e)
except MissingKey as e:
logger.exception("SAML Identity Provider is not configured "
"correctly: certificate key is missing!")
logger.exception("SAML Identity Provider is not configured correctly: "
"certificate key is missing!")
return fail_acs_response(request, exception=e)
except UnsolicitedResponse as e:
logger.exception("Received SAMLResponse when no request has been made.")
Expand Down Expand Up @@ -354,7 +352,7 @@ def post(self,
return fail_acs_response(request, exception=PermissionDenied('No user could be authenticated.'))

auth.login(self.request, user)
_set_subject_id(saml_session, session_info['name_id'])
_set_subject_id(request.saml_session, session_info['name_id'])
logger.debug("User %s authenticated via SSO.", user)
logger.debug('Sending the post_authenticated signal')

Expand Down Expand Up @@ -437,13 +435,12 @@ def logout(request, config_loader_path=None):
This view initiates the SAML2 Logout request
using the pysaml2 library to create the LogoutRequest.
"""
saml_session = get_saml_request_session(request)
state = StateCache(saml_session)
state = StateCache(request.saml_session)
conf = get_config(config_loader_path, request)

client = Saml2Client(conf, state_cache=state,
identity_cache=IdentityCache(saml_session))
subject_id = _get_subject_id(saml_session)
identity_cache=IdentityCache(request.saml_session))
subject_id = _get_subject_id(request.saml_session)
if subject_id is None:
logger.warning(
'The session does not contain the subject id for user %s',
Expand Down Expand Up @@ -510,10 +507,9 @@ def do_logout_service(request, data, binding, config_loader_path=None, next_page
logger.debug('Logout service started')
conf = get_config(config_loader_path, request)

saml_session = get_saml_request_session(request)
state = StateCache(saml_session)
state = StateCache(request.saml_session)
client = Saml2Client(conf, state_cache=state,
identity_cache=IdentityCache(saml_session))
identity_cache=IdentityCache(request.saml_session))

if 'SAMLResponse' in data: # we started the logout
logger.debug('Receiving a logout response from the IdP')
Expand All @@ -523,7 +519,7 @@ def do_logout_service(request, data, binding, config_loader_path=None, next_page

elif 'SAMLRequest' in data: # logout started by the IdP
logger.debug('Receiving a logout request from the IdP')
subject_id = _get_subject_id(saml_session)
subject_id = _get_subject_id(request.saml_session)

if subject_id is None:
logger.warning(
Expand Down

0 comments on commit 4c6e98f

Please sign in to comment.