6

I want to hold information about a users preferences in a session variable. If the user chooses a preference while logged out and then later logs in, I want the preference to be maintained without needing to reselect it.

Django sessions maintain a session key in a cookie to track a users session. The way I understand it, this key is changed when a user logs in.

a) Does this mean all session variables are deleted on login or is there any sort of passover

b) In the case of not being able to save preferences across a login, is manually setting cookies the best way to proceed? I imagine a scenario like:

  • while logged out, maintain preferences in cookie
  • on login, copy preferences to session variable and write to db (via signal?)
  • on logout, update cookies with preferences (via signal?)

Update

I managed to get this functionality by saving preferences in the User's profile object, as well as in a cookie (these preferences are not sensitive in any way). When the user is logged in, their profile setting takes preference. When not logged in, the cookie preference is chosen

Timmy O'Mahony
  • 48,778
  • 16
  • 144
  • 168

5 Answers5

11

Upon login, Django calls session.flush() or session.cycle_key(), which makes sure nothing from the old session is kept. This is a security measure that protects you against session fixation vulnerabilities. So, when applying this solution, be aware what find of variables you want to persist.

If you want to keep some state, you'll have to restore that after the login was issued.

The solution by Chase Seibert was a great start, it was very insecure due to threadsafety issues in that code. You can find an improved version here, which is safe to use:

from functools import wraps

class persist_session_vars(object):
    """
    Some views, such as login and logout, will reset all session state.
    (via a call to ``request.session.cycle_key()`` or ``session.flush()``).
    That is a security measure to mitigate session fixation vulnerabilities.

    By applying this decorator, some values are retained.
    Be very aware what find of variables you want to persist.
    """

    def __init__(self, vars):
        self.vars = vars

    def __call__(self, view_func):

        @wraps(view_func)
        def inner(request, *args, **kwargs):
            # Backup first
            session_backup = {}
            for var in self.vars:
                try:
                    session_backup[var] = request.session[var]
                except KeyError:
                    pass

            # Call the original view
            response = view_func(request, *args, **kwargs)

            # Restore variables in the new session
            for var, value in session_backup.items():
                request.session[var] = value

            return response

        return inner

and now you can write:

from django.contrib.auth import views

@persist_session_vars(['some_field'])
def login(request, *args, **kwargs):
    return views.login(request, *args, **kwargs)

And for class based views (django-allauth):

import allauth.account.views as auth_views
from django.utils.decorators import method_decorator

@method_decorator(persist_session_vars(['some_field']), name='dispatch')
class LoginView(auth_views.LoginView):
    pass

and use that view in the url patterns:

import allauth.urls
from django.conf.urls import include, url

from . import views

urlpatterns = [
    # Views that overlap the default:
    url(r'^login/$', views.LoginView.as_view(), name='account_login'),

    # default allauth urls
    url(r'', include(allauth.urls)),
]
Paolo
  • 16,171
  • 20
  • 78
  • 110
vdboor
  • 19,540
  • 11
  • 74
  • 91
2

When you login/logout Django will flush all sessions if another user logs in (request.session.flush() in auth/init.py).

You're better of storing user settings in the database and add some middleware to get that data and store it in your request.

Willian
  • 2,257
  • 14
  • 15
1

user data that persists sounds like it should live in something like a UserProfile model

second
  • 25,089
  • 7
  • 68
  • 73
  • It will be written to the DB once the user logs in, but if the user isn't logged in, i'd still like them to be able to set preferences and once they eventually do login, those preferences would be written to the userprofile model so the problem is maintaining the data across a login – Timmy O'Mahony Nov 24 '11 at 12:54
  • but you can't know if it's the same person or just another user of the same browser. so you are happy to assume this is the same person? – second Nov 24 '11 at 12:58
  • For simple display settings yes, but the settings DB objects can be marked as `authorized` so that only certain settings get written to a cookie while all of them get written to a logged in session. – Timmy O'Mahony Nov 24 '11 at 13:49
0

I actually think your initial design made sense. If you want to save some session variables across the login/logout boundary, you can do something like this.

from functools import wraps

class persist_session_vars(object):
    """ Some views, such as login and logout, will reset all session state.
    However, we occasionally want to persist some of those session variables.
    """

    session_backup = {}

    def __init__(self, vars):
        self.vars = vars

    def __enter__(self):
        for var in self.vars:
            self.session_backup[var] = self.request.session.get(var)

    def __exit__(self, exc_type, exc_value, traceback):
        for var in self.vars:
            self.request.session[var] = self.session_backup.get(var)

    def __call__(self, test_func, *args, **kwargs):

        @wraps(test_func)
        def inner(*args, **kwargs):
            if not args:
                raise Exception('Must decorate a view, ie a function taking request as the first parameter')
            self.request = args[0]
            with self:
                return test_func(*args, **kwargs)

        return inner

You would throw this decorator on whatever view you're calling auth.login/logout from. If you're delegating to the built-in view for those, you can easily wrap them.

from django.contrib.auth import views

@persist_session_vars(['HTTP_REFERER'])
def login(request, *args, **kwargs):
    return views.login(request, *args, **kwargs)
Chase Seibert
  • 15,061
  • 5
  • 48
  • 58
  • This is a great start, but isn't thread-safe! Since the `persist_session_vars` is instantiated once, all users are writing to the same `session_backup` dict (even making it an object variable instead of class attribute doesn't change that here) – vdboor Jan 25 '17 at 10:23
  • When two users login at the same time, they'll receive each others' session variable. in your example, your statistics would be off. However, for persisting personal information this way, it introduces a huge security/information leak in the web site. – vdboor Jan 25 '17 at 10:36
0

You can create a form Mixin that allows you to persist form values to the user's session (which doesn't require them being logged in). This is useful for things such as filter/sorting options on a public table-view report, where you want to keep their filter options persistent across refreshes.

filter form screenshot

View:

def list_states(request):
    if request.method == 'GET':
        form = StateListFilterForm().load_from_session(request.session)
    elif request.method == 'POST':
        form = StateListFilterForm(request.POST)
        form.persist_to_session()
    return render('forms/state_list.html', RequestContext(request, {'state_form': form})

Form:

class PersistableMixin:
    def persist_to_session(form, session):
        for key in form.fields.keys():
            val = getattr(form, 'cleaned_data', form.data).get(key, None)
            if val:  # will not store empty str values
                session[key] = val
        return True

    def load_from_session(form, session):
        for key in form.fields.keys():
            saved_val = session.get(key, '')
            if saved_val:  # will not load empty str values
                form.fields[key].initial = saved_val
        return form


class StateListFilterForm(forms.Form, PersistableMixin):
    states = forms.MultipleChoiceField(required=False, choices=US_STATES)
Nick Sweeting
  • 4,123
  • 5
  • 23
  • 35