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
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()
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
In your settings.py
file, update your app's name to point to exact app in apps.py
INSTALLED_APPS = [
.....
'accounts.apps.AccountsConfig',
]
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
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'
]
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)
Thanks
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.
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.
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.
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"
i have added the app in settings.py but my signals are not working please help
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
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 ...?
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
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.
Hi Emmanuel,
thanks a lot for this tutorial. I have implemented it on one of my project and it works except in twp situations:
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?
request.user.is_authenticated always coming as false for me so i am not going inside that middleware what should i do
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.
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
Gracias Excelente. me ayudo bastante ✔😃👌
Thank you. I'm glad it was helpful to you. Gracias amigo
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?
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