Code

Opened 3 years ago

Closed 3 years ago

Last modified 3 years ago

#14579 closed New feature (duplicate)

Use built-in sessions middleware for entirely cookie-based sessions

Reported by: oran Owned by: nobody
Component: contrib.sessions Version: 1.2
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by gabrielhurley)

My goal was to configure django to work with sessions that use nothing but cookies for session management.
Built-in session backends all use cookies just for the session key, but store session data elsewhere.
Since my session data is very small, and I don't / can't have them written to the disk or cache, I want to store it entirely in a cookie.

It turns out that not only does Django not provide such a sessions backend - it's also impossible to write one without also customizing the sessions middleware. This is because cookies can only be read from the request and written to the response, but these objects are not provided to the session by the middleware!

I ended up replacing the default sessions middleware with a patched version that adds the needed objects to the session, if the session object has the attributes that accept them. See patch + simple code for the backend below.

You might want to build this (or something similar) into the framework - others may also find this useful.

=================================================================
Middleware patch
=================================================================

8c8
< class SessionMiddleware(django.contrib.sessions.middleware.SessionMiddleware):
---
> class SessionMiddleware(object):
10d9
<         '''Similar to original constructor, but also keeps the request in the session'''
14,15d12
<         if hasattr(request.session, 'request'):
<             request.session.request = request
22,23d18
<         if hasattr(request.session, 'response'):
<             request.session.response = response
47,48d41
<                 if hasattr(request.session, 'put_in_cookie'):
<                     request.session.put_in_cookie()



=================================================================
Simple implementation for cookie-only sessions backend:
=================================================================

from Crypto.Cipher import AES
from django.conf import settings
from django.contrib.sessions.backends.base import SessionBase
from django.utils.http import cookie_date
import base64
import django.conf
import hashlib
import re
import time


BLOCK_SIZE = 32
PADDING = '{'
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING


class SessionStore(SessionBase):
    """
    Implements a cookie based session store.
    """
    def __init__(self, session_key=None):
        self.cookie_name = getattr(settings, "SESSION_COOKIE_DATA_NAME", "SessionData")
        m = hashlib.md5()
        m.update("cookies!")
        m.update(getattr(settings, "SECRET_KEY"))
        m.update("cookies!")
        self.secret = m.hexdigest()
        self.request = None
        self.response = None
        super(SessionStore, self).__init__(session_key)
        self.saved_session_data = None

    def _encrypt(self, plain):
        c = AES.new(self.secret)
        padded = pad(plain)
        encrypted = c.encrypt(padded)
        wrapped = base64.b64encode(encrypted) + '-%d'%len(plain)
        return wrapped
    
    def _decrypt(self, cipher):
        lstr = re.findall(r'-\d+$', cipher)[0]
        l = int(lstr[1:])
        c = AES.new(self.secret)
        encrypted = base64.b64decode(cipher[:-len(lstr)])
        padded = c.decrypt(encrypted)
        return padded[:l]
        
    def create(self):
        self._session_key = self._get_new_session_key()
        self.save(must_create=True)
        self.modified = True
        return

    def load(self):
        cookie_data = self.request.COOKIES.get(self.cookie_name, None)
        if cookie_data == None:
            return None
        decrypted = self._decrypt(cookie_data)
        decoded = self.decode(decrypted)
        return decoded

    def save(self, must_create=False):
        encoded = self.encode(self._get_session(no_load=must_create))
        encrypted = self._encrypt(encoded)
        session_data = encrypted
        self.saved_session_data = session_data
    
    def put_in_cookie(self):
        max_age = self.get_expiry_age()
        expires_time = time.time() + max_age
        expires = cookie_date(expires_time)
        if self.response==None:
            pass
        self.response.set_cookie(self.cookie_name,
                self.saved_session_data, max_age=max_age,
                        expires=expires, domain=django.conf.settings.SESSION_COOKIE_DOMAIN,
                        path=django.conf.settings.SESSION_COOKIE_PATH,
                        secure=django.conf.settings.SESSION_COOKIE_SECURE or None)

    def exists(self, session_key):
        return False     # we can't possibly have session cookie ID conflicts...

    def delete(self, session_key=None):
        if self.response:
            self.response.delete_cookie(
                    self.cookie_name, domain=django.conf.settings.SESSION_COOKIE_DOMAIN,
                    path=django.conf.settings.SESSION_COOKIE_PATH)

    def clean(self):
        pass

Attachments (1)

patch.2.diff (390 bytes) - added by oran <oran@…> 3 years ago.
the patch

Download all attachments as: .zip

Change History (9)

Changed 3 years ago by oran <oran@…>

the patch

comment:1 Changed 3 years ago by gabrielhurley

  • Description modified (diff)
  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

Reformatted ticket description.

comment:2 Changed 3 years ago by lukeplant

  • Triage Stage changed from Unreviewed to Accepted

It would definitely be nice to make an entirely cookie based session backend possible without patching Django. The patch as it is requires a fair bit of work:

  1. Unified diff please! Taken from the root of the django src. And preferably not backwards. It's extremely difficult to understand otherwise, and very fragile to apply.
  2. Tests needed
  3. Docs needed
  4. Do we really need to check whether 'request' and 'response' attributes exist before adding them?

But, more substantially: Is there actually some nicer way of giving the session access to the request and response objects? The method provided works (if I read the patch correctly, which is difficult), but is pretty ugly:

  • 'request' and 'response' are not passed in explicitly anywhere to a SessionStore method. This is a very unobvious API from the point of view of implementing the backend - you have to know exactly when the request and response objects are going to appear.
  • a custom 'put_in_cookie' method which needs explicit support in the middleware. It is special casing for this one backend that doesn't help any other backend.

Essentially this requirement needs a slightly different API to the one provided by SessionStore. Our normal way of handling this is to deprecate the existing API and migrate to a new, better one, gradually if it is possible - like object permissions in the auth backend. In this case, I think we need something like a SessionBase.save_to_response method, whose default implementation contains some of the code from the session middleware. The obvious place to pass in the request would be the __init__ method, which would not be backwards compatible.

To cater for these two, a custom class attribute (or more than one) could be used to distinguish the new API from the old.

This has already turned into a significant piece of work, sorry about that...

As the for the actual implementation of the backend, there are a lot more issues if it were to be considered for inclusion into Django, but I won't go into that.

comment:3 Changed 3 years ago by adamnelson

  • Component changed from Uncategorized to django.contrib.sessions
  • Summary changed from built-in sessions middleware can't be used for entirely cookie-based sessions to Use built-in sessions middleware for entirely cookie-based sessions

comment:4 Changed 3 years ago by PaulM

  • milestone set to 1.4

This is a feature that would be nice to have in Django, but it's not gonna happen in 1.3. Setting to the 1.4 milestone for now.

comment:5 Changed 3 years ago by julien

  • Severity set to Normal
  • Type set to New feature

comment:6 Changed 3 years ago by lukeplant

  • Easy pickings unset
  • UI/UX unset

#16199 is a duplicate, and has a much nicer approach, showing that Django does not need to be patched for this to work.

comment:7 Changed 3 years ago by lukeplant

  • Resolution set to duplicate
  • Status changed from new to closed

comment:8 Changed 3 years ago by jacob

  • milestone 1.4 deleted

Milestone 1.4 deleted

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as closed
as The resolution will be set. Next status will be 'closed'
The resolution will be deleted. Next status will be 'new'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.