Most optimized way to delete all sessions for a specific user in Django?

Viewed 16716

I'm running Django 1.3, using Sessions Middleware and Auth Middleware:

# settings.py

SESSION_ENGINE = django.contrib.sessions.backends.db   # Persist sessions to DB
SESSION_COOKIE_AGE = 1209600                           # Cookies last 2 weeks

Each time a user logs in from a different location (different computer/browser), a new Session() is created and saved with a unique session_id. This can result in multiple database entries for the same user. Their login persists on that node until the cookie is deleted or session expires.

When a user changes their password, I want to delete all unexpired sessions for that user from the DB. That way after a password change, they're forced to re-login. This is for security purposes, such as if your computer got stolen, or you accidentally left yourself logged-in on a public terminal.

I want to know the best way to optimize this. Here's how I've done it:

# sessions_helpers.py

from django.contrib.sessions.models import Session
import datetime

def all_unexpired_sessions_for_user(user):
    user_sessions = []
    all_sessions  = Session.objects.filter(expire_date__gte=datetime.datetime.now())
    for session in all_sessions:
        session_data = session.get_decoded()
        if user.pk == session_data.get('_auth_user_id'):
            user_sessions.append(session)
    return user_sessions

def delete_all_unexpired_sessions_for_user(user, session_to_omit=None):
    for session in all_unexpired_sessions_for_user(user):
        if session is not session_to_omit:
            session.delete()

A very simplified view:

# views.py

from django.http import HttpResponse
from django.shortcuts import render_to_response
from myapp.forms import ChangePasswordForm
from sessions_helpers import delete_all_unexpired_sessions_for_user

@never_cache
@login_required
def change_password(request):
    user = request.user

    if request.method == 'POST':
        form = ChangePasswordForm(data=request)

        if form.is_valid():
            user.set_password(form.get('password'))
            user.save()
            request.session.cycle_key()         # Flushes and replaces old key. Prevents replay attacks.
            delete_all_unexpired_sessions_for_user(user=user, session_to_omit=request.session)
            return HttpResponse('Success!')

    else:
        form = ChangePasswordForm()

    return render_to_response('change_password.html', {'form':form}, context_instance=RequestContext(request))

As you can see in sessions_helpers.py, I have to pull every unexpired session out of the DB, Session.objects.filter(expire_date__gte=datetime.datetime.now()), decode all of them, and then check to see if it matches a user or not. This will be extremely costly to the database if there are, say, 100,000+ sessions stored in there.

Is there a more-database-friendly way to do this? Is there a Sessions/Auth Middleware setting that'll let you store the username as a column in the Sessions table so I can run SQL against that, or will I have to modify Sessions to do that? Out-of-the-box it only has session_key, session_data, and expire_date columns.

Thanks for any insight or help you can offer. :)

6 Answers

We were in a similar situation where we had an SSO app that used diffrent kind of authentication/authorization solutions like OAuth, Token for CSR apps, and Cookie-Session for SSR apps. During logout we had to clear all session and tokens from all the apps to logout user realtime.

If you watch closely the source code of Session model in django you will find out all rows have a session_key. The main idea is to find the user's session_key in login then store it somewhere (it is better to be the user's model itself or a model that has a FK to it), then restore and delete session rows that have this key during logout.

Example:

# in model.py a regular User model
from django.contrib.postgres.fields import ArrayField

class User(AbstractUser):
    # other fields
    
    # This could be a JsonField to store other data of logedin user 
    # like IP or Agent to have more control on users logout
    session_keys = ArrayField(models.CharField(max_length=255), default=list)


# in views.py a simple login view
def login(request):
    form = LoginForm(request.POST or None, request=request)
    if form.is_valid():
        form.save()
        return redirect(request.GET.get('next'))

    context = {
        'form': form,
        'next': request.GET.get('next'),
    }
    return render(request, 'register.html', context)

# in forms.py a form that check regular password and user name checks


class LoginForm(forms.Form):
    username = forms.CharField(required=True)
    password = forms.CharField(required=True)

    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super().__init__(*args, **kwargs)

    def clean(self):
        # some check

    def save(self):
        # create a session for user
        # I had multiple backend if you have one session backend
        # there is no need to provide it
        login(self.request, self.user, backend='django.contrib.auth.backends.ModelBackend')
        
        # if everything be ok after creating session, login 
        # function will add created session instance to request 
        # object as a property and we can find its key
        # (it is little complicated then what I said...)
        self.user.session_keys.append(self.request.session.session_key)
        self.user.save()

# then again in views.py 
from django.contrib.sessions.models import Session

def logout(request):
    user = self.request.user
    Session.objects.filter(session_key__in=user.session_keys).delete()
    user.session_keys = []
    user.save()
    return render(request, 'logout.html')

This solution is for django 3 but for other versions may session behave diffrent

Related