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>