This post is part of my journey through Djangonaut Space, a program helping developers contribute to Django. Follow along as I navigate through this adventure!
Finding My First Ticket
As a new Djangonaut, I was eager to dive into my first contribution to Django. But where do you start with such a large and established codebase?
I followed some excellent advice from this YouTube short about finding tickets suitable for newcomers. The key was to look for tickets that:
- Already had a proposed patch but needed refinement
- Required documentation updates
- Hadn’t been modified in some time
With guidance from our captain Ryan, I came across ticket #27775: “Signed cookies does not support custom expiry.” This issue had been open for 8 years!
Understanding the Problem
The issue was straightforward but important: Django’s signed cookies session backend wasn’t respecting the set_expiry()
method. When you called this method on a signed cookie session, it simply did nothing - the call was silently ignored.
This was problematic for security reasons. Imagine you have a sensitive area of your application where you want sessions to expire quickly. If you set your default session timeout to a day but need a specific view to expire after 15 minutes, you’d expect set_expiry(900)
to work - but with signed cookies, it wouldn’t!
Looking at the code, I found this telling comment:
# This doesn't handle non-default expiry dates, see #19201
Clearly, this was a known limitation, but it had never been addressed.
The Solution Approach
The solution had several parts:
- Modify the core
signing
module to support anexpiration_key
parameter - Update the
signed_cookies
session backend to use this new parameter - Add proper tests and documentation
Let’s break down each part.
Enhancing Django’s Signing Module
The signing module is used by Django to cryptographically sign data. It already had timestamp-based expiration via max_age
, but this was a global setting. We needed a way to embed an expiration timestamp within the signed data itself.
I added an expiration_key
parameter to the TimestampSigner.unsign_object()
and loads()
functions. This parameter lets you specify a key in your data that contains an expiration time in seconds:
def unsign_object(self, signed_obj, max_age=None, expiration_key=None, **kwargs):
# First verify the outer max_age boundary
value = super().unsign_object(signed_obj, max_age=max_age, **kwargs)
# Then if expiration_key is provided, check it as an additional constraint
if expiration_key is not None:
expiry = value.get(expiration_key)
if expiry is not None:
self._verify_max_age(expiry)
return value
This means you can now sign data like {"username": "alice", "_session_expiry": 300}
and the signature will automatically expire after 300 seconds when using expiration_key="_session_expiry"
.
Updating the Signed Cookies Backend
With the core functionality in place, updating the session backend was simple. I just needed to add the expiration_key
parameter to the loads()
call:
return signing.loads(
self.session_key,
serializer=self.serializer,
max_age=self.get_session_cookie_age(),
salt="django.contrib.sessions.backends.signed_cookies",
expiration_key="_session_expiry", # This is the new line
)
Adding Tests
TODO
Documentation Updates
Documentation is crucial! I added details about the new expiration_key
parameter to the docs with a clear example:
.. class:: TimestampSigner(*, key=None, sep=':', salt=None, algorithm='sha256')
.. method:: unsign_object(signed_obj, serializer=JSONSerializer, max_age=None, expiration_key=None,)
Checks if ``signed_obj`` was signed less than ``max_age`` seconds ago,
otherwise raises ``SignatureExpired``. The ``max_age`` parameter can
accept an integer or a :py:class:`datetime.timedelta` object.
If ``expiration_key`` is provided, it specifies a key in the signed data
that contains an additional expiration time in seconds. This expiration
acts as an additional constraint - the signature must be within both
``max_age`` (if provided) and the time specified in ``expiration_key``.
For example, if ``max_age=30`` and the signed data contains
``{"foo": "bar", "exp": 10}``, the signature will expire after 10
seconds when using ``expiration_key="exp"``.
.. versionchanged:: 6.0
The ``expiration_key`` parameter was added.
I made sure to include the versionchanged
directive to indicate this is a new feature in Django 6.0.
Submitting My PR
With all the code, tests, and documentation in place, I submitted PR #19191.
I quickly received valuable feedback, particularly about handling non-dictionary values properly. One reviewer pointed out:
Value here is allowed to be a “complex data structure (e.g. list, tuple, or dictionary)”. Should support be added for non-dictionary types (perhaps supporting indexing?). Alternatively, should the error message be nicer if it is of the wrong type?
This was a great point! I responded by proposing a more graceful error handling approach:
if not isinstance(value, dict):
raise TypeError(
f"expiration_key is only supported for dict values, not {type(value).__name__}"
)
Lessons Learned
This first PR taught me several valuable lessons:
-
Understand the context: Reading both the ticket history and related issues (#19201) gave me important context about the problem and previous attempts to solve it.
-
Be thorough: Solving the issue required touching multiple parts of Django - the core signing module, the session backend, tests, and documentation.
-
Think about security: Session expiration is a security feature, and fixing this bug helps developers implement proper security practices.
-
Embrace the review process: Getting feedback from experienced Django developers is invaluable - they catch edge cases and potential improvements I might have missed.
What’s Next?
I’m excited to continue my Djangonaut journey! For my next contribution, I’m looking for:
- Another ticket that matches my skills
- An opportunity to dive deeper into other areas of Django’s internals
- A chance to work on a more complex feature
Stay tuned for Week 2 of my Djangonaut Space adventure!
This post is part of my journey through Djangonaut Space, a program helping developers contribute to Django. Follow along as I navigate through this adventure!