Skip to content

Commit 8879f49

Browse files
committed
SameSite cookie hanlders in SamlSessionMiddleware - thanks to Andre Borie for guidelines
1 parent 2b41019 commit 8879f49

File tree

8 files changed

+230
-67
lines changed

8 files changed

+230
-67
lines changed

.coverage

52 KB
Binary file not shown.

README.rst

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ do to make sure it is compatible with your Django version and environment.
6868
you run any other Django application test suite. Just type ``python manage.py
6969
test djangosaml2``.
7070

71-
Python 2 users need to ``pip install djangosaml2[test]`` in order to run the
71+
Python users need to ``pip install djangosaml2[test]`` in order to run the
7272
tests.
7373

7474
Then you have to add the ``djangosaml2.backends.Saml2Backend``
@@ -106,6 +106,10 @@ If you want to allow several authentication mechanisms in your project
106106
you should set the LOGIN_URL option to another view and put a link in such
107107
view to the ``/saml2/login/`` view.
108108

109+
Add the SAML Session Middleware as follow::
110+
111+
MIDDLEWARE.append('djangosaml2.middleware.SamlSessionMiddleware')
112+
109113
Handling Post-Login Redirects
110114
-----------------------------
111115
It is often desireable for the client to maintain the URL state (or at least manage it) so that
@@ -116,11 +120,11 @@ host matches the output of get_host(). However, in some cases it becomes desire
116120
hostnames to be used for the post-login redirect. In such cases, the setting::
117121

118122
SAML_ALLOWED_HOSTS = []
119-
123+
120124
May be set to a list of allowed post-login redirect hostnames (note, the URL components beyond the hostname
121-
may be specified by the client - typically with the ?next= parameter.)
125+
may be specified by the client - typically with the ?next= parameter.)
122126

123-
In the absence of a ?next= parameter, the LOGIN_REDIRECT_URL setting will be used (assuming the destination hostname
127+
In the absence of a ?next= parameter, the LOGIN_REDIRECT_URL setting will be used (assuming the destination hostname
124128
either matches the output of get_host() or is included in the SAML_ALLOWED_HOSTS setting)
125129

126130

@@ -205,11 +209,11 @@ We will see a typical configuration for protecting a Django project::
205209
},
206210
# Mandates that the identity provider MUST authenticate the
207211
# presenter directly rather than rely on a previous security context.
208-
'force_authn': False,
209-
212+
'force_authn': False,
213+
210214
# Enable AllowCreate in NameIDPolicy.
211215
'name_id_format_allow_create': False,
212-
216+
213217
# attributes that this project need to identify a user
214218
'required_attributes': ['uid'],
215219

@@ -327,6 +331,15 @@ setting::
327331
SAML_CONFIG_LOADER = 'python.path.to.your.callable'
328332

329333

334+
SameSite cookie
335+
...............
336+
337+
By default, djangosaml2 handle the saml2 session in a separate cookie.
338+
The storage linked to it is accessible by default at `request.saml_session`.
339+
You can even configure this using::
340+
341+
SAML_SESSION_COOKIE_NAME = 'saml_session'
342+
330343
Custom error handler
331344
....................
332345

@@ -544,12 +557,16 @@ Unit tests
544557
You can also run the unit tests as follows::
545558

546559
pip install -r requirements-dev.txt
560+
# or
561+
pip install djangosaml2[test]
547562
python3 tests/manage.py migrate
548-
563+
564+
then::
565+
549566
python tests/run_tests.py
550567

551568
or::
552-
569+
553570
cd tests/
554571
./manage.py test djangosaml2
555572

djangosaml2/cache.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def delete(self, saml2_session_id):
6363
del self._db[saml2_session_id]
6464
self._db.sync()
6565

66+
def sync(self):
67+
self._db.sync()
68+
6669

6770
class IdentityCache(Cache):
6871
"""Handles information about the users that have been succesfully

djangosaml2/middleware.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import time
2+
from importlib import import_module
3+
4+
from django.conf import settings
5+
from django.contrib.sessions.backends.base import UpdateError
6+
from django.contrib.sessions.middleware import SessionMiddleware
7+
from django.core.exceptions import SuspiciousOperation
8+
from django.utils.cache import patch_vary_headers
9+
from django.utils.http import http_date
10+
11+
12+
class SamlSessionMiddleware(SessionMiddleware):
13+
session_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
14+
15+
def process_request(self, request):
16+
session_key = request.COOKIES.get(self.session_name, None)
17+
setattr(request, self.session_name, self.SessionStore(session_key))
18+
19+
def process_response(self, request, response):
20+
"""
21+
If request.saml_session was modified, or if the configuration is to save the
22+
session every time, save the changes and set a session cookie or delete
23+
the session cookie if the session has been emptied.
24+
"""
25+
try:
26+
accessed = getattr(request, self.session_name).accessed
27+
modified = getattr(request, self.session_name).modified
28+
empty = getattr(request, self.session_name).is_empty()
29+
except AttributeError:
30+
return response
31+
# First check if we need to delete this cookie.
32+
# The session should be deleted only if the session is entirely empty.
33+
if self.session_name in request.COOKIES and empty:
34+
response.delete_cookie(
35+
self.session_name,
36+
path=settings.SESSION_COOKIE_PATH,
37+
domain=settings.SESSION_COOKIE_DOMAIN,
38+
samesite=None,
39+
)
40+
patch_vary_headers(response, ('Cookie',))
41+
else:
42+
if accessed:
43+
patch_vary_headers(response, ('Cookie',))
44+
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
45+
if request.session.get_expire_at_browser_close():
46+
max_age = None
47+
expires = None
48+
else:
49+
max_age = getattr(request, self.session_name).get_expiry_age()
50+
expires_time = time.time() + max_age
51+
expires = http_date(expires_time)
52+
# Save the session data and refresh the client cookie.
53+
# Skip session save for 500 responses, refs #3881.
54+
if response.status_code != 500:
55+
try:
56+
getattr(request, self.session_name).save()
57+
except UpdateError:
58+
raise SuspiciousOperation(
59+
"The request's session was deleted before the "
60+
"request completed. The user may have logged "
61+
"out in a concurrent request, for example."
62+
)
63+
response.set_cookie(
64+
self.session_name,
65+
getattr(request, self.session_name).session_key,
66+
max_age=max_age,
67+
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
68+
path=settings.SESSION_COOKIE_PATH,
69+
secure=settings.SESSION_COOKIE_SECURE or None,
70+
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
71+
samesite=None
72+
)
73+
return response

0 commit comments

Comments
 (0)