Perhaps this is completely normal behaviour, but I feel like the django_session table is much larger than it should have to be.
First of all, I run the following cleanup command daily so the size is not caused by expired sessions:
DELETE FROM %s WHERE expire_date < NOW()
The numbers:
We've got about 5000 unique visitors (bots excluded) every day.
The SESSION_COOKIE_AGE is set to the default, 2 weeks
The table has a little over 1,000,000 rows
So, I'm guessing that Django also generates session keys for all bots that visits the site and that the bots don't store the cookies so it continuously generates new cookies.
But... is this normal behaviour? Is there a setting so Django won't generate sessions for anonymous users, or atleast... no sessions for users that aren't using sessions?
After a bit of debugging I've managed to trace cause of the problem.
One of my middlewares (and most of my views) have a request.user.is_authenticated() in them.
The django.contrib.auth middleware sets request.user to LazyUser()
Source: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/middleware.py?rev=14919#L13 (I don't see why there is a return None there, but ok...)
class AuthenticationMiddleware(object):
def process_request(self, request):
assert hasattr(request, 'session'), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
request.__class__.user = LazyUser()
return None
The LazyUser calls get_user(request) to get the user:
Source: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/middleware.py?rev=14919#L5
class LazyUser(object):
def __get__(self, request, obj_type=None):
if not hasattr(request, '_cached_user'):
from django.contrib.auth import get_user
request._cached_user = get_user(request)
return request._cached_user
The get_user(request) method does a user_id = request.session[SESSION_KEY]
Source: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/init.py?rev=14919#L100
def get_user(request):
from django.contrib.auth.models import AnonymousUser
try:
user_id = request.session[SESSION_KEY]
backend_path = request.session[BACKEND_SESSION_KEY]
backend = load_backend(backend_path)
user = backend.get_user(user_id) or AnonymousUser()
except KeyError:
user = AnonymousUser()
return user
Upon accessing the session sets accessed to true:
Source: http://code.djangoproject.com/browser/django/trunk/django/contrib/sessions/backends/base.py?rev=14919#L183
def _get_session(self, no_load=False):
"""
Lazily loads session from storage (unless "no_load" is True, when only
an empty dict is stored) and stores it in the current instance.
"""
self.accessed = True
try:
return self._session_cache
except AttributeError:
if self._session_key is None or no_load:
self._session_cache = {}
else:
self._session_cache = self.load()
return self._session_cache
And that causes the session to initialize. The bug was caused by a faulty session backend that also generates a session when accessed is set to true...
Is it possible for robots to access any page where you set anything in a user session (even for anonymous users), or any page where you use session.set_test_cookie() (for example Django's default login view in calls this method)? In both of these cases a new session object is created. Excluding such URLs in robots.txt should help.
For my case, I wrongly set SESSION_SAVE_EVERY_REQUEST = True in settings.py without understanding the exact meaning.
Then every request to my django service would generate a session entry, especially the heartbeat test request from upstream load balancers. After several days' running, django_session table turned to a huge one.
Django offers a management command to cleanup these expired sessions!
Related
I have a cart where I have two fields that I use to connect to a user. One is User, I use that if user is logged in. Other is session, this is used to keep track of logged out users. It uses session_key.
Now the problem I am facing is that when a user logs in the cart disconnects because the session_key has changed. I know that I can use cookie to identify the browser or client. But I am not able to find an example of Django's set_cookies being used with JsonResponse(AJAX call). It is only for HttpResponse.
I think I could use either of these two ways, if possible.
Set cookie through AJAX
Or Set cookie when user visits website. I want this with ability that no matter which page the user visits at fist the cookie should be set on that visit.
Does anyone have a resource or example to achieve this?
Thank you
Answering my own question in case this might help someone.
I went with the point# 2 mentioned in my question. What I needed was Middleware. They allow us to inject data in request and response objects for our views.
In myproject/cart/middleware.py I have this:
from . import settings as cart_settings
import uuid
class SetCartSessionKeyMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
cart_key = request.COOKIES.get(cart_settings.CART_SESSION_ID_KEY, None)
if cart_key:
response = self.get_response(request)
else:
cart_id = str(uuid.uuid4())
response = self.get_response(request)
response.set_cookie(cart_settings.CART_SESSION_ID_KEY, cart_id)
return response
In my settings.py I have:
CART_SESSION_ID_KEY = 'user_cart_id'
Now you can access that cookie anywhere where you have access to request object like in your views. You can do session_id = request.COOKIES.get(cart_settings.CART_SESSION_ID_KEY, None)
Don't forget to import settings in views as you did in the middleware.py file. Remember that it is my app settings.py(ie. myproject/cart/settings.py) not the main project settings.py.
In your myproject/settings.py register your new middleware like this
MIDDLEWARE = [
'django.middleware.gzip.GZipMiddleware',
....
## Our custom middlewares
'cart.middleware.SetCartSessionKeyMiddleware',
]
Enjoy!
I'm developing a Django (2.2.3) application with Django Microsoft Auth installed to handle SSO with Azure AD. I've been able to follow the quickstart documentation to allow me to log into the Django Admin panel by either using my Microsoft identity, or a standard username and password I've added to the Django user table. This all works out of the box and is fine.
My question put (really) simply is "What do I do next?". From a user's perspective, I'd like them to:
Navigate to my application (example.com/ or example.com/content) - Django will realise they aren't authenticated, and either
automatically redirect them to the SSO portal in the same window, or
redirect them to example.com/login, which requires them to click a button that will open the SSO
portal in a window (which is what happens in the default admin case)
Allow them to sign in and use MFA with their Microsoft Account
Once successful redirect them to my #login_required pages (example.com/content)
Currently, at the root of my navigation (example.com/), I have this:
def index(request):
if request.user.is_authenticated:
return redirect("/content")
else:
return redirect("/login")
My original idea was to simply change the redirect("/login") to redirect(authorization_url) - and this is where my problems start..
As far as I can tell, there isn't any way to get the current instance(?) of the context processor or backend of the microsoft_auth plugin to call the authorization_url() function and redirect the user from views.py.
Ok... Then I thought I'd just instantiate the MicrosoftClient class that generates the auth URL. This didn't work - not 100% sure why, but it think it may have something to do with the fact that some state variable used by the actual MicrosoftClient instance on the backend/context processor is inconsistent with my instance.
Finally, I tried to mimic what the automatic /admin page does - present an SSO button for the user to click, and open the Azure portal in a separate window. After digging around a bit, I realise that I fundamentally have the same problem - the auth URL is passed into the admin login page template as inline JS, which is later used to create the Azure window asynchronously on the client side.
As a sanity check, I tried to manually navigate to the auth URL as it is presented in the admin login page, and that did work (though the redirect to /content didn't).
At this point, given how difficult I think I'm making it for myself, I'm feel like I'm going about this whole thing completely the wrong way. Sadly, I can't find any documentation on how to complete this part of the process.
So, what am I doing wrong?!
A couple more days at this and I eventually worked out the issues myself, and learned a little more about how Django works too.
The link I was missing was how/where context processors from (third party) Django modules pass their context's through to the page that's eventually rendered. I didn't realise that variables from the microsoft_auth package (such as the authorisation_url used in its template) were accessible to me in any of my templates by default as well. Knowing this, I was able to implement a slightly simpler version of the same JS based login process that the admin panel uses.
Assuming that anyone reading this in the future is going through the same (learning) process I have (with this package in particular), I might be able to guess at the next couple of questions you'll have...
The first one was "I've logged in successfully...how do I do anything on behalf of the user?!". One would assume you'd be given the user's access token to use for future requests, but at the time of writing this package didn't seem to do it in any obvious way by default. The docs for the package only get you as far as logging into the admin panel.
The (in my opinion, not so obvious) answer is that you have to set MICROSOFT_AUTH_AUTHENTICATE_HOOK to a function that can be called on a successful authentication. It will be passed the logged in user (model) and their token JSON object for you to do with as you wish. After some deliberation, I opted to extend my user model using AbstractUser and just keep each user's token with their other data.
models.py
class User(AbstractUser):
access_token = models.CharField(max_length=2048, blank=True, null=True)
id_token = models.CharField(max_length=2048, blank=True, null=True)
token_expires = models.DateTimeField(blank=True, null=True)
aad.py
from datetime import datetime
from django.utils.timezone import make_aware
def store_token(user, token):
user.access_token = token["access_token"]
user.id_token = token["id_token"]
user.token_expires = make_aware(datetime.fromtimestamp(token["expires_at"]))
user.save()
settings.py
MICROSOFT_AUTH_EXTRA_SCOPES = "User.Read"
MICROSOFT_AUTH_AUTHENTICATE_HOOK = "django_app.aad.store_token"
Note the MICROSOFT_AUTH_EXTRA_SCOPES setting, which might be your second/side question - The default scopes set in the package as SCOPE_MICROSOFT = ["openid", "email", "profile"], and how to add more isn't made obvious. I needed to add User.Read at the very least. Keep in mind that the setting expects a string of space separated scopes, not a list.
Once you have the access token, you're free to make requests to the Microsoft Graph API. Their Graph Explorer is extremely useful in helping out with this.
So I made this custom view in Django based on https://github.com/Azure-Samples/ms-identity-python-webapp.
Hopefully, this will help someone.
import logging
import uuid
from os import getenv
import msal
import requests
from django.http import JsonResponse
from django.shortcuts import redirect, render
from rest_framework.generics import ListAPIView
logging.getLogger("msal").setLevel(logging.WARN)
# Application (client) ID of app registration
CLIENT_ID = "<appid of client registered in AD>"
TENANT_ID = "<tenantid of AD>"
CLIENT_SECRET = getenv("CLIENT_SECRET")
AUTHORITY = "https://login.microsoftonline.com/" + TENANT_ID
# This resource requires no admin consent
GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me'
SCOPE = ["User.Read"]
LOGIN_URI = "https://<your_domain>/login"
# This is registered as a redirect URI in app registrations in AD
REDIRECT_URI = "https://<your_domain>/authorize"
class Login(ListAPIView):
'''initial login
'''
def get(self, request):
session = request.session
id_token_claims = get_token_from_cache(session, SCOPE)
if id_token_claims:
access_token = id_token_claims.get("access_token")
if access_token:
graph_response = microsoft_graph_call(access_token)
if graph_response.get("error"):
resp = JsonResponse(graph_response, status=401)
else:
resp = render(request, 'API_AUTH.html', graph_response)
else:
session["state"] = str(uuid.uuid4())
auth_url = build_auth_url(scopes=SCOPE, state=session["state"])
resp = redirect(auth_url)
else:
session["state"] = str(uuid.uuid4())
auth_url = build_auth_url(scopes=SCOPE, state=session["state"])
resp = redirect(auth_url)
return resp
class Authorize(ListAPIView):
'''authorize after login
'''
def get(self, request):
session = request.session
# If states don't match login again
if request.GET.get('state') != session.get("state"):
return redirect(LOGIN_URI)
# Authentication/Authorization failure
if "error" in request.GET:
return JsonResponse({"error":request.GET.get("error")})
if request.GET.get('code'):
cache = load_cache(session)
result = build_msal_app(cache=cache).acquire_token_by_authorization_code(
request.GET['code'],
# Misspelled scope would cause an HTTP 400 error here
scopes=SCOPE,
redirect_uri=REDIRECT_URI
)
if "error" in result:
resp = JsonResponse({"error":request.GET.get("error")})
else:
access_token = result["access_token"]
session["user"] = result.get("id_token_claims")
save_cache(session, cache)
# Get user details using microsoft graph api call
graph_response = microsoft_graph_call(access_token)
resp = render(request, 'API_AUTH.html', graph_response)
else:
resp = JsonResponse({"login":"failed"}, status=401)
return resp
def load_cache(session):
'''loads from msal cache
'''
cache = msal.SerializableTokenCache()
if session.get("token_cache"):
cache.deserialize(session["token_cache"])
return cache
def save_cache(session,cache):
'''saves to msal cache
'''
if cache.has_state_changed:
session["token_cache"] = cache.serialize()
def build_msal_app(cache=None, authority=None):
'''builds msal cache
'''
return msal.ConfidentialClientApplication(
CLIENT_ID, authority=authority or AUTHORITY,
client_credential=CLIENT_SECRET, token_cache=cache)
def build_auth_url(authority=None, scopes=None, state=None):
'''builds auth url per tenantid
'''
return build_msal_app(authority=authority).get_authorization_request_url(
scopes or [],
state=state or str(uuid.uuid4()),
redirect_uri=REDIRECT_URI)
def get_token_from_cache(session, scope):
'''get accesstoken from cache
'''
# This web app maintains one cache per session
cache = load_cache(session)
cca = build_msal_app(cache=cache)
accounts = cca.get_accounts()
# So all account(s) belong to the current signed-in user
if accounts:
result = cca.acquire_token_silent(scope, account=accounts[0])
save_cache(session, cache)
return result
def microsoft_graph_call(access_token):
'''graph api to microsoft
'''
# Use token to call downstream service
graph_data = requests.get(
url=GRAPH_ENDPOINT,
headers={'Authorization': 'Bearer ' + access_token},
).json()
if "error" not in graph_data:
return {
"Login" : "success",
"UserId" : graph_data.get("id"),
"UserName" : graph_data.get("displayName"),
"AccessToken" : access_token
}
else:
return {"error" : graph_data}
I am using Flask-login with remember=False (the only cookie is the session cookie). When copy-pasting the session cookie after logging out, for some reason the session is still valid and the user is logged in. Even though the logged out session was deleted properly in the flask logout_user() function - meaning that the ["user_id"] was deleted from the session dictionary. It seems like the session is restored from the old cookie. can someone explain?
I do not really have a right answer for this yet, as I am investigating it myself, but there are a couple of points I would like to make here:
the logout_user() from Flask-login does not really seem to be invalidating the session. It just changes the 'session' cookie's value in the client (the browser). While in the backend this session is still alive.
An experiment to prove this would be: (a simple browser plugin like CookieManager can be used to perform this exercise)
login to the app
take a note of the 'session' cookie's value post successful login
now logout
now observer the 'session' cookie's value again. And you would
notice that it has now changed.
Replace this value with the 'session'cookie's value previously noted
in step 1 above.
Try visiting an internal authenticated page again.
Result : You would successfully be able to view an internal page without re-logging in, proving that the logout_user() never really invalidated the session but just changed the 'session' cookie in the client.
Howeverm, I am myself still taking a look into flask-login logout_user() definition and trying to make sense of it.
I had This issue too. After diagnosing what i found is the decorator #login_required *does not invalidate the User in server side after logout*, which is a security threat. This will be a cake walk for a Hacker to hack your application since they can easily extract all the request and header data from developer tool of your browser and can again send request to you server from outside of the application.For ex: If you have used any API in your application the it will be very easy for Hacker to get all the request data and resend a request using POSTMAN.
I solved this issue by creating a separate decorator "#authentication_required" and used in place of "#login_required". then it worked for me,though #login_required is supposed to do the same.
So basically while logging in i generated a random string(token) and sent to database and same string(token) is added to session of flask i.e session["token"]="akhfkjdbfnd334fndf" use any random string generator function.(session object is globally available if u r using flask . u can very well add any field to session). and while logout i again generate a string(token) and update the old token with newly generated token in database. So what #authentication_required will do is it will get the token from session object and the token which is present in database and try to compare the value. if both are same then only #authentication_required will let the client access api.and dont forget to do session.clear() after logout_user().
#---------------------------------------------------------------#
##authentication_required definition
def authentication_required(f):
#wraps(f)
def wrap(*args, **kwargs):
try:
user_id=session['user_id'] #assigning "user_id" from flask session object to a variable "user_id"
user=User_table.find_first(_id=user_id)#couhdb query syntax
#comparing api_token sent to session object at the login time with api_token in sent to database at login time. If both doesn't matches then user is redirected to login page.
if not session["token"]==user.token:
return redirect(url_for('login'))
else:
return f(*args, **kwargs)
except:
app.logger.info(Response('Request Did not came through header !', 401, {'WWW-Authenticate': 'Login failed'}))
return redirect(url_for('login_to system'))
return wrap
#---------------------------------------------------------------#
-------------------------------------------------------
login api code
#app.route('/login_to_system', methods=['GET', 'POST'])
def login_to_system():
form = LoginForm()
user = User_table.find_first(username=form.username.data)
login_user(user, remember=False)
try:
#Generating an random api_token at login time and will send to db
token_string=''.join(random.choices(string.ascii_uppercase + string.digits, k=14))
user.token=token_string #assigning token_string value to field api_token in database.
user.save() #saving the value in user table(using couch Db You can follow syntax as per you DB)
except Exception as error:
app.logger.error(str(error))
app.logger.info("before setting api_token in session")
session["token"]= token_string #adding a "token" to session object
#app.logger.info("Rendering login form")
return render_template('login.html', title='Sign In', form=form)
#-------------------------------------------------------#
#-----------------------------------#
#logout api code
#app.route('/logout')
def logout():
try:
user=User_table.find_first(_id=user_id)
#Generating a random token while logging out and will overwrite eariler token sent at login time send to database.
user.token=token_string=''.join(random.choices(string.ascii_uppercase + string.digits, k=17))
user.save()
except Exception as error:
app.logger.error(str(error))
logout_user()
session.clear()#clearing session
return redirect(url_for('Home page'))
#-----------------------------------#
Note: Seems like login_required is not working fine for me thats why i had to create another decorator but login_required also does the same thing but its strange that it not working for me.
I'm trying to understand how session works in Django. Looking at the source code of SessionMiddleware:
class SessionMiddleware(object):
def process_request(self, request):
engine = import_module(settings.SESSION_ENGINE)
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
request.session = engine.SessionStore(session_key)
If I understand it correctly, for every request
SessionMiddleware.process_request() will get the sessionid from the
cookie and then create a new SessionStore instance using that
sessionid.
And when I looked at the source for __init__() of SessionStore and
SessionBase:
class SessionStore(SessionBase):
def __init__(self, session_key=None):
super(SessionStore, self).__init__(session_key)
class SessionBase(object):
def __init__(self, session_key=None):
self._session_key = session_key
self.accessed = False
self.modified = False
So basically SessionStore just creates a new instance without trying
to look up in the database to see whether a session with the specified sessionid already exists
or not. But shouldn't that be the whole point of session -- that for every
request Django needs to look up in the session database to see if the
session is already there? I'm guessing at some place this database lookup
takes place but I can't find it.
Can you tell me where can i find it? Or did I misunderstand how
session work in Django?
Thanks
The third line of SessionMiddleware invokes a specific session engine, which dictates which SessionStore to use.
If you go down into contrib/sessions/backends/base.py, you'll see the following code:
class SessionBase(object):
...
def __getitem__(self, key):
return self._session[key]
def _get_session(self, no_load=False):
"""
Lazily loads session from storage (unless "no_load" is True, when only
an empty dict is stored) and stores it in the current instance.
"""
self.accessed = True
try:
return self._session_cache
except AttributeError:
if self._session_key is None or no_load:
self._session_cache = {}
else:
self._session_cache = self.load()
return self._session_cache
_session = property(_get_session)
What this does is create a session proxy object, which the middleware has attached to the request. It does not load the session object from the database until you say:
x = request.session['key']
At which point, the __getitem__(self, key) attempts to dereference self._session, which (being a property) in turn returns a cached copy of the session dictionary for this transaction, or if no cache is available, fetches it from the store using the load() method. The load() method is implemented by specific child engines: database, file, cache, cache + db, etc.
SessionStore is a lightweight proxy for a full session; it only becomes a full session, hitting the database, when you need to read or write data to the session object associated with the key encoded in the session id cookie.
I would like to audit when a user has experienced an idle timeout in my Django application. In other words, if the user's session cookie's expiration date exceeds the SESSION_COOKIE_AGE found in settings.py, the user is redirected to the login page. When that occurs, an audit should also occur. By "audit", I mean a record should be written to my person.audit table.
Currently, I have configured some middleware to capture these events. Unfortunately, Django generates a new cookie when the user is redirected to the login page, so I cannot determine if the user was taken to the login page via an idle timeout or some other event.
From what I can tell, I would need to work with the "django_session" table. However, the records in this table cannot be associated with that user because the sessionid value in the cookie is reset when the redirect occurs.
I'm guessing I'm not the first to encounter this dilemma. Does anyone have insight into how to resolve the problem?
Update:
After a bit of testing, I realize that the code below doesn't answer your question. Although it works, and the signal handler gets called, prev_session_data if it exists, won't contain any useful information.
First, an inside peek at the sessions framework:
When a new visitor requests an application URL, a new session is generated for them - at this point, they're still anonymous (request.user is an instance of AnonymousUser).
If they request a view that requires authentication, they're redirected to the login view.
When the login view is requested, it sets a test value in the user's session (SessionStore._session); this automatically sets the accessed and modified flags on the current session.
During the response phase of the above request, the SessionMiddleware saves the current session, effectively creating a new Session instance in the django_session table (if you're using the default database-backed sessions, provided by django.contrib.sessions.backends.db). The id of the new session is saved in the settings.SESSION_COOKIE_NAME cookie.
When the user types in their username and password and submits the form, they are authenticated. If authentication succeeds, the login method from django.contrib.auth is called. login checks if the current session contains a user ID; if it does, and the ID is the same as the ID of the logged in user, SessionStore.cycle_key is called to create a new session key, while retaining the session data. Otherwise, SessionStore.flush is called, to remove all data and generate a new session. Both these methods should delete the previous session (for the anonymous user), and call SessionStore.create to create a new session.
At this point, the user is authenticated, and they have a new session. Their ID is saved in the session, along with the backend used to authenticate them. The session middleware saves this data to the database, and saves their new session ID in settings.SESSION_COOKIE_NAME.
So you see, the big problem with the previous solution is by the time create gets called (step 5.), the previous session's ID is long gone. As others have pointed out, this happens because once the session cookie expires, it is silently deleted by the browser.
Building on Alex Gaynor's suggestion, I think I've come up with another approach, that seems to do what you're asking, though it's still a little rough around the edges. Basically, I use a second long-lived "audit" cookie, to mirror the session ID, and some middleware to check for the presence of that cookie. For any request:
if neither the audit cookie nor the session cookie exist, this is probably a new user
if the audit cookie exists, but the session cookie doesn't, this is probably a user whose session just expired
if both cookies exist, and have the same value, this is an active session
Here's the code so far:
sessionaudit.middleware.py:
from django.conf import settings
from django.db.models import signals
from django.utils.http import cookie_date
import time
session_expired = signals.Signal(providing_args=['previous_session_key'])
AUDIT_COOKIE_NAME = 'sessionaudit'
class SessionAuditMiddleware(object):
def process_request(self, request):
# The 'print' statements are helpful if you're using the development server
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
audit_cookie = request.COOKIES.get(AUDIT_COOKIE_NAME, None)
if audit_cookie is None and session_key is None:
print "** Got new user **"
elif audit_cookie and session_key is None:
print "** User session expired, Session ID: %s **" % audit_cookie
session_expired.send(self.__class__, previous_session_key=audit_cookie)
elif audit_cookie == session_key:
print "** User session active, Session ID: %s **" % audit_cookie
def process_response(self, request, response):
if request.session.session_key:
audit_cookie = request.COOKIES.get(AUDIT_COOKIE_NAME, None)
if audit_cookie != request.session.session_key:
# New Session ID - update audit cookie:
max_age = 60 * 60 * 24 * 365 # 1 year
expires_time = time.time() + max_age
expires = cookie_date(expires_time)
response.set_cookie(
AUDIT_COOKIE_NAME,
request.session.session_key,
max_age=max_age,
expires=expires,
domain=settings.SESSION_COOKIE_DOMAIN,
path=settings.SESSION_COOKIE_PATH,
secure=settings.SESSION_COOKIE_SECURE or None
)
return response
audit.models.py:
from django.contrib.sessions.models import Session
from sessionaudit.middleware import session_expired
def audit_session_expire(sender, **kwargs):
try:
prev_session = Session.objects.get(session_key=kwargs['previous_session_key'])
prev_session_data = prev_session.get_decoded()
user_id = prev_session_data.get('_auth_user_id')
except Session.DoesNotExist:
pass
session_expired.connect(audit_session_expire)
settings.py:
MIDDLEWARE_CLASSES = (
...
'django.contrib.sessions.middleware.SessionMiddleware',
'sessionaudit.middleware.SessionAuditMiddleware',
...
)
INSTALLED_APPS = (
...
'django.contrib.sessions',
'audit',
...
)
If you're using this, you should implement a custom logout view, that explicitly deletes the audit cookie when the user logs out. Also, I'd suggest using the django signed-cookies middleware (but you're probably already doing that, aren't you?)
OLD:
I think you should be able to do this using a custom session backend. Here's some (untested) sample code:
from django.contrib.sessions.backends.db import SessionStore as DBStore
from django.db.models import signals
session_created = signals.Signal(providing_args=['previous_session_key', 'new_session_key'])
class SessionStore(DBStore):
"""
Override the default database session store.
The `create` method is called by the framework to:
* Create a new session, if we have a new user
* Generate a new session, if the current user's session has expired
What we want to do is override this method, so we can send a signal
whenever it is called.
"""
def create(self):
# Save the current session ID:
prev_session_id = self.session_key
# Call the superclass 'create' to create a new session:
super(SessionStore, self).create()
# We should have a new session - raise 'session_created' signal:
session_created.send(self.__class__, previous_session_key=prev_session_id, new_session_key=self.session_key)
Save the code above as 'customdb.py' and add that to your django project. In your settings.py, set or replace 'SESSION_ENGINE' with the path to the above file, e.g.:
SESSION_ENGINE = 'yourproject.customdb'
Then in your middleware, or models.py, provide a handler for the 'session_created' signal, like so:
from django.contrib.sessions.models import Session
from yourproject.customdb import session_created
def audit_session_expire(sender, **kwargs):
# remember that 'previous_session_key' can be None if we have a new user
try:
prev_session = Session.objects.get(kwargs['previous_session_key'])
prev_session_data = prev_session.get_decoded()
user_id = prev_session_data['_auth_user_id']
# do something with the user_id
except Session.DoesNotExist:
# new user; do something else...
session_created.connect(audit_session_expire)
Don't forget to include the app containing the models.py in INSTALLED_APPS.
I don't know about Django, but can you, simply create a non-persistent cookie, which stores the last access time to a page on your site (you update the cookie on each page load)
Then, on your login page, you can check if your user has your cookie, but no session, then, you know that the user's session has probably timed out. Since you have the time of the last access to a page on your site, you can also calculate, based on the duration of the session, if it has timed out.
SESSION_COOKIE_AGE = 1500 # 25 minutes
Put that in your settings and that should take care of that and expire the session.