Skip to content

Commit 4c6e98f

Browse files
committed
SAML session refactor and minor changes in README file
1 parent 9cfe3a5 commit 4c6e98f

File tree

4 files changed

+43
-44
lines changed

4 files changed

+43
-44
lines changed

README.rst

+17-10
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ any data model. The only reason we include it is to be able to run
6262
djangosaml2 test suite from our project, something you should always
6363
do to make sure it is compatible with your Django version and environment.
6464

65-
.. note::
65+
.. Note::
6666

6767
When you finish the configuration you can run the djangosaml2 test suite as
6868
you run any other Django application test suite. Just type ``python manage.py
@@ -96,7 +96,7 @@ been authenticated before. We are also telling that when the user closes
9696
his browser, the session should be terminated. This is useful in SAML2
9797
federations where the logout protocol is not always available.
9898

99-
.. note::
99+
.. Note::
100100

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

243-
# where the remote metadata is stored
243+
# where the remote metadata is stored, local, remote or mdq server.
244+
# One metadatastore or many ...
244245
'metadata': {
245246
'local': [path.join(BASEDIR, 'remote_metadata.xml')],
247+
'remote': [{"url": "https://idp.testunical.it/idp/shibboleth",
248+
"disable_ssl_certificate_validation": True},],
249+
'mdq': [{"url": "https://ds.testunical.it",
250+
"cert": "certficates/others/ds.testunical.it.cert",
251+
"disable_ssl_certificate_validation": True}]
246252
},
247253

248254
# set to 1 to output debugging information
249255
'debug': 1,
250256

251257
# Signing
252-
'key_file': path.join(BASEDIR, 'mycert.key'), # private part
253-
'cert_file': path.join(BASEDIR, 'mycert.pem'), # public part
258+
'key_file': path.join(BASEDIR, 'private.key'), # private part
259+
'cert_file': path.join(BASEDIR, 'public.pem'), # public part
254260

255261
# Encryption
256262
'encryption_keypairs': [{
257-
'key_file': path.join(BASEDIR, 'my_encryption_key.key'), # private part
258-
'cert_file': path.join(BASEDIR, 'my_encryption_cert.pem'), # public part
263+
'key_file': path.join(BASEDIR, 'private.key'), # private part
264+
'cert_file': path.join(BASEDIR, 'public.pem'), # public part
259265
}],
260266

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

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

312-
.. note::
317+
.. Note::
313318

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

322+
..
323+
openssl req -nodes -new -x509 -days 3650 -keyout private.key -out public.cert
317324
318325
Custom and dynamic configuration loading
319326
........................................
@@ -336,7 +343,7 @@ SameSite cookie
336343

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

341348
SAML_SESSION_COOKIE_NAME = 'saml_session'
342349

djangosaml2/middleware.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111

1212
class SamlSessionMiddleware(SessionMiddleware):
13-
session_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
13+
cookie_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
1414

1515
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))
16+
session_key = request.COOKIES.get(self.cookie_name, None)
17+
request.saml_session = self.SessionStore(session_key)
1818

1919
def process_response(self, request, response):
2020
"""
@@ -23,16 +23,16 @@ def process_response(self, request, response):
2323
the session cookie if the session has been emptied.
2424
"""
2525
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()
26+
accessed = request.saml_session.accessed
27+
modified = request.saml_session.modified
28+
empty = request.saml_session.is_empty()
2929
except AttributeError:
3030
return response
3131
# First check if we need to delete this cookie.
3232
# The session should be deleted only if the session is entirely empty.
33-
if self.session_name in request.COOKIES and empty:
33+
if self.cookie_name in request.COOKIES and empty:
3434
response.delete_cookie(
35-
self.session_name,
35+
self.cookie_name,
3636
path=settings.SESSION_COOKIE_PATH,
3737
domain=settings.SESSION_COOKIE_DOMAIN,
3838
samesite=None,
@@ -41,28 +41,29 @@ def process_response(self, request, response):
4141
else:
4242
if accessed:
4343
patch_vary_headers(response, ('Cookie',))
44+
# relies and the global one
4445
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
4546
if request.session.get_expire_at_browser_close():
4647
max_age = None
4748
expires = None
4849
else:
49-
max_age = getattr(request, self.session_name).get_expiry_age()
50+
max_age = getattr(request, self.cookie_name).get_expiry_age()
5051
expires_time = time.time() + max_age
5152
expires = http_date(expires_time)
5253
# Save the session data and refresh the client cookie.
5354
# Skip session save for 500 responses, refs #3881.
5455
if response.status_code != 500:
5556
try:
56-
getattr(request, self.session_name).save()
57+
request.saml_session.save()
5758
except UpdateError:
5859
raise SuspiciousOperation(
5960
"The request's session was deleted before the "
6061
"request completed. The user may have logged "
6162
"out in a concurrent request, for example."
6263
)
6364
response.set_cookie(
64-
self.session_name,
65-
getattr(request, self.session_name).session_key,
65+
self.cookie_name,
66+
request.saml_session.session_key,
6667
max_age=max_age,
6768
expires=expires, domain=settings.SESSION_COOKIE_DOMAIN,
6869
path=settings.SESSION_COOKIE_PATH,

djangosaml2/utils.py

-5
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,6 @@ def validate_referral_url(request, url):
101101
return url
102102

103103

104-
def get_saml_request_session(request):
105-
session_name = getattr(settings, 'SAML_SESSION_COOKIE_NAME', 'saml_session')
106-
return getattr(request, session_name)
107-
108-
109104
def saml2_from_httpredirect_request(url):
110105
urlquery = urllib.parse.urlparse(url).query
111106
b64_inflated_saml2req = urllib.parse.parse_qs(urlquery)['SAMLRequest'][0]

djangosaml2/views.py

+13-17
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
from .signals import post_authenticated
5454
from .utils import (available_idps, fail_acs_response, get_custom_setting,
5555
get_idp_sso_supported_bindings, get_location,
56-
validate_referral_url, get_saml_request_session)
56+
validate_referral_url)
5757

5858
try:
5959
from django.contrib.auth.views import LogoutView
@@ -246,9 +246,8 @@ def login(request,
246246
else:
247247
raise UnsupportedBinding('Unsupported binding: %s', binding)
248248

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

293-
saml_session = get_saml_request_session(request)
294-
client = Saml2Client(conf, identity_cache=IdentityCache(saml_session))
295-
oq_cache = OutstandingQueriesCache(saml_session)
292+
client = Saml2Client(conf, identity_cache=IdentityCache(request.saml_session))
293+
oq_cache = OutstandingQueriesCache(request.saml_session)
296294
oq_cache.sync()
297295
outstanding_queries = oq_cache.outstanding_queries()
298296

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

356354
auth.login(self.request, user)
357-
_set_subject_id(saml_session, session_info['name_id'])
355+
_set_subject_id(request.saml_session, session_info['name_id'])
358356
logger.debug("User %s authenticated via SSO.", user)
359357
logger.debug('Sending the post_authenticated signal')
360358

@@ -437,13 +435,12 @@ def logout(request, config_loader_path=None):
437435
This view initiates the SAML2 Logout request
438436
using the pysaml2 library to create the LogoutRequest.
439437
"""
440-
saml_session = get_saml_request_session(request)
441-
state = StateCache(saml_session)
438+
state = StateCache(request.saml_session)
442439
conf = get_config(config_loader_path, request)
443440

444441
client = Saml2Client(conf, state_cache=state,
445-
identity_cache=IdentityCache(saml_session))
446-
subject_id = _get_subject_id(saml_session)
442+
identity_cache=IdentityCache(request.saml_session))
443+
subject_id = _get_subject_id(request.saml_session)
447444
if subject_id is None:
448445
logger.warning(
449446
'The session does not contain the subject id for user %s',
@@ -510,10 +507,9 @@ def do_logout_service(request, data, binding, config_loader_path=None, next_page
510507
logger.debug('Logout service started')
511508
conf = get_config(config_loader_path, request)
512509

513-
saml_session = get_saml_request_session(request)
514-
state = StateCache(saml_session)
510+
state = StateCache(request.saml_session)
515511
client = Saml2Client(conf, state_cache=state,
516-
identity_cache=IdentityCache(saml_session))
512+
identity_cache=IdentityCache(request.saml_session))
517513

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

524520
elif 'SAMLRequest' in data: # logout started by the IdP
525521
logger.debug('Receiving a logout request from the IdP')
526-
subject_id = _get_subject_id(saml_session)
522+
subject_id = _get_subject_id(request.saml_session)
527523

528524
if subject_id is None:
529525
logger.warning(

0 commit comments

Comments
 (0)