views:

1397

answers:

2

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?

A: 

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.

Martin
I am an asp.net programmer, and this kind of problems happen too. (well, it's web development, so it's similar) and this is how I would try to solve this issue in asp.net, so I'm suggesting this answer.
Martin
+5  A: 

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:

  1. 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).
  2. If they request a view that requires authentication, they're redirected to the login view.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

elo80ka
I'm still confused. Where do I register the receiver for audit_session_expire()?
Huuuze
`audit_session_expire` is the handler for the custom `session_created` signal. I've updated the answer to show how it's registered.
elo80ka
I've followed your instructions and I never see the signal call the "audit_session_expire()" method. I have included it in my models.py file (in the "audit" application) and updated the INSTALLED_APPS. Even if it was working, wouldn't this code fire off if the user clicked "Logout"?
Huuuze
What version of django are you working with? I tested this in 1.1-alpha, but this should also work in 1.0.2 as the SessionStore API hasn't been changed. Could you update your question, and post your code so I can take a look?
elo80ka
I corrected the problem with the code not working -- it was a PYTHONPATH issue. Unfortunately, this code fires if the user explicitly logs out or is logged out based upon an expired session. I want to track the latter independently of the user logging out on his own.
Huuuze
BTW, feel free to join in on the conversation regarding this topic on the Django Google Group: http://groups.google.com/group/django-users/browse_thread/thread/7ada69977dcc13f0/b263f4268a684ea8#b263f4268a684ea8
Huuuze
I think we're getting closer. The process_response() generates an error: "AttributeError: 'WSGIRequest' object has no attribute 'session'". I'm working through that now.
Huuuze
Yikes. +1 for sheer amount of work put into this. :)
Paolo Bergantino
First, let me thank you for all of the work you've put into this problem. I have the code working, however, the process_request() call is made for each request on the page (which includes graphics). As a result, I have a log message for each item on the page. Any suggestions?
Huuuze
Hmm...are you serving images through your django process? If you are, is there some reason why (for instance, are the images generated dynamically)? If not, you should probably be using the static apache handler (assuming you're using apache, that is), or a separate lightweight server.
elo80ka
Gotcha...I was running on the local Django development server. It should be all good. A well earned green checkmark for all of your hard work!
Huuuze