8

I am using a gunicorn server in which I am trying to figure out a way to limit only one session per username i.e. if user A is logged in to the app from Chrome he should not be able to login through Firefox unless he logs out of chrome, or shouldn`t be able to open another TAB in chrome itself.

How can I generate a unique id for the browser and store it in a DB so that until the user logs out or session expires, the user can`t login through any other browser.

Sergio Ayestarán
  • 5,590
  • 4
  • 38
  • 62
RamPrasadBismil
  • 579
  • 2
  • 10
  • 30

2 Answers2

12

A possible method of limiting sessions to a single tab involves creating a random token on page load and embedding this token into the page. This most recently generated token gets stored in the user's session as well. This will be similar to how various frameworks add validation tokens to prevent CSFR attacks.

Brief example:

  • User loads page in tab 1 in Firefox. Token1 is generated, embedded and stored in session
  • User loads page in tab 2 in Firefox. Token2 is generated, embedded and stored in session. This overwrites previous value.
  • User loads page in tab 1 in Chrome. Token3 is generated, embedded and stored in session. this overwrites previous value.

At this point, the user has the page open in 3 tabs. The user's session, though, only has Token3 stored. This method prevents the user from being locked out (different IP addresses, different user agent strings, incogneto mode, etc) because each new session simply generates a new token. The newest load becomes the active window, immediately invalidating all previous sessions.

Next, any time the page interacts with the server (clicks a link, submits data, etc.), the token embedded in the page is sent as well. The server validates that the passed token matches the token in the session. If they match, the action succeeds. If they do not match, the server returns a failure message.


You can generate random numbers in multiple ways, but you probably want something secure. We'll use the example from another question:

import string
import random
...
N = 20  # Length of the string we want, adjust as appropriate
''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(N))

This uses random.SystemRandom, which is more secure than simply using random.choice


On page load, you need to check if the existing token is valid, generate the random token and store this in the user's session. Since we want this everywhere, let's make a decorator first, to reduce duplicate code later. The decorator checks if the session is valid and if not you get to select what to do (insert your own logic). It also sets a session token. This is needed (or you need logic to exclude your main page) otherwise you'll hit an infinite loop where the user attempts to load the main page, doesn't have a token, fails and the process repeats. I have the token regenerating on each page load via the else clause. If you do not implement the if section, this decorator is pointless as both paths perform the same action and simply reset the token on page load. The logic in the if is what will prevent the user from having multiple sessions.

from flask import session
from functools import wraps

def random_string(length):
    return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))

def validate_token(f):
    @wraps(f)
    def wrapper(existing_token, *args, **kwargs):
        if session['token'] != existing_token:
            # Logic on failure. Do you present a 404, do you bounce them back to your main page, do you do something else?
            # It is IMPORTANT that you determine and implement this logic
            # otherwise the decorator simply changes the token (and behaves the same way as the else block).
            session['token'] = random_string(20)
        else:
            session['token'] = random_string(20)
        return f(*args, **kwargs)
    return wrapper

Now in our routes, we can apply this decorator to each, so that the user session gets updated on each page load:

from flask import render_template

@app.route('/path')
@validate_token
def path(token=None):
    return render_template('path.html', token=session['token'])

In your template, you want to utilize this token value anywhere you need to prevent the session from continuing. For example, put it on links, in forms (though Flask has a method of CSRF protection already), etc. The server itself can check if the passed token is valid. The template could look as simple as this:

<a href="{{ url_for('path', token=token) }}">Click here to continue</a> 
Community
  • 1
  • 1
Andy
  • 49,085
  • 60
  • 166
  • 233
2

I don't know how to implement it exactly, but it seems that you need websockets (See here)

This way, on every page load, you could have an Ajax request to the server, which would use the websocket functionality to query the previously opened browser+tab, if any. If it has an answer, it means that there is another browser+tab open. The server should then should return this information.

Your script which called the server through Ajax, depending on the return, should then decide to redirect to an error page, or to continue loading it.

I never used Websocket, but I'm pretty confident this would work (as is is well implemented in most browsers now).

BriceP
  • 508
  • 2
  • 13