DEV Community

Emmanuel Okiche
Emmanuel Okiche

Posted on

Prevent Multiple Sessions for a User in your Django Application

Welcome to my first tutorial on this platform. In this one i'm going to show you how to prevent a user account from having multiple sessions at the same time.
Whenever a user logs in, the previous sessions is deleted.
This approach could be useful in subscription based apps where you want a single access for the subscribing user.

Our Approach

For this tutorial, We'll be using django 2.0.4 and its middleware framework.
The middleware is simply the middle guy or lady in between django's request/response processing. Django has some middlewares which we already use in our projects by default.
The middleware framework is what we're going to use to check if the user has an existing session on our application whenever a user makes a request.
If a session, exists, it deletes it and assigns the new session from the request as the current session.

Note: "Django stores session data in the "django_session" table, so deleting a user's session from the database has the same effect as logging out a user."

Code Time

Too much talk, right? OK let's just code.
Ermm... one more thing. All the code below should be stored in one app. I believe you know this but there's no harm in stating the obvious sometimes.
Create an app in your django project called 'accounts' and follow this tutorial.

models.py

We need to create a model to store the list of users that are currently logged into our app

from django.conf import settings

User = settings.AUTH_USER_MODEL

# Model to store the list of logged in users
class LoggedInUser(models.Model):
    user = models.OneToOneField(User, related_name='logged_in_user')
    # Session keys are 32 characters long
    session_key = models.CharField(max_length=32, null=True, blank=True)

    def __str__(self):
        return self.user.username
Enter fullscreen mode Exit fullscreen mode

We created a user field with a OneToOneField so that a user can only have one entry in the table. In simple terms, this uniquely identifies a user in the table. In simpler terms, there are no duplicate entries for a user in the table. In the simplest term ['insert yours here']. The related_name attribute is used to give a more friendly name for reverse relationship to the User model (you'll see soon).
The session_key would be used to store the session_key. So sorry if you were expecting a simpler explanation. Those times are past.

signals.py

Create a new file, called signals.py in your app directory. Signals are used to broadcast events whenever an action occurs in our app.
For our app, we need to know whenever a user logs in and logs out. With the help of signals, we can know this and perform some actions (here, insert the user to the LoggedInUser table).

# Signals that fires when a user logs in and logs out

from django.contrib.auth import user_logged_in, user_logged_out
from django.dispatch import receiver
from accounts.models import LoggedInUser

@receiver(user_logged_in)
def on_user_logged_in(sender, request, **kwargs):
    LoggedInUser.objects.get_or_create(user=kwargs.get('user')) 


@receiver(user_logged_out)
def on_user_logged_out(sender, **kwargs):
    LoggedInUser.objects.filter(user=kwargs.get('user')).delete()
Enter fullscreen mode Exit fullscreen mode

We're using the inbuilt user_logged_in and user_logged_out signals which fires whenever a user successfully logs in and log outs respectively.
If you were wondering how we got the user' through the kwargs, wonder no more my friend. The user argument is always sent with the two signals mentioned above.

apps.py

Add the following code in your apps.py file to import your signals.

from django.apps import AppConfig
class AccountsConfig(AppConfig):
    name = 'accounts'
    # This function is the only new thing in this file
    # it just imports the signal file when the app is ready
    def ready(self):
        import accounts.signals
Enter fullscreen mode Exit fullscreen mode

In your settings.py file, update your app's name to point to exact app in apps.py

INSTALLED_APPS = [
    .....

    'accounts.apps.AccountsConfig',
]
Enter fullscreen mode Exit fullscreen mode

middleware.py

Create a file called middleware.py in your app directory.

#Session model stores the session data
from django.contrib.sessions.models import Session

class OneSessionPerUserMiddleware:
    # Called only once when the web server starts
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        if request.user.is_authenticated:
            stored_session_key = request.user.logged_in_user.session_key

            # if there is a stored_session_key  in our database and it is
            # different from the current session, delete the stored_session_key
            # session_key with from the Session table
            if stored_session_key and stored_session_key != request.session.session_key:
                Session.objects.get(session_key=stored_session_key).delete()

            request.user.logged_in_user.session_key = request.session.session_key
            request.user.logged_in_user.save()

        response = self.get_response(request)

        # This is where you add any extra code to be executed for each request/response after
        # the view is called.
        # For this tutorial, we're not adding any code so we just return the response

        return response
Enter fullscreen mode Exit fullscreen mode

Notice, request.user.logged_in_user.session_key we can use logged_in_user here because we defined it as the related name for the user field in your LoggedInUser model. If we didnt do that, the reverse relationship name would be loggedinuser which doesnt follow python's naming convention.

Finally, add this middleware we just created in your MIDDLEWARE list in your settings.py file

MIDDLEWARE = [
    ....

    'accounts.middleware.OneSessionPerUserMiddleware'

]
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this one we learnt how to prevent a user from having multiple sessions in your app. More like preventing bad people from streaming videos on 50 devices (50 friends and family's devices to be precise) from one subscribed account on your web app.

This tutorial was to show you how to create custom middlewares in your django app.
There are definitely other ways which might be better in achieving this. This might not be a perfect solution to your needs. The idea was just to show you how middlewares work.
Now go forth and explore!!
Thanks for reading.

Video Version

You know what to do after watching this video, right?
OK, let me just state the obvious once more. Hit the 'subscribe' button then click the bell icon. Thanks once again for reading.

Top comments (29)

Collapse
 
shivamrohilla profile image
Shivam Rohilla

Thanks

Collapse
 
rhymes profile image
rhymes

Nice! BTW in a real world app instead of logging out the other user(s) you would probably tell them "Hey, stop streaming 50 videos :D".

Having more than one session could be accidental: I'm logged in on the phone and on the computer and I would hate to be automatically logged out on the other device.

Collapse
 
fleepgeek profile image
Emmanuel Okiche

That's very true.
Thanks for your comment on my first post here. I really appreciate.
My approach could be useful when the partner to a cheat in a relationship picks up the phone and the cheat is logged in on my tinder-like app. The cheater could quickly rush to his/her computer and log out. (LIFE SAVER SCENARIO)
The idea was just to show how to create a custom middleware. Like i stated in while concluding, this might not be the best approach.

Collapse
 
techxhelp profile image
techxhelp

I am getting this error when I uploaded the site to pythonanywhere (It was working fine locally)

Environment:

Request Method: GET
Request URL: careerabacusgallery.pythonanywhere...

Django Version: 4.0.1
Python Version: 3.9.5
Installed Applications:
['onlinetest.apps.OnlinetestConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'onlinetest.middleware.OneSessionPerUserMiddleware']

Traceback (most recent call last):
File "/home/careerabacusgallery/.virtualenvs/test/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/home/careerabacusgallery/statelevel/onlinetest/middleware.py", line 19, in call
Session.objects.get(session_key=stored_session_key).delete()
File "/home/careerabacusgallery/.virtualenvs/test/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/home/careerabacusgallery/.virtualenvs/test/lib/python3.9/site-packages/django/db/models/query.py", line 439, in get
raise self.model.DoesNotExist(

Exception Type: DoesNotExist at /lgi
Exception Value: Session matching query does not exist.

Collapse
 
dhruv354 profile image
dhruv354

my signals.py

from django.contrib.auth import user_logged_in, user_logged_out
from django.dispatch import receiver
from .models import User, LoggedInUser
from django.contrib.sessions.models import Session
from accounts.models import User, LoggedInUser, UserSession

@receiver(user_logged_in)
def when_user_logs_in(sender, request, **kwargs):
print('user_logs_signal is called')
LoggedInUser.objects.get_or_create(user=kwargs.get('user'))

@receiver(user_logged_out)
def when_user_logs_out(sender, request, **kwargs):
print('user logs out signal iscalled')
LoggedInUser.objects.get_or_create(user=kwargs.get('user')).delete()

my models.py

class LoggedInUser(models.Model):
user = models.OneToOneField(User, related_name='logged_in_user', on_delete =models.CASCADE, null=True, blank=True)
session_key = models.CharField(max_length=32, null=True, blank=True)

my apps.py

from django.apps import AppConfig

class AccountsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "accounts"

def ready(self):
    import accounts.signals
Enter fullscreen mode Exit fullscreen mode

i have added the app in settings.py but my signals are not working please help

Collapse
 
olleugra profile image
olleugra

this one works for me:
class OneSessionPerUserMiddleware:
# Called only once when the web server starts
def init(self, get_response):
self.get_response = get_response

def call(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
if request.user.is_authenticated:
session_key = request.session.session_key

try:
logged_in_user = request.user.logged_in_user
stored_session_key = logged_in_user.session_key
# stored_session_key exists so delete it if it's different
if stored_session_key and stored_session_key != request.session.session_key:
Session.objects.get(session_key=stored_session_key).delete()
request.user.logged_in_user.session_key = request.session.session_key
request.user.logged_in_user.save()
except LoggedInUser.DoesNotExist:
LoggedInUser.objects.create(user=request.user, session_key=session_key)
stored_session_key = request.user.logged_in_user.session_key

# if there is a stored_session_key in our database and it is
# different from the current session, delete the stored_session_key
# session_key with from the Session table
if stored_session_key and stored_session_key != request.session.session_key:
Session.objects.get(session_key=stored_session_key).delete()

request.user.logged_in_user.session_key = request.session.session_key
request.user.logged_in_user.save()

response = self.get_response(request)

# This is where you add any extra code to be executed for each request/response after
# the view is called.
# For this tutorial, we're not adding any code so we just return the response

return response

Collapse
 
tlbrea profile image
Anthony Brea • Edited

class OneSessionPerUserMiddleware:

Called only once when the web server starts

def init(self, get_response):
self.get_response = get_response

How do you implement this code? Could you spell out where this code should reside? Which directories, files etc ...?

Collapse
 
pyb1l profile image
Papavassiliou Vassilis

Thanks for the tutorial,
Is there any way to enforce this behavior without user interaction to website?
For instance in livestream scenarios if a user logins with credentials from device A and shares credentials to device B, if user never interacts with device A both could watch the livestream

Collapse
 
fleepgeek profile image
Emmanuel Okiche

Good question.
The approach used here would log out the user whenever he/she makes any request.
For your streaming example, something like Django channels would need to be used.

So you can take the logic used in my middleware and integrate it with Django channels.
So immediately the user logs in from another device, you send the logout event to the first device and maybe redirect to the login page.

Django channels supports the standard Django authentication out of the box.

Collapse
 
lecarrou profile image
LECARROU

Hi Emmanuel,

thanks a lot for this tutorial. I have implemented it on one of my project and it works except in twp situations:

  1. if user open multiple tabs in the same browser
  2. if user change his password: error Session objects doesn't exist

The second issue is due to Django behavior when updating password that update session_key. So when we want to delete, previous session_key no more exist

I just add a condition to check for session_key exsit before deleting and it seems to works but need to investigate more in detail.

if stored_session_key and stored_session_key != request.session.session_key:
if Session.objects.filter(session_key=stored_session_key).exists():
Session.objects.get(session_key=stored_session_key).delete()

Do you had the same issues? How do you solve it?

Collapse
 
dhruv354 profile image
dhruv354

request.user.is_authenticated always coming as false for me so i am not going inside that middleware what should i do

Collapse
 
fleepgeek profile image
Emmanuel Okiche • Edited

It means you're not logged in.
To confirm if you're loggedn in print out request.user.
If it returns AnonymousUser then it means you're not logged in correctly.

Collapse
 
dhruv354 profile image
dhruv354

Yes that was my mistake i solved , please help me in my last error. Error is that my signals are not firing i had added the signals as mentioned, also i imported signals in apps.py and added my app in settings.py but they are not working please help me in this issue

Collapse
 
diegolipa profile image
Diego Frank Lipa Choque

Gracias Excelente. me ayudo bastante ✔😃👌

Collapse
 
fleepgeek profile image
Emmanuel Okiche

Thank you. I'm glad it was helpful to you. Gracias amigo

Collapse
 
chargetank profile image
Charge Tank

Is there a specific reason why you do this check with middleware (every request)? Why don't you just check once when the user logs in?

Collapse
 
fleepgeek profile image
Emmanuel Okiche

Did that for demonstration purpose just to show how middlewares work and how you could use them.
You could just trigger the check in a signal when the user logs in and it would still give you the same result.
I stated in the conclusion that this might not be the perfect solution for such feature.
Thanks for your comment and i'm glad you spotted that.
You're really smart.

All the best