Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to limit number of concurrent users logging in to same account in Django

My site is a digital marketplace website written in Django.

Digital content(text, images, videos) on the site is 'locked' by default. Only users who bought those content can view it.

There's a story that certain user(who bought the content) give away username/password for free to many people(1,000+ people in Facebook groups, for example). Those 1,000 users can then login using that single username/password and view the 'locked' digital content without paying a cent.

Is it possible to limit number of concurrent login to the same account?

I've found this package:

https://github.com/pcraston/django-preventconcurrentlogins

but what it does is logging previous user out when someone logged in using the same username/password. That would not help because each user only need to type in username/password each time to access 'locked' content.

like image 301
Kitti Wateesatogkij Avatar asked Nov 02 '16 11:11

Kitti Wateesatogkij


3 Answers

To limit the concurrent users, keep an eye on the existing sessions.

In your current approach, when a user logs in, a new session is created. That new session co-exists with the older sessions, so you have N concurrent sessions at the same time.

You want to allow a single session. The easiest approach would be to invalidate older session when a new login happens:

  • detect/extend the login event (use the "user_logged_in" signal)
  • for each login, remove the other existing sessions from the same user (see "Clearing the session store")

Other (more complete, but more complex) approaches would be using Two-factor authentication, blocking per IP, throttling the login event, requiring email confirmation, etc...

like image 133
David Arcos Avatar answered Oct 17 '22 14:10

David Arcos


Store User-Session mapping in another model.

from django.conf import settings
from django.contrib.sessions.models import Session
from django.db import models

class UserSessions(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='user_sessions')
    session = models.OneToOneField(Session, related_name='user_sessions',
                                   on_delete=models.CASCADE)

    def __str__(self):
        return '%s - %s' % (self.user, self.session.session_key)

If you have your own login view, you can update this model yourself:

from django.contrib.auth.views import login as auth_login

def login(request):
    auth_login(request)
    if request.user.is_authenticated():
        session = Session.objects.get(session_key=request.session.session_key)
        user_session = UserSession.objects.create(user=request.user, session=session)
    no_of_logins = request.user.user_sessions.count()
    if no_of_logins > 1:  # whatever your limit is
        request.SESSION['EXTRA_LOGIN'] = True
        # Do your stuff here

Other option is to use Signal. Django provides signals: user_logged_in, user_login_failed and user_logged_out, if you use the Django login view, that is.

# signals.py
from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver

@receiver(user_logged_in)
def concurrent_logins(sender, **kwargs):
    user = kwargs.get('user')
    request = kwargs.get('request')
    if user is not None and request is not None:
        session = Session.objects.get(session_key=request.session.session_key)
        UserSessions.objects.create(user=user, session=session)
    if user is not None:
        request.session['LOGIN_COUNT'] = user.user_sessions.count()

# your login view
def login(request):
     auth_login(request)
     if request.user.is_authenticated() and request.session['LOGIN_COUNT'] > 1:
         # 'LOGIN_COUNT' populated by signal
         request.session['EXTRA_LOGIN'] = True
         # Do your stuff

If EXTRA_LOGIN is True, you can list the previous sessions, and ask the user to choose which sessions to logout. (Don't stop him from logging in, else he might be locked out - if he doesn't have access to his previous sessions now)

like image 3
jatinderjit Avatar answered Oct 17 '22 15:10

jatinderjit


1 In your users/profiles app add a management command file

To add managment command, follow this guide: https://docs.djangoproject.com/en/1.10/howto/custom-management-commands/

2 The management command code: kills all sessions from users that have more then 10 sessions, you may change that to 1K if needed, or send this value as a param to the management command

from django.core.management.base import BaseCommand, CommandError
from django.contrib.sessions.models import Session
from django.contrib.auth.models import User

class Command(BaseCommand):
    def handle(self, *args, **options):
        session_user_dict = {}


        # users with more than 10 sessions - del all

        for ses in Session.objects.all():
            data = ses.get_decoded()
            user_owner = User.objects.filter(pk = data.get('_auth_user_id', None))

            if int(data.get('_auth_user_id', None)) in session_user_dict:
                session_user_dict[int(data.get('_auth_user_id', None))] += 1
            else:
                session_user_dict[int(data.get('_auth_user_id', None))] = 1

        for k,v in session_user_dict.iteritems():
            if v > 10:
                for ses in Session.objects.all():
                    data = ses.get_decoded()
                    if str(k) == data.get('_auth_user_id', None):
                        ses.delete()

3 Optional password change- after killing the bad-users sessions -change the bad users password to a diff one. To do that change the last loop in the above code

            for k,v in session_user_dict.iteritems():
            if v > 10:
                for ses in Session.objects.all():
                    data = ses.get_decoded()
                    if str(k) == data.get('_auth_user_id', None):
                        ses.delete()
                        theuser =  User.objects.filter(pk=k)
                        #maybe use uuid to pick a password ...
                        theuser.set_password('new_unknown_password')

4 Add a django management command to crontab every minute / hour or whenever use this guide: https://www.cyberciti.biz/faq/how-do-i-add-jobs-to-cron-under-linux-or-unix-oses/

if you are using a virtual env , remember that a management command that runs from cron needs to enter to the virtual env first, you may do it with a .sh script, ask for help if needed

like image 3
Ohad the Lad Avatar answered Oct 17 '22 15:10

Ohad the Lad